You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
518 lines
15 KiB
518 lines
15 KiB
import { beforeEach, describe, expect, it, vi } from "vitest"; |
|
import type { NDKEvent } from "@nostr-dev-kit/ndk"; |
|
import { |
|
fetchProfilesForNewEvents, |
|
fetchTaggedEventsFromRelays, |
|
findTaggedEventsInFetched, |
|
} from "../../src/lib/utils/tag_event_fetch"; |
|
|
|
// Mock NDKEvent for testing |
|
class MockNDKEvent { |
|
id: string; |
|
kind: number; |
|
pubkey: string; |
|
created_at: number; |
|
content: string; |
|
tags: string[][]; |
|
|
|
constructor( |
|
id: string, |
|
kind: number, |
|
pubkey: string, |
|
created_at: number, |
|
content: string = "", |
|
tags: string[][] = [], |
|
) { |
|
this.id = id; |
|
this.kind = kind; |
|
this.pubkey = pubkey; |
|
this.created_at = created_at; |
|
this.content = content; |
|
this.tags = tags; |
|
} |
|
|
|
tagValue(tagName: string): string | undefined { |
|
const tag = this.tags.find((t) => t[0] === tagName); |
|
return tag ? tag[1] : undefined; |
|
} |
|
|
|
getMatchingTags(tagName: string): string[][] { |
|
return this.tags.filter((tag) => tag[0] === tagName); |
|
} |
|
} |
|
|
|
// Mock NDK instance |
|
const mockNDK = { |
|
fetchEvents: vi.fn(), |
|
pool: {}, |
|
debug: false, |
|
mutedIds: new Set(), |
|
queuesZapConfig: {}, |
|
// Add other required properties as needed for the mock |
|
} as any; |
|
|
|
// Mock the ndkInstance store |
|
// TODO: Replace with getNdkContext mock. |
|
vi.mock("../../src/lib/ndk", () => ({ |
|
ndkInstance: { |
|
subscribe: vi.fn((fn) => { |
|
fn(mockNDK); |
|
return { unsubscribe: vi.fn() }; |
|
}), |
|
}, |
|
})); |
|
|
|
// Mock the profile cache utilities |
|
vi.mock("../../src/lib/utils/profileCache", () => ({ |
|
extractPubkeysFromEvents: vi.fn((events: NDKEvent[]) => { |
|
const pubkeys = new Set<string>(); |
|
events.forEach((event) => { |
|
if (event.pubkey) pubkeys.add(event.pubkey); |
|
}); |
|
return pubkeys; |
|
}), |
|
batchFetchProfiles: vi.fn( |
|
async ( |
|
pubkeys: string[], |
|
ndk: any, |
|
onProgress?: (fetched: number, total: number) => void, |
|
) => { |
|
// Simulate progress updates |
|
if (onProgress) { |
|
onProgress(0, pubkeys.length); |
|
onProgress(pubkeys.length, pubkeys.length); |
|
} |
|
return []; |
|
}, |
|
), |
|
})); |
|
|
|
describe("Tag Expansion Tests", () => { |
|
let mockPublications: MockNDKEvent[]; |
|
let mockContentEvents: MockNDKEvent[]; |
|
let mockAllEvents: MockNDKEvent[]; |
|
|
|
beforeEach(() => { |
|
vi.clearAllMocks(); |
|
|
|
// Create test publication index events (kind 30040) |
|
mockPublications = [ |
|
new MockNDKEvent("pub1", 30040, "author1", 1000, "Book 1", [ |
|
["t", "bitcoin"], |
|
["t", "cryptocurrency"], |
|
["a", "30041:author1:chapter-1"], |
|
["a", "30041:author1:chapter-2"], |
|
]), |
|
new MockNDKEvent("pub2", 30040, "author2", 1100, "Book 2", [ |
|
["t", "bitcoin"], |
|
["t", "blockchain"], |
|
["a", "30041:author2:chapter-1"], |
|
]), |
|
new MockNDKEvent("pub3", 30040, "author3", 1200, "Book 3", [ |
|
["t", "ethereum"], |
|
["a", "30041:author3:chapter-1"], |
|
]), |
|
]; |
|
|
|
// Create test content events (kind 30041) |
|
mockContentEvents = [ |
|
new MockNDKEvent( |
|
"content1", |
|
30041, |
|
"author1", |
|
1000, |
|
"Chapter 1 content", |
|
[["d", "chapter-1"]], |
|
), |
|
new MockNDKEvent( |
|
"content2", |
|
30041, |
|
"author1", |
|
1100, |
|
"Chapter 2 content", |
|
[["d", "chapter-2"]], |
|
), |
|
new MockNDKEvent( |
|
"content3", |
|
30041, |
|
"author2", |
|
1200, |
|
"Author 2 Chapter 1", |
|
[["d", "chapter-1"]], |
|
), |
|
new MockNDKEvent( |
|
"content4", |
|
30041, |
|
"author3", |
|
1300, |
|
"Author 3 Chapter 1", |
|
[["d", "chapter-1"]], |
|
), |
|
]; |
|
|
|
// Combine all events for testing |
|
mockAllEvents = [...mockPublications, ...mockContentEvents]; |
|
}); |
|
|
|
describe("fetchTaggedEventsFromRelays", () => { |
|
it("should fetch publications with matching tags from relays", async () => { |
|
// Mock the NDK fetch to return publications with 'bitcoin' tag |
|
const bitcoinPublications = mockPublications.filter((pub) => |
|
pub.tags.some((tag) => tag[0] === "t" && tag[1] === "bitcoin") |
|
); |
|
mockNDK.fetchEvents.mockResolvedValueOnce( |
|
new Set(bitcoinPublications as NDKEvent[]), |
|
); |
|
mockNDK.fetchEvents.mockResolvedValueOnce( |
|
new Set(mockContentEvents as NDKEvent[]), |
|
); |
|
|
|
const existingEventIds = new Set<string>(["existing-event"]); |
|
const baseEvents: NDKEvent[] = []; |
|
const debug = vi.fn(); |
|
|
|
const result = await fetchTaggedEventsFromRelays( |
|
["bitcoin"], |
|
existingEventIds, |
|
baseEvents, |
|
mockNDK as any, |
|
debug, |
|
); |
|
|
|
// Should fetch publications with bitcoin tag |
|
expect(mockNDK.fetchEvents).toHaveBeenCalledWith({ |
|
kinds: [30040], |
|
"#t": ["bitcoin"], |
|
limit: 30, |
|
}); |
|
|
|
// Should return the matching publications |
|
expect(result.publications).toHaveLength(2); |
|
expect(result.publications.map((p: any) => p.id)).toContain("pub1"); |
|
expect(result.publications.map((p: any) => p.id)).toContain("pub2"); |
|
|
|
// Should fetch content events for the publications |
|
expect(mockNDK.fetchEvents).toHaveBeenCalledWith({ |
|
kinds: [30041, 30818], |
|
"#d": ["chapter-1", "chapter-2"], |
|
}); |
|
}); |
|
|
|
it("should filter out existing events to avoid duplicates", async () => { |
|
mockNDK.fetchEvents.mockResolvedValueOnce( |
|
new Set(mockPublications as NDKEvent[]), |
|
); |
|
mockNDK.fetchEvents.mockResolvedValueOnce( |
|
new Set(mockContentEvents as NDKEvent[]), |
|
); |
|
|
|
const existingEventIds = new Set<string>(["pub1"]); // pub1 already exists |
|
const baseEvents: NDKEvent[] = []; |
|
const debug = vi.fn(); |
|
|
|
const result = await fetchTaggedEventsFromRelays( |
|
["bitcoin"], |
|
existingEventIds, |
|
baseEvents, |
|
mockNDK as any, |
|
debug, |
|
); |
|
|
|
// Should exclude pub1 since it already exists |
|
expect(result.publications).toHaveLength(2); |
|
expect(result.publications.map((p: any) => p.id)).not.toContain("pub1"); |
|
expect(result.publications.map((p: any) => p.id)).toContain("pub2"); |
|
expect(result.publications.map((p: any) => p.id)).toContain("pub3"); |
|
}); |
|
|
|
it("should handle empty tag array gracefully", async () => { |
|
// Mock empty result for empty tags |
|
mockNDK.fetchEvents.mockResolvedValueOnce(new Set()); |
|
|
|
const existingEventIds = new Set<string>(); |
|
const baseEvents: NDKEvent[] = []; |
|
const debug = vi.fn(); |
|
|
|
const result = await fetchTaggedEventsFromRelays( |
|
[], |
|
existingEventIds, |
|
baseEvents, |
|
mockNDK as any, |
|
debug, |
|
); |
|
|
|
expect(result.publications).toHaveLength(0); |
|
expect(result.contentEvents).toHaveLength(0); |
|
}); |
|
}); |
|
|
|
describe("findTaggedEventsInFetched", () => { |
|
it("should find publications with matching tags in already fetched events", () => { |
|
const existingEventIds = new Set<string>(["existing-event"]); |
|
const baseEvents: NDKEvent[] = []; |
|
const debug = vi.fn(); |
|
|
|
const result = findTaggedEventsInFetched( |
|
mockAllEvents as NDKEvent[], |
|
["bitcoin"], |
|
existingEventIds, |
|
baseEvents, |
|
debug, |
|
); |
|
|
|
// Should find publications with bitcoin tag |
|
expect(result.publications).toHaveLength(2); |
|
expect(result.publications.map((p: any) => p.id)).toContain("pub1"); |
|
expect(result.publications.map((p: any) => p.id)).toContain("pub2"); |
|
|
|
// Should find content events for those publications |
|
expect(result.contentEvents).toHaveLength(4); |
|
expect(result.contentEvents.map((c: any) => c.id)).toContain("content1"); |
|
expect(result.contentEvents.map((c: any) => c.id)).toContain("content2"); |
|
expect(result.contentEvents.map((c: any) => c.id)).toContain("content3"); |
|
expect(result.contentEvents.map((c: any) => c.id)).toContain("content4"); |
|
}); |
|
|
|
it("should exclude base events from search results", () => { |
|
const existingEventIds = new Set<string>(["pub1"]); // pub1 is a base event |
|
const baseEvents: NDKEvent[] = []; |
|
const debug = vi.fn(); |
|
|
|
const result = findTaggedEventsInFetched( |
|
mockAllEvents as NDKEvent[], |
|
["bitcoin"], |
|
existingEventIds, |
|
baseEvents, |
|
debug, |
|
); |
|
|
|
// Should exclude pub1 since it's a base event |
|
expect(result.publications).toHaveLength(1); |
|
expect(result.publications.map((p: any) => p.id)).not.toContain("pub1"); |
|
expect(result.publications.map((p: any) => p.id)).toContain("pub2"); |
|
}); |
|
|
|
it("should handle multiple tags (OR logic)", () => { |
|
const existingEventIds = new Set<string>(); |
|
const baseEvents: NDKEvent[] = []; |
|
const debug = vi.fn(); |
|
|
|
const result = findTaggedEventsInFetched( |
|
mockAllEvents as NDKEvent[], |
|
["bitcoin", "ethereum"], |
|
existingEventIds, |
|
baseEvents, |
|
debug, |
|
); |
|
|
|
// Should find publications with either bitcoin OR ethereum tags |
|
expect(result.publications).toHaveLength(3); |
|
expect(result.publications.map((p: any) => p.id)).toContain("pub1"); // bitcoin |
|
expect(result.publications.map((p: any) => p.id)).toContain("pub2"); // bitcoin |
|
expect(result.publications.map((p: any) => p.id)).toContain("pub3"); // ethereum |
|
}); |
|
|
|
it("should handle events without tags gracefully", () => { |
|
const eventWithoutTags = new MockNDKEvent( |
|
"no-tags", |
|
30040, |
|
"author4", |
|
1000, |
|
"No tags", |
|
); |
|
const allEventsWithNoTags = [...mockAllEvents, eventWithoutTags]; |
|
|
|
const existingEventIds = new Set<string>(); |
|
const baseEvents: NDKEvent[] = []; |
|
const debug = vi.fn(); |
|
|
|
const result = findTaggedEventsInFetched( |
|
allEventsWithNoTags as NDKEvent[], |
|
["bitcoin"], |
|
existingEventIds, |
|
baseEvents, |
|
debug, |
|
); |
|
|
|
// Should not include events without tags |
|
expect(result.publications.map((p: any) => p.id)).not.toContain("no-tags"); |
|
}); |
|
}); |
|
|
|
describe("fetchProfilesForNewEvents", () => { |
|
it("should extract pubkeys and fetch profiles for new events", async () => { |
|
const onProgressUpdate = vi.fn(); |
|
const debug = vi.fn(); |
|
|
|
await fetchProfilesForNewEvents( |
|
mockPublications as NDKEvent[], |
|
mockContentEvents as NDKEvent[], |
|
mockNDK as any, |
|
onProgressUpdate, |
|
debug, |
|
); |
|
|
|
// Should call progress update with initial state |
|
expect(onProgressUpdate).toHaveBeenCalledWith({ current: 0, total: 3 }); |
|
|
|
// Should call progress update with final state |
|
expect(onProgressUpdate).toHaveBeenCalledWith({ current: 3, total: 3 }); |
|
|
|
// Should clear progress at the end |
|
expect(onProgressUpdate).toHaveBeenCalledWith(null); |
|
}); |
|
|
|
it("should handle empty event arrays gracefully", async () => { |
|
const onProgressUpdate = vi.fn(); |
|
const debug = vi.fn(); |
|
|
|
await fetchProfilesForNewEvents( |
|
[], |
|
[], |
|
mockNDK as any, |
|
onProgressUpdate, |
|
debug, |
|
); |
|
|
|
// Should not call progress update for empty arrays |
|
expect(onProgressUpdate).not.toHaveBeenCalled(); |
|
}); |
|
}); |
|
|
|
describe("Tag Expansion Integration", () => { |
|
it("should demonstrate the complete tag expansion flow", async () => { |
|
// This test simulates the complete flow from the visualize page |
|
|
|
// Step 1: Mock relay fetch for 'bitcoin' tag |
|
const bitcoinPublications = mockPublications.filter((pub) => |
|
pub.tags.some((tag) => tag[0] === "t" && tag[1] === "bitcoin") |
|
); |
|
mockNDK.fetchEvents.mockResolvedValueOnce( |
|
new Set(bitcoinPublications as NDKEvent[]), |
|
); |
|
mockNDK.fetchEvents.mockResolvedValueOnce( |
|
new Set(mockContentEvents as NDKEvent[]), |
|
); |
|
|
|
const existingEventIds = new Set<string>(["base-event"]); |
|
const baseEvents: NDKEvent[] = []; |
|
const debug = vi.fn(); |
|
|
|
// Step 2: Fetch from relays |
|
const relayResult = await fetchTaggedEventsFromRelays( |
|
["bitcoin"], |
|
existingEventIds, |
|
baseEvents, |
|
mockNDK as any, |
|
debug, |
|
); |
|
|
|
expect(relayResult.publications).toHaveLength(2); |
|
expect(relayResult.contentEvents).toHaveLength(4); |
|
|
|
// Step 3: Search in fetched events |
|
const searchResult = findTaggedEventsInFetched( |
|
mockAllEvents as NDKEvent[], |
|
["bitcoin"], |
|
existingEventIds, |
|
baseEvents, |
|
debug, |
|
); |
|
|
|
expect(searchResult.publications).toHaveLength(2); |
|
expect(searchResult.contentEvents).toHaveLength(4); |
|
|
|
// Step 4: Fetch profiles |
|
const onProgressUpdate = vi.fn(); |
|
await fetchProfilesForNewEvents( |
|
relayResult.publications, |
|
relayResult.contentEvents, |
|
mockNDK as any, |
|
onProgressUpdate, |
|
debug, |
|
); |
|
|
|
expect(onProgressUpdate).toHaveBeenCalledWith(null); |
|
}); |
|
}); |
|
|
|
describe("Edge Cases and Error Handling", () => { |
|
it("should handle malformed a-tags gracefully", () => { |
|
const malformedPublication = new MockNDKEvent( |
|
"malformed", |
|
30040, |
|
"author1", |
|
1000, |
|
"Malformed", |
|
[ |
|
["t", "bitcoin"], |
|
["a", "invalid-tag-format"], // Missing parts |
|
["a", "30041:author1:chapter-1"], // Valid format |
|
], |
|
); |
|
|
|
const allEventsWithMalformed = [...mockAllEvents, malformedPublication]; |
|
const existingEventIds = new Set<string>(); |
|
const baseEvents: NDKEvent[] = []; |
|
const debug = vi.fn(); |
|
|
|
const result = findTaggedEventsInFetched( |
|
allEventsWithMalformed as NDKEvent[], |
|
["bitcoin"], |
|
existingEventIds, |
|
baseEvents, |
|
debug, |
|
); |
|
|
|
// Should still work and include the publication with valid a-tags |
|
expect(result.publications).toHaveLength(3); |
|
expect(result.contentEvents.length).toBeGreaterThan(0); |
|
}); |
|
|
|
it("should handle events with d-tags containing colons", () => { |
|
const publicationWithColonDTag = new MockNDKEvent( |
|
"colon-pub", |
|
30040, |
|
"author1", |
|
1000, |
|
"Colon d-tag", |
|
[ |
|
["t", "bitcoin"], |
|
["a", "30041:author1:chapter:with:colons"], |
|
], |
|
); |
|
|
|
const contentWithColonDTag = new MockNDKEvent( |
|
"colon-content", |
|
30041, |
|
"author1", |
|
1100, |
|
"Content with colon d-tag", |
|
[ |
|
["d", "chapter:with:colons"], |
|
], |
|
); |
|
|
|
const allEventsWithColons = [ |
|
...mockAllEvents, |
|
publicationWithColonDTag, |
|
contentWithColonDTag, |
|
]; |
|
const existingEventIds = new Set<string>(); |
|
const baseEvents: NDKEvent[] = []; |
|
const debug = vi.fn(); |
|
|
|
const result = findTaggedEventsInFetched( |
|
allEventsWithColons as NDKEvent[], |
|
["bitcoin"], |
|
existingEventIds, |
|
baseEvents, |
|
debug, |
|
); |
|
|
|
// Should handle d-tags with colons correctly |
|
expect(result.publications).toHaveLength(3); |
|
expect(result.contentEvents.map((c: any) => c.id)).toContain("colon-content"); |
|
}); |
|
}); |
|
});
|
|
|