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.
15 kB · 465 lines
TypeScript
at development
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466import { RemoveCardFromCollectionUseCase } from '../../application/useCases/commands/RemoveCardFromCollectionUseCase';import { InMemoryCardRepository } from '../utils/InMemoryCardRepository';import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';import { CardCollectionService } from '../../domain/services/CardCollectionService';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';
describe('RemoveCardFromCollectionUseCase', () => { let useCase: RemoveCardFromCollectionUseCase; let cardRepository: InMemoryCardRepository; let collectionRepository: InMemoryCollectionRepository; let collectionPublisher: FakeCollectionPublisher; let cardCollectionService: CardCollectionService; let curatorId: CuratorId; let otherCuratorId: CuratorId;
beforeEach(() => { cardRepository = new InMemoryCardRepository(); collectionRepository = new InMemoryCollectionRepository(); collectionPublisher = new FakeCollectionPublisher(); cardCollectionService = new CardCollectionService( collectionRepository, collectionPublisher, );
useCase = new RemoveCardFromCollectionUseCase( cardRepository, cardCollectionService, );
curatorId = CuratorId.create('did:plc:testcurator').unwrap(); otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap(); });
afterEach(() => { cardRepository.clear(); collectionRepository.clear(); collectionPublisher.clear(); });
const createCard = async (type: CardTypeEnum = CardTypeEnum.URL) => { const card = new CardBuilder().withType(type).build();
if (card instanceof Error) { throw new Error(`Failed to create card: ${card.message}`); }
await cardRepository.save(card); return card; };
const createCollection = async (authorId: CuratorId, name: string) => { const collection = new CollectionBuilder() .withAuthorId(authorId.value) .withName(name) .withPublished(true) .build();
if (collection instanceof Error) { throw new Error(`Failed to create collection: ${collection.message}`); }
await collectionRepository.save(collection); return collection; };
const addCardToCollection = async ( card: any, collection: any, curatorId: CuratorId, ) => { const addResult = await cardCollectionService.addCardToCollection( card, collection.collectionId, curatorId, ); if (addResult.isErr()) { throw new Error( `Failed to add card to collection: ${addResult.error.message}`, ); } };
describe('Basic card removal from collection', () => { it('should successfully remove card from collection', async () => { const card = await createCard(); const collection = await createCollection(curatorId, 'Test Collection');
// Add card to collection first await addCardToCollection(card, collection, curatorId);
const request = { cardId: card.cardId.getStringValue(), collectionIds: [collection.collectionId.getStringValue()], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true); const response = result.unwrap(); expect(response.cardId).toBe(card.cardId.getStringValue());
// Verify card was removed from collection const removedLinks = collectionPublisher.getRemovedLinksForCollection( collection.collectionId.getStringValue(), ); expect(removedLinks).toHaveLength(1); expect(removedLinks[0]?.cardId).toBe(card.cardId.getStringValue()); });
it('should remove card from multiple collections', async () => { const card = await createCard(); const collection1 = await createCollection(curatorId, 'Collection 1'); const collection2 = await createCollection(curatorId, 'Collection 2');
// Add card to both collections await addCardToCollection(card, collection1, curatorId); await addCardToCollection(card, collection2, curatorId);
const request = { cardId: card.cardId.getStringValue(), collectionIds: [ collection1.collectionId.getStringValue(), collection2.collectionId.getStringValue(), ], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card was removed from both collections const removedLinks1 = collectionPublisher.getRemovedLinksForCollection( collection1.collectionId.getStringValue(), ); const removedLinks2 = collectionPublisher.getRemovedLinksForCollection( collection2.collectionId.getStringValue(), );
expect(removedLinks1).toHaveLength(1); expect(removedLinks2).toHaveLength(1); expect(removedLinks1[0]?.cardId).toBe(card.cardId.getStringValue()); expect(removedLinks2[0]?.cardId).toBe(card.cardId.getStringValue()); });
it("should handle partial removal when some collections don't contain the card", async () => { const card = await createCard(); const collection1 = await createCollection(curatorId, 'Collection 1'); const collection2 = await createCollection(curatorId, 'Collection 2');
// Add card to only one collection await addCardToCollection(card, collection1, curatorId);
const request = { cardId: card.cardId.getStringValue(), collectionIds: [ collection1.collectionId.getStringValue(), collection2.collectionId.getStringValue(), ], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card was removed from collection1 but no error for collection2 const removedLinks1 = collectionPublisher.getRemovedLinksForCollection( collection1.collectionId.getStringValue(), ); const removedLinks2 = collectionPublisher.getRemovedLinksForCollection( collection2.collectionId.getStringValue(), );
expect(removedLinks1).toHaveLength(1); expect(removedLinks2).toHaveLength(0); }); });
describe('Authorization', () => { it('should fail when trying to remove card from closed collection without permission', async () => { const card = await createCard(); const collection = new CollectionBuilder() .withAuthorId(otherCuratorId.value) .withName('Closed Collection') .withAccessType('CLOSED') .withPublished(true) .build();
if (collection instanceof Error) { throw new Error(`Failed to create collection: ${collection.message}`); } collection.addCard(card.cardId, otherCuratorId);
await collectionRepository.save(collection);
const request = { cardId: card.cardId.getStringValue(), collectionIds: [collection.collectionId.getStringValue()], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('does not have permission'); } });
it('should allow removal from open collection by any user', async () => { const card = await createCard(); const collection = new CollectionBuilder() .withAuthorId(otherCuratorId.value) .withName('Open Collection') .withAccessType('OPEN') .withPublished(true) .build();
if (collection instanceof Error) { throw new Error(`Failed to create collection: ${collection.message}`); }
await collectionRepository.save(collection);
// Add card to collection first (as collection owner) await addCardToCollection(card, collection, otherCuratorId);
const request = { cardId: card.cardId.getStringValue(), collectionIds: [collection.collectionId.getStringValue()], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true); });
it('should allow collection author to remove any card', async () => { const card = await createCard(); const collection = await createCollection( curatorId, "Author's Collection", );
await addCardToCollection(card, collection, curatorId);
const request = { cardId: card.cardId.getStringValue(), collectionIds: [collection.collectionId.getStringValue()], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true); }); });
describe('Validation', () => { it('should fail with invalid card ID', async () => { const collection = await createCollection(curatorId, 'Test Collection');
const request = { cardId: 'invalid-card-id', collectionIds: [collection.collectionId.getStringValue()], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Card not found'); } });
it('should fail with invalid curator ID', async () => { const card = await createCard(); const collection = await createCollection(curatorId, 'Test Collection');
const request = { cardId: card.cardId.getStringValue(), collectionIds: [collection.collectionId.getStringValue()], curatorId: 'invalid-curator-id', };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Invalid curator ID'); } });
it('should fail with invalid collection ID', async () => { const card = await createCard();
const request = { cardId: card.cardId.getStringValue(), collectionIds: ['invalid-collection-id'], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Collection not found'); } });
it('should fail when card does not exist', async () => { const collection = await createCollection(curatorId, 'Test Collection');
const request = { cardId: 'non-existent-card-id', collectionIds: [collection.collectionId.getStringValue()], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Card not found'); } });
it('should fail when collection does not exist', async () => { const card = await createCard();
const request = { cardId: card.cardId.getStringValue(), collectionIds: ['non-existent-collection-id'], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); if (result.isErr()) { expect(result.error.message).toContain('Collection not found'); } });
it('should handle empty collection IDs array', async () => { const card = await createCard();
const request = { cardId: card.cardId.getStringValue(), collectionIds: [], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// No removal operations should have occurred const allRemovedLinks = collectionPublisher.getAllRemovedLinks(); expect(allRemovedLinks).toHaveLength(0); }); });
describe('Edge cases', () => { it('should handle removing card that was never in the collection', async () => { const card = await createCard(); const collection = await createCollection(curatorId, 'Empty Collection');
const request = { cardId: card.cardId.getStringValue(), collectionIds: [collection.collectionId.getStringValue()], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// No removal should have occurred const removedLinks = collectionPublisher.getRemovedLinksForCollection( collection.collectionId.getStringValue(), ); expect(removedLinks).toHaveLength(0); });
it('should handle removing card from same collection multiple times', async () => { const card = await createCard(); const collection = await createCollection(curatorId, 'Test Collection');
await addCardToCollection(card, collection, curatorId);
const request = { cardId: card.cardId.getStringValue(), collectionIds: [collection.collectionId.getStringValue()], curatorId: curatorId.value, };
// First removal should succeed const firstResult = await useCase.execute(request); expect(firstResult.isOk()).toBe(true);
// Second removal should also succeed (idempotent) const secondResult = await useCase.execute(request); expect(secondResult.isOk()).toBe(true);
// Only one removal operation should have been recorded const removedLinks = collectionPublisher.getRemovedLinksForCollection( collection.collectionId.getStringValue(), ); expect(removedLinks).toHaveLength(1); });
it('should handle different card types', async () => { const urlCard = await createCard(CardTypeEnum.URL); const noteCard = await createCard(CardTypeEnum.NOTE); const collection = await createCollection(curatorId, 'Mixed Collection');
await addCardToCollection(urlCard, collection, curatorId); await addCardToCollection(noteCard, collection, curatorId);
const request = { cardId: urlCard.cardId.getStringValue(), collectionIds: [collection.collectionId.getStringValue()], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify only the URL card was removed const removedLinks = collectionPublisher.getRemovedLinksForCollection( collection.collectionId.getStringValue(), ); expect(removedLinks).toHaveLength(1); expect(removedLinks[0]?.cardId).toBe(urlCard.cardId.getStringValue()); });
it('should handle repository errors gracefully', async () => { const card = await createCard(); const collection = await createCollection(curatorId, 'Test Collection');
// Configure repository to fail cardRepository.setShouldFail(true);
const request = { cardId: card.cardId.getStringValue(), collectionIds: [collection.collectionId.getStringValue()], curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); }); });});