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.
TypeScript
at development
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651import { RemoveCardFromLibraryUseCase } from '../../application/useCases/commands/RemoveCardFromLibraryUseCase';import { InMemoryCardRepository } from '../utils/InMemoryCardRepository';import { FakeCardPublisher } from '../utils/FakeCardPublisher';import { CardLibraryService } from '../../domain/services/CardLibraryService';import { CuratorId } from '../../domain/value-objects/CuratorId';import { CardBuilder } from '../utils/builders/CardBuilder';import { CardTypeEnum } from '../../domain/value-objects/CardType';import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';import { CardCollectionService } from '../../domain/services/CardCollectionService';import { CollectionBuilder } from '../utils/builders/CollectionBuilder';
describe('RemoveCardFromLibraryUseCase', () => { let useCase: RemoveCardFromLibraryUseCase; let cardRepository: InMemoryCardRepository; let collectionRepository: InMemoryCollectionRepository; let cardPublisher: FakeCardPublisher; let collectionPublisher: FakeCollectionPublisher; let cardCollectionService: CardCollectionService; let cardLibraryService: CardLibraryService; let curatorId: CuratorId; let otherCuratorId: CuratorId;
beforeEach(() => { cardRepository = new InMemoryCardRepository(); cardPublisher = new FakeCardPublisher(); collectionRepository = new InMemoryCollectionRepository(); collectionPublisher = new FakeCollectionPublisher(); cardCollectionService = new CardCollectionService( collectionRepository, collectionPublisher, ); cardLibraryService = new CardLibraryService( cardRepository, cardPublisher, collectionRepository, cardCollectionService, );
useCase = new RemoveCardFromLibraryUseCase( cardRepository, cardLibraryService, );
curatorId = CuratorId.create('did:plc:testcurator').unwrap(); otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap(); });
afterEach(() => { cardRepository.clear(); cardPublisher.clear(); });
const createCard = async ( type: CardTypeEnum = CardTypeEnum.URL, creatorId: CuratorId = curatorId, ) => { const card = new CardBuilder() .withType(type) .withCuratorId(creatorId.value) .build();
if (card instanceof Error) { throw new Error(`Failed to create card: ${card.message}`); }
await cardRepository.save(card); return card; };
const addCardToLibrary = async (card: any, curatorId: CuratorId) => { // For URL cards, can only add to creator's library if (card.isUrlCard && !card.curatorId.equals(curatorId)) { throw new Error("URL cards can only be added to creator's library"); }
const addResult = await cardLibraryService.addCardToLibrary( card, curatorId, ); if (addResult.isErr()) { throw new Error( `Failed to add card to library: ${addResult.error.message}`, ); } };
const createCollection = async ( curatorId: CuratorId, name: string = 'Test Collection', ) => { const collection = new CollectionBuilder() .withAuthorId(curatorId.value) .withName(name) .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 library', () => { it('should successfully remove card from library', async () => { const card = await createCard();
// Add card to library first await addCardToLibrary(card, curatorId);
const request = { cardId: card.cardId.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 library and deleted (since it's no longer in any libraries and curator is owner) const updatedCardResult = await cardRepository.findById(card.cardId); const updatedCard = updatedCardResult.unwrap(); expect(updatedCard).toBeNull();
// Verify unpublish operation occurred const unpublishedCards = cardPublisher.getUnpublishedCards(); expect(unpublishedCards).toHaveLength(1); });
it("should remove URL card from creator's library", async () => { const card = await createCard();
// Add card to creator's library only (URL cards can only be in creator's library) await addCardToLibrary(card, curatorId);
const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card was removed from creator's library and deleted (since it's no longer in any libraries and curator is owner) const updatedCardResult = await cardRepository.findById(card.cardId); const updatedCard = updatedCardResult.unwrap(); expect(updatedCard).toBeNull();
// Verify unpublish operation occurred const unpublishedCards = cardPublisher.getUnpublishedCards(); expect(unpublishedCards).toHaveLength(1); });
it('should handle different card types', async () => { // Create URL card with curatorId as creator const urlCard = await createCard(CardTypeEnum.URL, curatorId); // Create note card with curatorId as creator const noteCard = await createCard(CardTypeEnum.NOTE, curatorId);
await addCardToLibrary(urlCard, curatorId); await addCardToLibrary(noteCard, curatorId);
// Remove URL card const urlRequest = { cardId: urlCard.cardId.getStringValue(), curatorId: curatorId.value, };
const urlResult = await useCase.execute(urlRequest); expect(urlResult.isOk()).toBe(true);
// Remove note card const noteRequest = { cardId: noteCard.cardId.getStringValue(), curatorId: curatorId.value, };
const noteResult = await useCase.execute(noteRequest); expect(noteResult.isOk()).toBe(true);
// Verify both cards were removed from library and deleted (since they're no longer in any libraries and curator is owner) const updatedUrlCardResult = await cardRepository.findById( urlCard.cardId, ); const updatedNoteCardResult = await cardRepository.findById( noteCard.cardId, );
const updatedUrlCard = updatedUrlCardResult.unwrap(); const updatedNoteCard = updatedNoteCardResult.unwrap();
expect(updatedUrlCard).toBeNull(); expect(updatedNoteCard).toBeNull(); }); });
describe('Validation', () => { it('should fail with invalid card ID', async () => { const request = { cardId: 'invalid-card-id', 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 request = { cardId: card.cardId.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 when card does not exist', async () => { const request = { cardId: 'non-existent-card-id', 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'); } }); });
describe('Publishing integration', () => { it('should unpublish card from library when removed', async () => { const card = await createCard(); await addCardToLibrary(card, curatorId);
const initialUnpublishCount = cardPublisher.getUnpublishedCards().length;
const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify unpublish operation occurred const finalUnpublishCount = cardPublisher.getUnpublishedCards().length; expect(finalUnpublishCount).toBe(initialUnpublishCount + 1);
// Verify the correct card was unpublished const unpublishedCards = cardPublisher.getUnpublishedCards(); const unpublishedCard = unpublishedCards.find( (uc) => uc.cardId === card.cardId.getStringValue(), ); expect(unpublishedCard).toBeDefined(); });
it('should handle unpublish failure gracefully', async () => { const card = await createCard(); await addCardToLibrary(card, curatorId);
// Configure publisher to fail unpublish cardPublisher.setShouldFailUnpublish(true);
const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true);
// Verify card was not removed from library if unpublish failed const cardResult = await cardRepository.findById(card.cardId); const cardFromRepo = cardResult.unwrap()!; expect(cardFromRepo.isInLibrary(curatorId)).toBe(true); });
it('should not unpublish if card was never published', async () => { const card = await createCard();
// Manually add to library without publishing const addResult = card.addToLibrary(curatorId); expect(addResult.isOk()).toBe(true); await cardRepository.save(card);
const initialUnpublishCount = cardPublisher.getUnpublishedCards().length;
const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify no unpublish operation occurred const finalUnpublishCount = cardPublisher.getUnpublishedCards().length; expect(finalUnpublishCount).toBe(initialUnpublishCount);
// Verify card was still removed from library and deleted (since it's no longer in any libraries and curator is owner) const updatedCardResult = await cardRepository.findById(card.cardId); const updatedCard = updatedCardResult.unwrap(); expect(updatedCard).toBeNull(); }); });
describe('Collection integration', () => { it('should remove card from all curator collections when removed from library', async () => { const card = await createCard(); await addCardToLibrary(card, curatorId);
// Create multiple collections for the curator const collection1 = await createCollection(curatorId, 'Collection 1'); const collection2 = await createCollection(curatorId, 'Collection 2'); const collection3 = await createCollection(curatorId, 'Collection 3');
// Add card to all collections await addCardToCollection(card, collection1, curatorId); await addCardToCollection(card, collection2, curatorId); await addCardToCollection(card, collection3, curatorId);
// Verify card is in all collections const initialCollection1Result = await collectionRepository.findById( collection1.collectionId, ); const initialCollection2Result = await collectionRepository.findById( collection2.collectionId, ); const initialCollection3Result = await collectionRepository.findById( collection3.collectionId, );
expect( initialCollection1Result .unwrap()! .cardIds.some((id) => id.equals(card.cardId)), ).toBe(true); expect( initialCollection2Result .unwrap()! .cardIds.some((id) => id.equals(card.cardId)), ).toBe(true); expect( initialCollection3Result .unwrap()! .cardIds.some((id) => id.equals(card.cardId)), ).toBe(true);
// Remove card from library const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card was removed from library and deleted (since it's no longer in any libraries and curator is owner) const updatedCardResult = await cardRepository.findById(card.cardId); const updatedCard = updatedCardResult.unwrap(); expect(updatedCard).toBeNull();
// Verify card was removed from all collections const finalCollection1Result = await collectionRepository.findById( collection1.collectionId, ); const finalCollection2Result = await collectionRepository.findById( collection2.collectionId, ); const finalCollection3Result = await collectionRepository.findById( collection3.collectionId, );
expect( finalCollection1Result .unwrap()! .cardIds.some((id) => id.equals(card.cardId)), ).toBe(false); expect( finalCollection2Result .unwrap()! .cardIds.some((id) => id.equals(card.cardId)), ).toBe(false); expect( finalCollection3Result .unwrap()! .cardIds.some((id) => id.equals(card.cardId)), ).toBe(false);
const unpublishedCollectionLinks = collectionPublisher.getAllRemovedLinks(); expect(unpublishedCollectionLinks).toHaveLength(3); });
it('should remove URL card from creator collections only', async () => { const card = await createCard(); await addCardToLibrary(card, curatorId);
// Create collections for the creator only (URL cards can only be in creator's library) const curatorCollection = await createCollection( curatorId, 'Curator Collection', );
// Add card to creator's collection await addCardToCollection(card, curatorCollection, curatorId);
// Remove card from creator's library const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card was removed from curator's collection const curatorCollectionResult = await collectionRepository.findById( curatorCollection.collectionId, );
expect( curatorCollectionResult .unwrap()! .cardIds.some((id) => id.equals(card.cardId)), ).toBe(false);
// Verify card was removed from creator's library and deleted (since it's no longer in any libraries and curator is owner) const updatedCardResult = await cardRepository.findById(card.cardId); const updatedCard = updatedCardResult.unwrap(); expect(updatedCard).toBeNull(); });
it('should handle card removal when no collections contain the card', async () => { const card = await createCard(); await addCardToLibrary(card, curatorId);
// Create collections but don't add the card to them await createCollection(curatorId, 'Empty Collection 1'); await createCollection(curatorId, 'Empty Collection 2');
const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card was removed from library and deleted (since it's no longer in any libraries and curator is owner) const updatedCardResult = await cardRepository.findById(card.cardId); const updatedCard = updatedCardResult.unwrap(); expect(updatedCard).toBeNull();
// Verify no collection unpublish operations occurred const unpublishedCollections = collectionPublisher.getUnpublishedCollections(); expect(unpublishedCollections).toHaveLength(0); }); });
describe('Card deletion behavior', () => { it('should delete card when removed from last library and curator is owner', async () => { const card = await createCard(); await addCardToLibrary(card, curatorId);
// Verify card exists and is in library expect(card.isInLibrary(curatorId)).toBe(true); expect(card.libraryMembershipCount).toBe(1);
const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card was deleted const cardResult = await cardRepository.findById(card.cardId); const cardFromRepo = cardResult.unwrap(); expect(cardFromRepo).toBeNull(); });
it('should not delete card when curator is not the owner', async () => { // Create card with different owner const card = await createCard(CardTypeEnum.NOTE, otherCuratorId);
// Add to other curator's library first await addCardToLibrary(card, otherCuratorId);
// Add to current curator's library (note cards can be in multiple libraries) await addCardToLibrary(card, curatorId);
// Remove from current curator's library const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card still exists (not deleted because curator is not owner) const cardResult = await cardRepository.findById(card.cardId); const cardFromRepo = cardResult.unwrap()!; expect(cardFromRepo).not.toBeNull(); expect(cardFromRepo.isInLibrary(curatorId)).toBe(false); expect(cardFromRepo.isInLibrary(otherCuratorId)).toBe(true); });
it('should not delete card when it still has other library memberships', async () => { // Create note card (can be in multiple libraries) const card = await createCard(CardTypeEnum.NOTE, curatorId);
// Add to both curator's libraries await addCardToLibrary(card, curatorId); await addCardToLibrary(card, otherCuratorId);
expect(card.libraryMembershipCount).toBe(2);
// Remove from curator's library const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card still exists (not deleted because it's still in other curator's library) const cardResult = await cardRepository.findById(card.cardId); const cardFromRepo = cardResult.unwrap()!; expect(cardFromRepo).not.toBeNull(); expect(cardFromRepo.isInLibrary(curatorId)).toBe(false); expect(cardFromRepo.isInLibrary(otherCuratorId)).toBe(true); expect(cardFromRepo.libraryMembershipCount).toBe(1); }); });
describe('Edge cases', () => { it('should handle URL card with single library membership', async () => { // Create URL card with curatorId as creator const card = await createCard(CardTypeEnum.URL, curatorId);
// Add to creator's library only (URL cards can only be in creator's library) await addCardToLibrary(card, curatorId);
expect(card.libraryMembershipCount).toBe(1);
const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card was deleted (since it's no longer in any libraries and curator is owner) const updatedCardResult = await cardRepository.findById(card.cardId); const updatedCard = updatedCardResult.unwrap(); expect(updatedCard).toBeNull(); });
it('should handle repository save failure', async () => { const card = await createCard(); await addCardToLibrary(card, curatorId);
// Configure repository to fail save cardRepository.setShouldFailSave(true);
const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isErr()).toBe(true); });
it('should preserve card properties when removing from library', async () => { // Create URL card with curatorId as creator const card = await createCard(CardTypeEnum.URL, curatorId); await addCardToLibrary(card, curatorId);
const originalCreatedAt = card.createdAt; const originalType = card.type.value; const originalContent = card.content;
const request = { cardId: card.cardId.getStringValue(), curatorId: curatorId.value, };
const result = await useCase.execute(request);
expect(result.isOk()).toBe(true);
// Verify card was deleted (since it's no longer in any libraries and curator is owner) const updatedCardResult = await cardRepository.findById(card.cardId); const updatedCard = updatedCardResult.unwrap(); expect(updatedCard).toBeNull(); }); });});