clone of repo on github
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.
 
 
 
 

420 lines
14 KiB

import { describe, it, expect, vi, beforeEach } from 'vitest';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import {
fetchTaggedEventsFromRelays,
findTaggedEventsInFetched,
fetchProfilesForNewEvents,
type TagExpansionResult
} 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()
};
// Mock the ndkInstance store
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[], onProgress: (fetched: number, total: number) => void) => {
// Simulate progress updates
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,
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 => p.id)).toContain('pub1');
expect(result.publications.map(p => 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,
debug
);
// Should exclude pub1 since it already exists
expect(result.publications).toHaveLength(2);
expect(result.publications.map(p => p.id)).not.toContain('pub1');
expect(result.publications.map(p => p.id)).toContain('pub2');
expect(result.publications.map(p => 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,
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 => p.id)).toContain('pub1');
expect(result.publications.map(p => p.id)).toContain('pub2');
// Should find content events for those publications
expect(result.contentEvents).toHaveLength(4);
expect(result.contentEvents.map(c => c.id)).toContain('content1');
expect(result.contentEvents.map(c => c.id)).toContain('content2');
expect(result.contentEvents.map(c => c.id)).toContain('content3');
expect(result.contentEvents.map(c => 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 => p.id)).not.toContain('pub1');
expect(result.publications.map(p => 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 => p.id)).toContain('pub1'); // bitcoin
expect(result.publications.map(p => p.id)).toContain('pub2'); // bitcoin
expect(result.publications.map(p => 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 => 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[],
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(
[],
[],
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,
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,
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 => c.id)).toContain('colon-content');
});
});
});