Something went wrong. Try again.
A social knowledge tool for research trails, built on ATProto semble.so
research-tool social-network tools-for-thought atproto
Something went wrong. Try again.
19 kB · 577 lines
TypeScript
at development
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578import { GetUrlStatusForMyLibraryUseCase } from '../../application/useCases/queries/GetUrlStatusForMyLibraryUseCase';import { InMemoryCardRepository } from '../utils/InMemoryCardRepository';import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';import { InMemoryCollectionQueryRepository } from '../utils/InMemoryCollectionQueryRepository';import { FakeCardPublisher } from '../utils/FakeCardPublisher';import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';import { FakeEventPublisher } from '../utils/FakeEventPublisher';import { CuratorId } from '../../domain/value-objects/CuratorId';import { CardBuilder } from '../utils/builders/CardBuilder';import { CollectionBuilder } from '../utils/builders/CollectionBuilder';import { CardTypeEnum } from '../../domain/value-objects/CardType';import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId';import { URL } from '../../domain/value-objects/URL';import { err } from 'src/shared/core/Result';
describe('GetUrlStatusForMyLibraryUseCase', () => { let useCase: GetUrlStatusForMyLibraryUseCase; let cardRepository: InMemoryCardRepository; let collectionRepository: InMemoryCollectionRepository; let collectionQueryRepository: InMemoryCollectionQueryRepository; let cardPublisher: FakeCardPublisher; let collectionPublisher: FakeCollectionPublisher; let eventPublisher: FakeEventPublisher; let curatorId: CuratorId; let otherCuratorId: CuratorId;
beforeEach(() => { cardRepository = new InMemoryCardRepository(); collectionRepository = new InMemoryCollectionRepository(); collectionQueryRepository = new InMemoryCollectionQueryRepository( collectionRepository, ); cardPublisher = new FakeCardPublisher(); collectionPublisher = new FakeCollectionPublisher(); eventPublisher = new FakeEventPublisher();
useCase = new GetUrlStatusForMyLibraryUseCase( cardRepository, collectionQueryRepository, eventPublisher, );
curatorId = CuratorId.create('did:plc:testcurator').unwrap(); otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap(); });
afterEach(() => { cardRepository.clear(); collectionRepository.clear(); collectionQueryRepository.clear(); cardPublisher.clear(); collectionPublisher.clear(); eventPublisher.clear(); });
describe('URL card in collections', () => { it('should return card ID and collections when user has URL card in multiple collections', async () => { const testUrl = 'https://example.com/test-article';
// Create a URL card const url = URL.create(testUrl).unwrap(); const card = new CardBuilder() .withCuratorId(curatorId.value) .withType(CardTypeEnum.URL) .withUrl(url) .build();
if (card instanceof Error) { throw new Error(`Failed to create card: ${card.message}`); }
// Add card to library const addToLibResult = card.addToLibrary(curatorId); if (addToLibResult.isErr()) { throw new Error( `Failed to add card to library: ${addToLibResult.error.message}`, ); }
await cardRepository.save(card);
// Publish the card to simulate it being published cardPublisher.publishCardToLibrary(card, curatorId);
// Create first collection const collection1 = new CollectionBuilder() .withAuthorId(curatorId.value) .withName('Tech Articles') .withDescription('Collection of technology articles') .build();
if (collection1 instanceof Error) { throw new Error(`Failed to create collection1: ${collection1.message}`); }
// Create second collection const collection2 = new CollectionBuilder() .withAuthorId(curatorId.value) .withName('Reading List') .withDescription('My personal reading list') .build();
if (collection2 instanceof Error) { throw new Error(`Failed to create collection2: ${collection2.message}`); }
// Add card to both collections const addToCollection1Result = collection1.addCard( card.cardId, curatorId, ); if (addToCollection1Result.isErr()) { throw new Error( `Failed to add card to collection1: ${addToCollection1Result.error.message}`, ); }
const addToCollection2Result = collection2.addCard( card.cardId, curatorId, ); if (addToCollection2Result.isErr()) { throw new Error( `Failed to add card to collection2: ${addToCollection2Result.error.message}`, ); }
// 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);
// Mark card links as published in collections const cardLinkPublishedRecordId1 = PublishedRecordId.create({ uri: 'at://did:plc:testcurator/network.cosmik.collection/collection1/link1', cid: 'bafyreilink1cid', });
const cardLinkPublishedRecordId2 = PublishedRecordId.create({ uri: 'at://did:plc:testcurator/network.cosmik.collection/collection2/link2', cid: 'bafyreilink2cid', });
collection1.markCardLinkAsPublished( card.cardId, cardLinkPublishedRecordId1, ); collection2.markCardLinkAsPublished( card.cardId, cardLinkPublishedRecordId2, );
// Save collections await collectionRepository.save(collection1); await collectionRepository.save(collection2);
// Publish collections and links collectionPublisher.publish(collection1); collectionPublisher.publish(collection2); collectionPublisher.publishCardAddedToCollection( card, collection1, curatorId, ); collectionPublisher.publishCardAddedToCollection( card, collection2, curatorId, );
// Execute the use case const query = { url: testUrl, curatorId: curatorId.value, };
const result = await useCase.execute(query);
// Verify the result expect(result.isOk()).toBe(true); const response = result.unwrap();
expect(response.cardId).toBe(card.cardId.getStringValue()); expect(response.collections).toHaveLength(2);
// Verify collection details const techArticlesCollection = response.collections?.find( (c) => c.name === 'Tech Articles', ); const readingListCollection = response.collections?.find( (c) => c.name === 'Reading List', );
expect(techArticlesCollection).toBeDefined(); expect(techArticlesCollection?.id).toBe( collection1.collectionId.getStringValue(), ); expect(techArticlesCollection?.uri).toBe( 'at://did:plc:testcurator/network.cosmik.collection/collection1', ); expect(techArticlesCollection?.name).toBe('Tech Articles'); expect(techArticlesCollection?.description).toBe( 'Collection of technology articles', );
expect(readingListCollection).toBeDefined(); expect(readingListCollection?.id).toBe( collection2.collectionId.getStringValue(), ); expect(readingListCollection?.uri).toBe( 'at://did:plc:testcurator/network.cosmik.collection/collection2', ); expect(readingListCollection?.name).toBe('Reading List'); expect(readingListCollection?.description).toBe( 'My personal reading list', ); });
it('should return card ID and empty collections when user has URL card but not in any collections', async () => { const testUrl = 'https://example.com/standalone-article';
// Create a URL card const url = URL.create(testUrl).unwrap(); const card = new CardBuilder() .withCuratorId(curatorId.value) .withType(CardTypeEnum.URL) .withUrl(url) .build();
if (card instanceof Error) { throw new Error(`Failed to create card: ${card.message}`); }
// Add card to library const addToLibResult = card.addToLibrary(curatorId); if (addToLibResult.isErr()) { throw new Error( `Failed to add card to library: ${addToLibResult.error.message}`, ); }
await cardRepository.save(card);
// Publish the card cardPublisher.publishCardToLibrary(card, curatorId);
// Execute the use case const query = { url: testUrl, curatorId: curatorId.value, };
const result = await useCase.execute(query);
// Verify the result expect(result.isOk()).toBe(true); const response = result.unwrap();
expect(response.cardId).toBe(card.cardId.getStringValue()); expect(response.collections).toHaveLength(0); });
it('should return empty result when user does not have URL card for the URL', async () => { const testUrl = 'https://example.com/nonexistent-article';
// Execute the use case without creating any cards const query = { url: testUrl, curatorId: curatorId.value, };
const result = await useCase.execute(query);
// Verify the result expect(result.isOk()).toBe(true); const response = result.unwrap();
expect(response.cardId).toBeUndefined(); expect(response.collections).toBeUndefined(); });
it('should not return collections from other users even if they have the same URL', async () => { const testUrl = 'https://example.com/shared-article';
// Create URL card for first user const url = URL.create(testUrl).unwrap(); const card1 = new CardBuilder() .withCuratorId(curatorId.value) .withType(CardTypeEnum.URL) .withUrl(url) .build();
if (card1 instanceof Error) { throw new Error(`Failed to create card1: ${card1.message}`); }
const addToLibResult1 = card1.addToLibrary(curatorId); if (addToLibResult1.isErr()) { throw new Error( `Failed to add card1 to library: ${addToLibResult1.error.message}`, ); }
await cardRepository.save(card1);
// Create URL card for second user (different card, same URL) const card2 = new CardBuilder() .withCuratorId(otherCuratorId.value) .withType(CardTypeEnum.URL) .withUrl(url) .build();
if (card2 instanceof Error) { throw new Error(`Failed to create card2: ${card2.message}`); }
const addToLibResult2 = card2.addToLibrary(otherCuratorId); if (addToLibResult2.isErr()) { throw new Error( `Failed to add card2 to library: ${addToLibResult2.error.message}`, ); }
await cardRepository.save(card2);
// Create collection for second user and add their card const otherUserCollection = new CollectionBuilder() .withAuthorId(otherCuratorId.value) .withName('Other User Collection') .build();
if (otherUserCollection instanceof Error) { throw new Error( `Failed to create other user collection: ${otherUserCollection.message}`, ); }
const addToOtherCollectionResult = otherUserCollection.addCard( card2.cardId, otherCuratorId, ); if (addToOtherCollectionResult.isErr()) { throw new Error( `Failed to add card2 to other collection: ${addToOtherCollectionResult.error.message}`, ); }
await collectionRepository.save(otherUserCollection);
// Execute the use case for first user const query = { url: testUrl, curatorId: curatorId.value, };
const result = await useCase.execute(query);
// Verify the result - should only see first user's card, no collections expect(result.isOk()).toBe(true); const response = result.unwrap();
expect(response.cardId).toBe(card1.cardId.getStringValue()); expect(response.collections).toHaveLength(0); // No collections for first user });
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 url = URL.create(testUrl).unwrap(); const card = new CardBuilder() .withCuratorId(curatorId.value) .withType(CardTypeEnum.URL) .withUrl(url) .build();
if (card instanceof Error) { throw new Error(`Failed to create card: ${card.message}`); }
const addToLibResult = card.addToLibrary(curatorId); if (addToLibResult.isErr()) { throw new Error( `Failed to add card to library: ${addToLibResult.error.message}`, ); }
await cardRepository.save(card);
// Create user's own collection const userCollection = new CollectionBuilder() .withAuthorId(curatorId.value) .withName('My Collection') .build();
if (userCollection instanceof Error) { throw new Error( `Failed to create user collection: ${userCollection.message}`, ); }
const addToUserCollectionResult = userCollection.addCard( card.cardId, curatorId, ); if (addToUserCollectionResult.isErr()) { throw new Error( `Failed to add card to user collection: ${addToUserCollectionResult.error.message}`, ); }
await collectionRepository.save(userCollection);
// Create another user's collection (this should not appear in results) const otherUserCollection = new CollectionBuilder() .withAuthorId(otherCuratorId.value) .withName('Other User Collection') .build();
if (otherUserCollection instanceof Error) { throw new Error( `Failed to create other user collection: ${otherUserCollection.message}`, ); }
// 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 use case const query = { url: testUrl, curatorId: curatorId.value, };
const result = await useCase.execute(query);
// Verify the result - should only see user's own collection expect(result.isOk()).toBe(true); const response = result.unwrap();
expect(response.cardId).toBe(card.cardId.getStringValue()); expect(response.collections).toHaveLength(1); expect(response.collections?.[0]?.name).toBe('My Collection'); expect(response.collections?.[0]?.id).toBe( userCollection.collectionId.getStringValue(), ); }); });
describe('Validation', () => { it('should fail with invalid URL', async () => { const query = { url: 'not-a-valid-url', curatorId: curatorId.value, };
const result = await useCase.execute(query);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Invalid URL'); } });
it('should fail with invalid curator ID', async () => { const query = { url: 'https://example.com/valid-url', curatorId: 'invalid-curator-id', };
const result = await useCase.execute(query);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Invalid curator ID'); } }); });
describe('Error handling', () => { it('should handle repository errors gracefully', async () => { // Create a mock repository that returns an error Result const errorCardRepository = { findUsersUrlCardByUrl: jest .fn() .mockResolvedValue(err(new Error('Database error'))), save: jest.fn(), findById: jest.fn(), delete: jest.fn(), findByUrl: jest.fn(), findByCuratorId: jest.fn(), findByParentCardId: jest.fn(), };
const errorUseCase = new GetUrlStatusForMyLibraryUseCase( errorCardRepository, collectionQueryRepository, eventPublisher, );
const query = { url: 'https://example.com/test-url', curatorId: curatorId.value, };
const result = await errorUseCase.execute(query);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Database error'); } });
it('should handle collection query repository errors gracefully', async () => { const testUrl = 'https://example.com/error-test';
// Create a URL card const url = URL.create(testUrl).unwrap(); const card = new CardBuilder() .withCuratorId(curatorId.value) .withType(CardTypeEnum.URL) .withUrl(url) .build();
if (card instanceof Error) { throw new Error(`Failed to create card: ${card.message}`); }
const addToLibResult = card.addToLibrary(curatorId); if (addToLibResult.isErr()) { throw new Error( `Failed to add card to library: ${addToLibResult.error.message}`, ); }
await cardRepository.save(card);
// Create a mock collection query repository that throws an error const errorCollectionQueryRepository = { findByCreator: jest.fn(), getCollectionsContainingCardForUser: jest .fn() .mockRejectedValue(new Error('Collection query error')), getCollectionsWithUrl: jest.fn(), };
const errorUseCase = new GetUrlStatusForMyLibraryUseCase( cardRepository, errorCollectionQueryRepository, eventPublisher, );
const query = { url: testUrl, curatorId: curatorId.value, };
const result = await errorUseCase.execute(query);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Collection query error'); } }); });});