import { PostgreSqlContainer, StartedPostgreSqlContainer, } from '@testcontainers/postgresql'; import postgres from 'postgres'; import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { DrizzleCardQueryRepository } from '../../infrastructure/repositories/DrizzleCardQueryRepository'; import { DrizzleCardRepository } from '../../infrastructure/repositories/DrizzleCardRepository'; import { CuratorId } from '../../domain/value-objects/CuratorId'; import { cards } from '../../infrastructure/repositories/schema/card.sql'; import { libraryMemberships } from '../../infrastructure/repositories/schema/libraryMembership.sql'; import { publishedRecords } from '../../infrastructure/repositories/schema/publishedRecord.sql'; import { CardBuilder } from '../utils/builders/CardBuilder'; import { URL } from '../../domain/value-objects/URL'; import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository'; import { createTestSchema } from '../test-utils/createTestSchema'; import { CardTypeEnum } from '../../domain/value-objects/CardType'; describe('DrizzleCardQueryRepository - getLibrariesForUrl', () => { let container: StartedPostgreSqlContainer; let db: PostgresJsDatabase; let queryRepository: DrizzleCardQueryRepository; let cardRepository: DrizzleCardRepository; // Test data let curator1: CuratorId; let curator2: CuratorId; let curator3: CuratorId; // Setup before all tests beforeAll(async () => { // Start PostgreSQL container container = await new PostgreSqlContainer('postgres:14').start(); // Create database connection const connectionString = container.getConnectionUri(); process.env.DATABASE_URL = connectionString; const client = postgres(connectionString); db = drizzle(client); // Create repositories queryRepository = new DrizzleCardQueryRepository(db); cardRepository = new DrizzleCardRepository(db); // Create schema using helper function await createTestSchema(db); // Create test data curator1 = CuratorId.create('did:plc:curator1').unwrap(); curator2 = CuratorId.create('did:plc:curator2').unwrap(); curator3 = CuratorId.create('did:plc:curator3').unwrap(); }, 60000); // Increase timeout for container startup // Cleanup after all tests afterAll(async () => { // Stop container await container.stop(); }); // Clear data between tests beforeEach(async () => { await db.delete(libraryMemberships); await db.delete(cards); await db.delete(publishedRecords); }); describe('getLibrariesForUrl', () => { it('should return all users who have cards with the specified URL', async () => { const testUrl = 'https://example.com/shared-article'; const url = URL.create(testUrl).unwrap(); // Create URL cards for different users with the same URL const card1 = new CardBuilder() .withCuratorId(curator1.value) .withType(CardTypeEnum.URL) .withUrl(url) .buildOrThrow(); const card2 = new CardBuilder() .withCuratorId(curator2.value) .withType(CardTypeEnum.URL) .withUrl(url) .buildOrThrow(); const card3 = new CardBuilder() .withCuratorId(curator3.value) .withType(CardTypeEnum.URL) .withUrl(url) .buildOrThrow(); // Add cards to their respective libraries card1.addToLibrary(curator1); card2.addToLibrary(curator2); card3.addToLibrary(curator3); // Save cards await cardRepository.save(card1); await cardRepository.save(card2); await cardRepository.save(card3); // Execute the query const result = await queryRepository.getLibrariesForUrl(testUrl, { page: 1, limit: 10, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); // Verify the result expect(result.items).toHaveLength(3); expect(result.totalCount).toBe(3); expect(result.hasMore).toBe(false); // Check that all three users are included const userIds = result.items.map((lib) => lib.userId); expect(userIds).toContain(curator1.value); expect(userIds).toContain(curator2.value); expect(userIds).toContain(curator3.value); // Check that card IDs are correct const cardIds = result.items.map((lib) => lib.cardId); expect(cardIds).toContain(card1.cardId.getStringValue()); expect(cardIds).toContain(card2.cardId.getStringValue()); expect(cardIds).toContain(card3.cardId.getStringValue()); }); it('should return empty result when no users have cards with the specified URL', async () => { const testUrl = 'https://example.com/nonexistent-article'; const result = await queryRepository.getLibrariesForUrl(testUrl, { page: 1, limit: 10, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); expect(result.items).toHaveLength(0); expect(result.totalCount).toBe(0); expect(result.hasMore).toBe(false); }); it('should not return users who have different URLs', async () => { const testUrl1 = 'https://example.com/article1'; const testUrl2 = 'https://example.com/article2'; const url1 = URL.create(testUrl1).unwrap(); const url2 = URL.create(testUrl2).unwrap(); // Create cards with different URLs const card1 = new CardBuilder() .withCuratorId(curator1.value) .withType(CardTypeEnum.URL) .withUrl(url1) .buildOrThrow(); const card2 = new CardBuilder() .withCuratorId(curator2.value) .withType(CardTypeEnum.URL) .withUrl(url2) .buildOrThrow(); card1.addToLibrary(curator1); card2.addToLibrary(curator2); await cardRepository.save(card1); await cardRepository.save(card2); // Query for testUrl1 const result = await queryRepository.getLibrariesForUrl(testUrl1, { page: 1, limit: 10, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); expect(result.items).toHaveLength(1); expect(result.items[0]!.userId).toBe(curator1.value); expect(result.items[0]!.cardId).toBe(card1.cardId.getStringValue()); }); it('should not return NOTE cards even if they have the same URL', async () => { const testUrl = 'https://example.com/article'; const url = URL.create(testUrl).unwrap(); // Create URL card const urlCard = new CardBuilder() .withCuratorId(curator1.value) .withType(CardTypeEnum.URL) .withUrl(url) .buildOrThrow(); // Create NOTE card with same URL (shouldn't happen in practice but test edge case) const noteCard = new CardBuilder() .withCuratorId(curator2.value) .withType(CardTypeEnum.NOTE) .withUrl(url) .buildOrThrow(); urlCard.addToLibrary(curator1); noteCard.addToLibrary(curator2); await cardRepository.save(urlCard); await cardRepository.save(noteCard); const result = await queryRepository.getLibrariesForUrl(testUrl, { page: 1, limit: 10, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); // Should only return the URL card, not the NOTE card expect(result.items).toHaveLength(1); expect(result.items[0]!.userId).toBe(curator1.value); expect(result.items[0]!.cardId).toBe(urlCard.cardId.getStringValue()); }); it('should handle multiple cards from same user with same URL', async () => { const testUrl = 'https://example.com/article'; const url = URL.create(testUrl).unwrap(); // Create two URL cards from same user with same URL const card1 = new CardBuilder() .withCuratorId(curator1.value) .withType(CardTypeEnum.URL) .withUrl(url) .buildOrThrow(); const card2 = new CardBuilder() .withCuratorId(curator1.value) .withType(CardTypeEnum.URL) .withUrl(url) .buildOrThrow(); card1.addToLibrary(curator1); card2.addToLibrary(curator1); await cardRepository.save(card1); await cardRepository.save(card2); const result = await queryRepository.getLibrariesForUrl(testUrl, { page: 1, limit: 10, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); // Should return both cards expect(result.items).toHaveLength(2); expect(result.totalCount).toBe(2); // Both should be from the same user expect(result.items[0]!.userId).toBe(curator1.value); expect(result.items[1]!.userId).toBe(curator1.value); // But different card IDs const cardIds = result.items.map((lib) => lib.cardId); expect(cardIds).toContain(card1.cardId.getStringValue()); expect(cardIds).toContain(card2.cardId.getStringValue()); }); it('should handle cards not in any library', async () => { const testUrl = 'https://example.com/article'; const url = URL.create(testUrl).unwrap(); // Create URL card but don't add to library const card = new CardBuilder() .withCuratorId(curator1.value) .withType(CardTypeEnum.URL) .withUrl(url) .buildOrThrow(); await cardRepository.save(card); const result = await queryRepository.getLibrariesForUrl(testUrl, { page: 1, limit: 10, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); // Should return empty since card is not in any library expect(result.items).toHaveLength(0); expect(result.totalCount).toBe(0); }); }); describe('pagination', () => { it('should paginate results correctly', async () => { const testUrl = 'https://example.com/popular-article'; const url = URL.create(testUrl).unwrap(); // Create 5 cards with the same URL from different users const cards = []; const curators = []; for (let i = 1; i <= 5; i++) { const curator = CuratorId.create(`did:plc:curator${i}`).unwrap(); curators.push(curator); const card = new CardBuilder() .withCuratorId(curator.value) .withType(CardTypeEnum.URL) .withUrl(url) .buildOrThrow(); card.addToLibrary(curator); cards.push(card); await cardRepository.save(card); } // Test first page with limit 2 const result1 = await queryRepository.getLibrariesForUrl(testUrl, { page: 1, limit: 2, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); expect(result1.items).toHaveLength(2); expect(result1.totalCount).toBe(5); expect(result1.hasMore).toBe(true); // Test second page const result2 = await queryRepository.getLibrariesForUrl(testUrl, { page: 2, limit: 2, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); expect(result2.items).toHaveLength(2); expect(result2.totalCount).toBe(5); expect(result2.hasMore).toBe(true); // Test last page const result3 = await queryRepository.getLibrariesForUrl(testUrl, { page: 3, limit: 2, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); expect(result3.items).toHaveLength(1); expect(result3.totalCount).toBe(5); expect(result3.hasMore).toBe(false); // Verify no duplicate entries across pages const allUserIds = [ ...result1.items.map((lib) => lib.userId), ...result2.items.map((lib) => lib.userId), ...result3.items.map((lib) => lib.userId), ]; const uniqueUserIds = [...new Set(allUserIds)]; expect(uniqueUserIds).toHaveLength(5); }); it('should handle empty pages correctly', async () => { const testUrl = 'https://example.com/empty-test'; const result = await queryRepository.getLibrariesForUrl(testUrl, { page: 2, limit: 10, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); expect(result.items).toHaveLength(0); expect(result.totalCount).toBe(0); expect(result.hasMore).toBe(false); }); it('should handle large page numbers gracefully', async () => { const testUrl = 'https://example.com/single-card'; const url = URL.create(testUrl).unwrap(); // Create single card const card = new CardBuilder() .withCuratorId(curator1.value) .withType(CardTypeEnum.URL) .withUrl(url) .buildOrThrow(); card.addToLibrary(curator1); await cardRepository.save(card); // Request page 10 when there's only 1 item const result = await queryRepository.getLibrariesForUrl(testUrl, { page: 10, limit: 10, sortBy: CardSortField.UPDATED_AT, sortOrder: SortOrder.DESC, }); expect(result.items).toHaveLength(0); expect(result.totalCount).toBe(1); expect(result.hasMore).toBe(false); }); }); });