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]); }); }); });