Files
grimoire/src/lib/nostr-utils.test.ts
2025-12-16 00:07:20 +01:00

421 lines
16 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { resolveFilterAliases } from "./nostr-utils";
import type { NostrFilter } from "@/types/nostr";
describe("resolveFilterAliases", () => {
describe("$me alias resolution", () => {
it("should replace $me with account pubkey in authors", () => {
const filter: NostrFilter = { authors: ["$me"] };
const accountPubkey = "a".repeat(64);
const result = resolveFilterAliases(filter, accountPubkey, []);
expect(result.authors).toEqual([accountPubkey]);
expect(result.authors).not.toContain("$me");
});
it("should replace $me with account pubkey in #p tags", () => {
const filter: NostrFilter = { "#p": ["$me"] };
const accountPubkey = "a".repeat(64);
const result = resolveFilterAliases(filter, accountPubkey, []);
expect(result["#p"]).toEqual([accountPubkey]);
expect(result["#p"]).not.toContain("$me");
});
it("should handle $me when no account is set", () => {
const filter: NostrFilter = { authors: ["$me"] };
const result = resolveFilterAliases(filter, undefined, []);
expect(result.authors).toEqual([]);
});
it("should preserve other pubkeys when resolving $me", () => {
const hex = "b".repeat(64);
const filter: NostrFilter = { authors: ["$me", hex] };
const accountPubkey = "a".repeat(64);
const result = resolveFilterAliases(filter, accountPubkey, []);
expect(result.authors).toContain(accountPubkey);
expect(result.authors).toContain(hex);
expect(result.authors).not.toContain("$me");
});
});
describe("$contacts alias resolution", () => {
it("should replace $contacts with contact pubkeys in authors", () => {
const filter: NostrFilter = { authors: ["$contacts"] };
const contacts = ["a".repeat(64), "b".repeat(64), "c".repeat(64)];
const result = resolveFilterAliases(filter, undefined, contacts);
expect(result.authors).toEqual(contacts);
expect(result.authors).not.toContain("$contacts");
});
it("should replace $contacts with contact pubkeys in #p tags", () => {
const filter: NostrFilter = { "#p": ["$contacts"] };
const contacts = ["a".repeat(64), "b".repeat(64)];
const result = resolveFilterAliases(filter, undefined, contacts);
expect(result["#p"]).toEqual(contacts);
expect(result["#p"]).not.toContain("$contacts");
});
it("should handle $contacts with empty contact list", () => {
const filter: NostrFilter = { authors: ["$contacts"] };
const result = resolveFilterAliases(filter, undefined, []);
expect(result.authors).toEqual([]);
});
it("should preserve other pubkeys when resolving $contacts", () => {
const hex = "d".repeat(64);
const filter: NostrFilter = { authors: ["$contacts", hex] };
const contacts = ["a".repeat(64), "b".repeat(64)];
const result = resolveFilterAliases(filter, undefined, contacts);
expect(result.authors).toContain(hex);
expect(result.authors).toContain(contacts[0]);
expect(result.authors).toContain(contacts[1]);
expect(result.authors).not.toContain("$contacts");
});
});
describe("combined $me and $contacts", () => {
it("should resolve both $me and $contacts in authors", () => {
const filter: NostrFilter = { authors: ["$me", "$contacts"] };
const accountPubkey = "a".repeat(64);
const contacts = ["b".repeat(64), "c".repeat(64)];
const result = resolveFilterAliases(filter, accountPubkey, contacts);
expect(result.authors).toContain(accountPubkey);
expect(result.authors).toContain(contacts[0]);
expect(result.authors).toContain(contacts[1]);
expect(result.authors).not.toContain("$me");
expect(result.authors).not.toContain("$contacts");
});
it("should resolve aliases in both authors and #p tags", () => {
const filter: NostrFilter = {
authors: ["$me"],
"#p": ["$contacts"],
};
const accountPubkey = "a".repeat(64);
const contacts = ["b".repeat(64), "c".repeat(64)];
const result = resolveFilterAliases(filter, accountPubkey, contacts);
expect(result.authors).toEqual([accountPubkey]);
expect(result["#p"]).toEqual(contacts);
});
it("should handle mix of aliases and regular pubkeys", () => {
const hex1 = "d".repeat(64);
const hex2 = "e".repeat(64);
const filter: NostrFilter = {
authors: ["$me", hex1, "$contacts"],
"#p": [hex2, "$me"],
};
const accountPubkey = "a".repeat(64);
const contacts = ["b".repeat(64), "c".repeat(64)];
const result = resolveFilterAliases(filter, accountPubkey, contacts);
expect(result.authors).toContain(accountPubkey);
expect(result.authors).toContain(hex1);
expect(result.authors).toContain(contacts[0]);
expect(result.authors).toContain(contacts[1]);
expect(result["#p"]).toContain(hex2);
expect(result["#p"]).toContain(accountPubkey);
});
});
describe("deduplication", () => {
it("should deduplicate when $me is in contacts", () => {
const accountPubkey = "a".repeat(64);
const contacts = [accountPubkey, "b".repeat(64), "c".repeat(64)];
const filter: NostrFilter = { authors: ["$me", "$contacts"] };
const result = resolveFilterAliases(filter, accountPubkey, contacts);
// Account pubkey should appear once (deduplicated)
const accountCount = result.authors?.filter(
(a) => a === accountPubkey,
).length;
expect(accountCount).toBe(1);
expect(result.authors?.length).toBe(3); // account + 2 other contacts
});
it("should deduplicate regular pubkeys that appear multiple times", () => {
const hex = "d".repeat(64);
const filter: NostrFilter = { authors: [hex, hex, hex] };
const result = resolveFilterAliases(filter, undefined, []);
expect(result.authors).toEqual([hex]);
});
it("should deduplicate across resolved contacts and explicit pubkeys", () => {
const hex1 = "a".repeat(64);
const hex2 = "b".repeat(64);
const contacts = [hex1, hex2, "c".repeat(64)];
const filter: NostrFilter = { authors: ["$contacts", hex1, hex2] };
const result = resolveFilterAliases(filter, undefined, contacts);
// Each pubkey should appear once
expect(result.authors?.filter((a) => a === hex1).length).toBe(1);
expect(result.authors?.filter((a) => a === hex2).length).toBe(1);
expect(result.authors?.length).toBe(3); // 3 unique contacts
});
});
describe("filter preservation", () => {
it("should preserve other filter properties", () => {
const filter: NostrFilter = {
authors: ["$me"],
kinds: [1, 3, 7],
limit: 50,
since: 1234567890,
"#t": ["nostr", "bitcoin"],
};
const accountPubkey = "a".repeat(64);
const result = resolveFilterAliases(filter, accountPubkey, []);
expect(result.kinds).toEqual([1, 3, 7]);
expect(result.limit).toBe(50);
expect(result.since).toBe(1234567890);
expect(result["#t"]).toEqual(["nostr", "bitcoin"]);
});
it("should not modify original filter", () => {
const filter: NostrFilter = { authors: ["$me"] };
const accountPubkey = "a".repeat(64);
resolveFilterAliases(filter, accountPubkey, []);
// Original filter should still have $me
expect(filter.authors).toContain("$me");
});
it("should handle filters without aliases", () => {
const hex = "a".repeat(64);
const filter: NostrFilter = {
authors: [hex],
kinds: [1],
};
const result = resolveFilterAliases(filter, "b".repeat(64), []);
expect(result.authors).toEqual([hex]);
expect(result.kinds).toEqual([1]);
});
it("should handle empty filter", () => {
const filter: NostrFilter = {};
const result = resolveFilterAliases(filter, "a".repeat(64), []);
expect(result).toEqual({});
});
});
describe("edge cases", () => {
it("should handle undefined authors array", () => {
const filter: NostrFilter = { kinds: [1] };
const result = resolveFilterAliases(filter, "a".repeat(64), []);
expect(result.authors).toBeUndefined();
});
it("should handle undefined #p array", () => {
const filter: NostrFilter = { authors: ["$me"] };
const accountPubkey = "a".repeat(64);
const result = resolveFilterAliases(filter, accountPubkey, []);
expect(result["#p"]).toBeUndefined();
});
it("should handle large contact lists", () => {
const contacts = Array.from({ length: 5000 }, (_, i) =>
i.toString(16).padStart(64, "0"),
);
const filter: NostrFilter = { authors: ["$contacts"] };
const result = resolveFilterAliases(filter, undefined, contacts);
expect(result.authors?.length).toBe(5000);
});
it("should handle mixed case aliases (case-insensitive)", () => {
// Aliases are case-insensitive for user convenience
const accountPubkey = "a".repeat(64);
const contacts = ["b".repeat(64), "c".repeat(64)];
const filter: NostrFilter = { authors: ["$Me", "$CONTACTS"] };
const result = resolveFilterAliases(filter, accountPubkey, contacts);
// These should be resolved despite case differences
expect(result.authors).toContain(accountPubkey);
expect(result.authors).toContain(contacts[0]);
expect(result.authors).toContain(contacts[1]);
expect(result.authors).not.toContain("$Me");
expect(result.authors).not.toContain("$CONTACTS");
});
});
describe("case-insensitive alias resolution", () => {
it("should resolve $ME (uppercase) in authors", () => {
const filter: NostrFilter = { authors: ["$ME"] };
const accountPubkey = "a".repeat(64);
const result = resolveFilterAliases(filter, accountPubkey, []);
expect(result.authors).toEqual([accountPubkey]);
expect(result.authors).not.toContain("$ME");
});
it("should resolve $Me (mixed case) in #p tags", () => {
const filter: NostrFilter = { "#p": ["$Me"] };
const accountPubkey = "a".repeat(64);
const result = resolveFilterAliases(filter, accountPubkey, []);
expect(result["#p"]).toEqual([accountPubkey]);
expect(result["#p"]).not.toContain("$Me");
});
it("should resolve $CONTACTS (uppercase) in authors", () => {
const filter: NostrFilter = { authors: ["$CONTACTS"] };
const contacts = ["a".repeat(64), "b".repeat(64)];
const result = resolveFilterAliases(filter, undefined, contacts);
expect(result.authors).toEqual(contacts);
expect(result.authors).not.toContain("$CONTACTS");
});
it("should resolve $Contacts (mixed case) in #P tags", () => {
const filter: NostrFilter = { "#P": ["$Contacts"] };
const contacts = ["a".repeat(64), "b".repeat(64)];
const result = resolveFilterAliases(filter, undefined, contacts);
expect(result["#P"]).toEqual(contacts);
expect(result["#P"]).not.toContain("$Contacts");
});
it("should handle multiple case variations in same filter", () => {
const filter: NostrFilter = {
authors: ["$me", "$ME", "$Me"],
"#p": ["$contacts", "$CONTACTS", "$Contacts"],
};
const accountPubkey = "a".repeat(64);
const contacts = ["b".repeat(64), "c".repeat(64)];
const result = resolveFilterAliases(filter, accountPubkey, contacts);
// Should deduplicate all variants of $me to single pubkey
expect(result.authors).toEqual([accountPubkey]);
// Should deduplicate all variants of $contacts
expect(result["#p"]).toEqual(contacts);
});
it("should handle sloppy typing with whitespace-like patterns", () => {
const filter: NostrFilter = {
authors: ["$ME", "$me", "$Me"],
"#P": ["$CONTACTS", "$contacts", "$Contacts"],
};
const accountPubkey = "a".repeat(64);
const contacts = ["b".repeat(64), "c".repeat(64)];
const result = resolveFilterAliases(filter, accountPubkey, contacts);
expect(result.authors?.length).toBe(1);
expect(result.authors).toContain(accountPubkey);
expect(result["#P"]).toEqual(contacts);
});
});
describe("uppercase #P tag resolution", () => {
it("should replace $me with account pubkey in #P tags", () => {
const filter: NostrFilter = { "#P": ["$me"] };
const accountPubkey = "a".repeat(64);
const result = resolveFilterAliases(filter, accountPubkey, []);
expect(result["#P"]).toEqual([accountPubkey]);
expect(result["#P"]).not.toContain("$me");
});
it("should replace $contacts with contact pubkeys in #P tags", () => {
const filter: NostrFilter = { "#P": ["$contacts"] };
const contacts = ["a".repeat(64), "b".repeat(64), "c".repeat(64)];
const result = resolveFilterAliases(filter, undefined, contacts);
expect(result["#P"]).toEqual(contacts);
expect(result["#P"]).not.toContain("$contacts");
});
it("should handle $me when no account is set in #P", () => {
const filter: NostrFilter = { "#P": ["$me"] };
const result = resolveFilterAliases(filter, undefined, []);
expect(result["#P"]).toEqual([]);
});
it("should preserve other pubkeys when resolving $me in #P", () => {
const hex = "b".repeat(64);
const filter: NostrFilter = { "#P": ["$me", hex] };
const accountPubkey = "a".repeat(64);
const result = resolveFilterAliases(filter, accountPubkey, []);
expect(result["#P"]).toContain(accountPubkey);
expect(result["#P"]).toContain(hex);
expect(result["#P"]).not.toContain("$me");
});
it("should handle mix of $me, $contacts, and regular pubkeys in #P", () => {
const hex1 = "d".repeat(64);
const filter: NostrFilter = { "#P": ["$me", hex1, "$contacts"] };
const accountPubkey = "a".repeat(64);
const contacts = ["b".repeat(64), "c".repeat(64)];
const result = resolveFilterAliases(filter, accountPubkey, contacts);
expect(result["#P"]).toContain(accountPubkey);
expect(result["#P"]).toContain(hex1);
expect(result["#P"]).toContain(contacts[0]);
expect(result["#P"]).toContain(contacts[1]);
});
it("should deduplicate when $me is in contacts for #P", () => {
const accountPubkey = "a".repeat(64);
const contacts = [accountPubkey, "b".repeat(64), "c".repeat(64)];
const filter: NostrFilter = { "#P": ["$me", "$contacts"] };
const result = resolveFilterAliases(filter, accountPubkey, contacts);
// Account pubkey should appear once (deduplicated)
const accountCount = result["#P"]?.filter(
(a) => a === accountPubkey,
).length;
expect(accountCount).toBe(1);
expect(result["#P"]?.length).toBe(3); // account + 2 other contacts
});
});
describe("mixed #p and #P tag resolution", () => {
it("should resolve aliases in both #p and #P independently", () => {
const filter: NostrFilter = {
"#p": ["$me"],
"#P": ["$contacts"],
};
const accountPubkey = "a".repeat(64);
const contacts = ["b".repeat(64), "c".repeat(64)];
const result = resolveFilterAliases(filter, accountPubkey, contacts);
expect(result["#p"]).toEqual([accountPubkey]);
expect(result["#P"]).toEqual(contacts);
});
it("should handle same aliases in both tags without interference", () => {
const filter: NostrFilter = {
"#p": ["$me", "$contacts"],
"#P": ["$me", "$contacts"],
};
const accountPubkey = "a".repeat(64);
const contacts = ["b".repeat(64), "c".repeat(64)];
const result = resolveFilterAliases(filter, accountPubkey, contacts);
// Both should have same resolved values
expect(result["#p"]).toContain(accountPubkey);
expect(result["#p"]).toContain(contacts[0]);
expect(result["#p"]).toContain(contacts[1]);
expect(result["#P"]).toContain(accountPubkey);
expect(result["#P"]).toContain(contacts[0]);
expect(result["#P"]).toContain(contacts[1]);
});
});
});