import { PostgreSqlContainer, StartedPostgreSqlContainer, } from '@testcontainers/postgresql'; import postgres from 'postgres'; import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js'; import { DrizzleCollectionQueryRepository } from '../../infrastructure/repositories/DrizzleCollectionQueryRepository'; import { DrizzleCardRepository } from '../../infrastructure/repositories/DrizzleCardRepository'; import { DrizzleCollectionRepository } from '../../infrastructure/repositories/DrizzleCollectionRepository'; import { CuratorId } from '../../domain/value-objects/CuratorId'; import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID'; import { collections, collectionCollaborators, collectionCards, } from '../../infrastructure/repositories/schema/collection.sql'; 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 { Collection, CollectionAccessType } from '../../domain/Collection'; import { CardFactory } from '../../domain/CardFactory'; import { CardTypeEnum } from '../../domain/value-objects/CardType'; import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId'; import { UrlMetadata } from '../../domain/value-objects/UrlMetadata'; import { createTestSchema } from '../test-utils/createTestSchema'; describe('DrizzleCollectionQueryRepository - getCollectionsContainingCardForUser', () => { let container: StartedPostgreSqlContainer; let db: PostgresJsDatabase; let queryRepository: DrizzleCollectionQueryRepository; let collectionRepository: DrizzleCollectionRepository; let cardRepository: DrizzleCardRepository; // Test data let curatorId: CuratorId; let otherCuratorId: 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 DrizzleCollectionQueryRepository(db); collectionRepository = new DrizzleCollectionRepository(db); cardRepository = new DrizzleCardRepository(db); // Create schema using helper function await createTestSchema(db); // Create test data curatorId = CuratorId.create('did:plc:testcurator').unwrap(); otherCuratorId = CuratorId.create('did:plc:othercurator').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(collectionCards); await db.delete(collectionCollaborators); await db.delete(collections); await db.delete(libraryMemberships); await db.delete(cards); await db.delete(publishedRecords); }); describe('URL card in collections', () => { it('should return collections when user has URL card in multiple collections', async () => { const testUrl = 'https://example.com/test-article'; // Create a URL card const urlMetadata = UrlMetadata.create({ url: testUrl, title: 'Test Article', description: 'A test article for testing', author: 'Test Author', siteName: 'Example.com', }).unwrap(); const card = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.URL, url: testUrl, metadata: urlMetadata, }, }).unwrap(); // Add card to library const addToLibResult = card.addToLibrary(curatorId); expect(addToLibResult.isOk()).toBe(true); await cardRepository.save(card); // Create first collection const collection1 = Collection.create( { authorId: curatorId, name: 'Tech Articles', description: 'Collection of technology articles', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); // Create second collection const collection2 = Collection.create( { authorId: curatorId, name: 'Reading List', description: 'My personal reading list', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); // Add card to both collections const addToCollection1Result = collection1.addCard( card.cardId, curatorId, ); expect(addToCollection1Result.isOk()).toBe(true); const addToCollection2Result = collection2.addCard( card.cardId, curatorId, ); expect(addToCollection2Result.isOk()).toBe(true); // Mark collections as published const collection1PublishedRecordId = PublishedRecordId.create({ uri: 'at://did:plc:testcurator/network.cosmik.collection/collection1', cid: 'bafyreicollection1cid', }); const collection2PublishedRecordId = PublishedRecordId.create({ uri: 'at://did:plc:testcurator/network.cosmik.collection/collection2', cid: 'bafyreicollection2cid', }); collection1.markAsPublished(collection1PublishedRecordId); collection2.markAsPublished(collection2PublishedRecordId); // Save collections await collectionRepository.save(collection1); await collectionRepository.save(collection2); // Execute the query const result = await queryRepository.getCollectionsContainingCardForUser( card.cardId.getStringValue(), curatorId.value, ); // Verify the result expect(result).toHaveLength(2); // Sort by name for consistent testing result.sort((a, b) => a.name.localeCompare(b.name)); // Verify collection details expect(result[0]?.id).toBe(collection2.collectionId.getStringValue()); // Reading List comes first alphabetically expect(result[0]?.uri).toBe( 'at://did:plc:testcurator/network.cosmik.collection/collection2', ); expect(result[0]?.name).toBe('Reading List'); expect(result[0]?.description).toBe('My personal reading list'); expect(result[1]?.id).toBe(collection1.collectionId.getStringValue()); // Tech Articles comes second expect(result[1]?.uri).toBe( 'at://did:plc:testcurator/network.cosmik.collection/collection1', ); expect(result[1]?.name).toBe('Tech Articles'); expect(result[1]?.description).toBe('Collection of technology articles'); }); it('should return empty array when user has URL card but not in any collections', async () => { const testUrl = 'https://example.com/standalone-article'; // Create a URL card const urlMetadata = UrlMetadata.create({ url: testUrl, title: 'Standalone Article', description: 'A standalone article for testing', }).unwrap(); const card = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.URL, url: testUrl, metadata: urlMetadata, }, }).unwrap(); // Add card to library const addToLibResult = card.addToLibrary(curatorId); expect(addToLibResult.isOk()).toBe(true); await cardRepository.save(card); // Execute the query const result = await queryRepository.getCollectionsContainingCardForUser( card.cardId.getStringValue(), curatorId.value, ); // Verify the result expect(result).toHaveLength(0); }); it('should return empty array when card does not exist', async () => { const nonExistentCardId = new UniqueEntityID().toString(); // Execute the query const result = await queryRepository.getCollectionsContainingCardForUser( nonExistentCardId, curatorId.value, ); // Verify the result expect(result).toHaveLength(0); }); it('should not return collections from other users even if they have the same card', async () => { const testUrl = 'https://example.com/shared-article'; // Create URL card for first user const urlMetadata1 = UrlMetadata.create({ url: testUrl, title: 'Shared Article', description: 'An article shared between users', }).unwrap(); const card1 = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.URL, url: testUrl, metadata: urlMetadata1, }, }).unwrap(); const addToLibResult1 = card1.addToLibrary(curatorId); expect(addToLibResult1.isOk()).toBe(true); await cardRepository.save(card1); // Create URL card for second user (different card, same URL) const urlMetadata2 = UrlMetadata.create({ url: testUrl, title: 'Shared Article', description: 'An article shared between users', }).unwrap(); const card2 = CardFactory.create({ curatorId: otherCuratorId.value, cardInput: { type: CardTypeEnum.URL, url: testUrl, metadata: urlMetadata2, }, }).unwrap(); const addToLibResult2 = card2.addToLibrary(otherCuratorId); expect(addToLibResult2.isOk()).toBe(true); await cardRepository.save(card2); // Create collection for second user and add their card const otherUserCollection = Collection.create( { authorId: otherCuratorId, name: 'Other User Collection', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); const addToOtherCollectionResult = otherUserCollection.addCard( card2.cardId, otherCuratorId, ); expect(addToOtherCollectionResult.isOk()).toBe(true); await collectionRepository.save(otherUserCollection); // Execute the query for first user's card const result = await queryRepository.getCollectionsContainingCardForUser( card1.cardId.getStringValue(), curatorId.value, ); // Verify the result - should be empty since first user's card is not in any collections expect(result).toHaveLength(0); }); it('should only return collections owned by the requesting user', async () => { const testUrl = 'https://example.com/multi-user-article'; // Create URL card for the user const urlMetadata = UrlMetadata.create({ url: testUrl, title: 'Multi User Article', description: 'An article for multi-user testing', }).unwrap(); const card = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.URL, url: testUrl, metadata: urlMetadata, }, }).unwrap(); const addToLibResult = card.addToLibrary(curatorId); expect(addToLibResult.isOk()).toBe(true); await cardRepository.save(card); // Create user's own collection const userCollection = Collection.create( { authorId: curatorId, name: 'My Collection', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); const addToUserCollectionResult = userCollection.addCard( card.cardId, curatorId, ); expect(addToUserCollectionResult.isOk()).toBe(true); await collectionRepository.save(userCollection); // Create another user's collection (this should not appear in results) const otherUserCollection = Collection.create( { authorId: otherCuratorId, name: 'Other User Collection', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); // Note: We don't add the card to the other user's collection since they can't add // another user's card to their collection in this domain model await collectionRepository.save(otherUserCollection); // Execute the query const result = await queryRepository.getCollectionsContainingCardForUser( card.cardId.getStringValue(), curatorId.value, ); // Verify the result - should only see user's own collection expect(result).toHaveLength(1); expect(result[0]?.name).toBe('My Collection'); expect(result[0]?.id).toBe(userCollection.collectionId.getStringValue()); }); }); describe('Note cards in collections', () => { it('should return collections containing note cards', async () => { // Create a note card const card = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.NOTE, text: 'This is a test note', }, }).unwrap(); // Add card to library const addToLibResult = card.addToLibrary(curatorId); expect(addToLibResult.isOk()).toBe(true); await cardRepository.save(card); // Create collection const collection = Collection.create( { authorId: curatorId, name: 'My Notes', description: 'Collection of my personal notes', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); // Add card to collection const addToCollectionResult = collection.addCard(card.cardId, curatorId); expect(addToCollectionResult.isOk()).toBe(true); await collectionRepository.save(collection); // Execute the query const result = await queryRepository.getCollectionsContainingCardForUser( card.cardId.getStringValue(), curatorId.value, ); // Verify the result expect(result).toHaveLength(1); expect(result[0]?.id).toBe(collection.collectionId.getStringValue()); expect(result[0]?.name).toBe('My Notes'); expect(result[0]?.description).toBe('Collection of my personal notes'); expect(result[0]?.uri).toBeUndefined(); // Not published }); it('should handle collections with and without descriptions', async () => { // Create a note card const card = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.NOTE, text: 'Test note for collections', }, }).unwrap(); await cardRepository.save(card); // Create collection with description const collectionWithDesc = Collection.create( { authorId: curatorId, name: 'Collection With Description', description: 'This collection has a description', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); // Create collection without description const collectionWithoutDesc = Collection.create( { authorId: curatorId, name: 'Collection Without Description', // No description provided accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); // Add card to both collections collectionWithDesc.addCard(card.cardId, curatorId); collectionWithoutDesc.addCard(card.cardId, curatorId); await collectionRepository.save(collectionWithDesc); await collectionRepository.save(collectionWithoutDesc); // Execute the query const result = await queryRepository.getCollectionsContainingCardForUser( card.cardId.getStringValue(), curatorId.value, ); // Verify the result expect(result).toHaveLength(2); // Sort by name for consistent testing result.sort((a, b) => a.name.localeCompare(b.name)); expect(result[0]?.name).toBe('Collection With Description'); expect(result[0]?.description).toBe('This collection has a description'); expect(result[1]?.name).toBe('Collection Without Description'); expect(result[1]?.description).toBeUndefined(); }); }); describe('Published and unpublished collections', () => { it('should return URI for published collections and undefined for unpublished', async () => { // Create a card const card = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.NOTE, text: 'Card for published/unpublished test', }, }).unwrap(); await cardRepository.save(card); // Create published collection const publishedCollection = Collection.create( { authorId: curatorId, name: 'Published Collection', description: 'This collection is published', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); // Create unpublished collection const unpublishedCollection = Collection.create( { authorId: curatorId, name: 'Unpublished Collection', description: 'This collection is not published', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); // Mark published collection as published const publishedRecordId = PublishedRecordId.create({ uri: 'at://did:plc:testcurator/network.cosmik.collection/published123', cid: 'bafyreipublishedcid', }); publishedCollection.markAsPublished(publishedRecordId); // Add card to both collections publishedCollection.addCard(card.cardId, curatorId); unpublishedCollection.addCard(card.cardId, curatorId); await collectionRepository.save(publishedCollection); await collectionRepository.save(unpublishedCollection); // Execute the query const result = await queryRepository.getCollectionsContainingCardForUser( card.cardId.getStringValue(), curatorId.value, ); // Verify the result expect(result).toHaveLength(2); // Find collections by name const publishedResult = result.find( (c) => c.name === 'Published Collection', ); const unpublishedResult = result.find( (c) => c.name === 'Unpublished Collection', ); expect(publishedResult?.uri).toBe( 'at://did:plc:testcurator/network.cosmik.collection/published123', ); expect(unpublishedResult?.uri).toBeUndefined(); }); }); describe('Sorting and ordering', () => { it('should return collections sorted by name in ascending order', async () => { // Create a card const card = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.NOTE, text: 'Card for sorting test', }, }).unwrap(); await cardRepository.save(card); // Create collections with names that will test alphabetical sorting const collectionNames = [ 'Zebra Collection', 'Alpha Collection', 'Beta Collection', ]; for (const name of collectionNames) { const collection = Collection.create( { authorId: curatorId, name, accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); collection.addCard(card.cardId, curatorId); await collectionRepository.save(collection); } // Execute the query const result = await queryRepository.getCollectionsContainingCardForUser( card.cardId.getStringValue(), curatorId.value, ); // Verify the result is sorted by name expect(result).toHaveLength(3); expect(result[0]?.name).toBe('Alpha Collection'); expect(result[1]?.name).toBe('Beta Collection'); expect(result[2]?.name).toBe('Zebra Collection'); }); }); describe('Edge cases', () => { it('should handle non-existent curator gracefully', async () => { const card = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.NOTE, text: 'Test card', }, }).unwrap(); await cardRepository.save(card); // Execute the query with non-existent curator const result = await queryRepository.getCollectionsContainingCardForUser( card.cardId.getStringValue(), 'did:plc:nonexistent', ); // Verify the result expect(result).toHaveLength(0); }); it('should handle empty curator ID gracefully', async () => { const card = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.NOTE, text: 'Test card', }, }).unwrap(); await cardRepository.save(card); // Execute the query with empty curator ID const result = await queryRepository.getCollectionsContainingCardForUser( card.cardId.getStringValue(), '', ); // Verify the result expect(result).toHaveLength(0); }); it('should handle collections with null published records', async () => { // Create a card const card = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.NOTE, text: 'Card for null published record test', }, }).unwrap(); await cardRepository.save(card); // Create collection without published record const collection = Collection.create( { authorId: curatorId, name: 'Collection Without Published Record', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); collection.addCard(card.cardId, curatorId); await collectionRepository.save(collection); // Execute the query const result = await queryRepository.getCollectionsContainingCardForUser( card.cardId.getStringValue(), curatorId.value, ); // Verify the result expect(result).toHaveLength(1); expect(result[0]?.uri).toBeUndefined(); expect(result[0]?.name).toBe('Collection Without Published Record'); }); }); describe('Multiple card types', () => { it('should work with different card types in the same collection', async () => { // Create different types of cards const urlMetadata = UrlMetadata.create({ url: 'https://example.com/test', title: 'Test URL', description: 'A test URL for mixed content', }).unwrap(); const urlCard = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.URL, url: 'https://example.com/test', metadata: urlMetadata, }, }).unwrap(); const noteCard = CardFactory.create({ curatorId: curatorId.value, cardInput: { type: CardTypeEnum.NOTE, text: 'Test note', }, }).unwrap(); await cardRepository.save(urlCard); await cardRepository.save(noteCard); // Create collection and add all cards const collection = Collection.create( { authorId: curatorId, name: 'Mixed Content Collection', description: 'Collection with different card types', accessType: CollectionAccessType.OPEN, collaboratorIds: [], createdAt: new Date(), updatedAt: new Date(), }, new UniqueEntityID(), ).unwrap(); collection.addCard(urlCard.cardId, curatorId); collection.addCard(noteCard.cardId, curatorId); await collectionRepository.save(collection); // Test each card type const urlResult = await queryRepository.getCollectionsContainingCardForUser( urlCard.cardId.getStringValue(), curatorId.value, ); const noteResult = await queryRepository.getCollectionsContainingCardForUser( noteCard.cardId.getStringValue(), curatorId.value, ); // Verify all return the same collection expect(urlResult).toHaveLength(1); expect(noteResult).toHaveLength(1); expect(urlResult[0]?.name).toBe('Mixed Content Collection'); expect(noteResult[0]?.name).toBe('Mixed Content Collection'); expect(urlResult[0]?.id).toBe(collection.collectionId.getStringValue()); expect(noteResult[0]?.id).toBe(collection.collectionId.getStringValue()); }); }); });