A social knowledge tool for researchers built on ATProto
1import { AddCardToLibraryUseCase } from '../../application/useCases/commands/AddCardToLibraryUseCase'; 2import { InMemoryCardRepository } from '../utils/InMemoryCardRepository'; 3import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository'; 4import { FakeCardPublisher } from '../utils/FakeCardPublisher'; 5import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher'; 6import { CardLibraryService } from '../../domain/services/CardLibraryService'; 7import { CardCollectionService } from '../../domain/services/CardCollectionService'; 8import { CuratorId } from '../../domain/value-objects/CuratorId'; 9import { CollectionBuilder } from '../utils/builders/CollectionBuilder'; 10import { CardBuilder } from '../utils/builders/CardBuilder'; 11import { CardTypeEnum } from '../../domain/value-objects/CardType'; 12import { CARD_ERROR_MESSAGES } from '../../domain/Card'; 13 14describe('AddCardToLibraryUseCase', () => { 15 let useCase: AddCardToLibraryUseCase; 16 let cardRepository: InMemoryCardRepository; 17 let collectionRepository: InMemoryCollectionRepository; 18 let cardPublisher: FakeCardPublisher; 19 let collectionPublisher: FakeCollectionPublisher; 20 let cardLibraryService: CardLibraryService; 21 let cardCollectionService: CardCollectionService; 22 let curatorId: CuratorId; 23 let curatorId2: CuratorId; 24 25 beforeEach(() => { 26 cardRepository = new InMemoryCardRepository(); 27 collectionRepository = new InMemoryCollectionRepository(); 28 cardPublisher = new FakeCardPublisher(); 29 collectionPublisher = new FakeCollectionPublisher(); 30 31 cardLibraryService = new CardLibraryService( 32 cardRepository, 33 cardPublisher, 34 collectionRepository, 35 cardCollectionService, 36 ); 37 cardCollectionService = new CardCollectionService( 38 collectionRepository, 39 collectionPublisher, 40 ); 41 42 useCase = new AddCardToLibraryUseCase( 43 cardRepository, 44 cardLibraryService, 45 cardCollectionService, 46 ); 47 48 curatorId = CuratorId.create('did:plc:testcurator').unwrap(); 49 curatorId2 = CuratorId.create('did:plc:testcurator2').unwrap(); 50 }); 51 52 afterEach(() => { 53 cardRepository.clear(); 54 collectionRepository.clear(); 55 cardPublisher.clear(); 56 collectionPublisher.clear(); 57 }); 58 59 describe('Basic card addition to library', () => { 60 it('should not allow adding an existing url card to library', async () => { 61 // Create and save a card first 62 const card = new CardBuilder() 63 .withCuratorId(curatorId.value) 64 .withType(CardTypeEnum.URL) 65 .build(); 66 67 if (card instanceof Error) { 68 throw new Error(`Failed to create card: ${card.message}`); 69 } 70 71 const addToLibResult = card.addToLibrary(curatorId); 72 if (addToLibResult.isErr()) { 73 throw new Error( 74 `Failed to add card to library: ${addToLibResult.error.message}`, 75 ); 76 } 77 78 await cardRepository.save(card); 79 80 const request = { 81 cardId: card.cardId.getStringValue(), 82 curatorId: curatorId2.value, 83 }; 84 85 const result = await useCase.execute(request); 86 87 if (result.isOk()) { 88 throw new Error('Expected use case to fail, but it succeeded'); 89 } 90 expect(result.isErr()).toBe(true); 91 expect(result.error.message).toContain( 92 CARD_ERROR_MESSAGES.URL_CARD_SINGLE_LIBRARY_ONLY, 93 ); 94 }); 95 96 it('should fail when card does not exist', async () => { 97 const request = { 98 cardId: 'non-existent-card-id', 99 curatorId: curatorId.value, 100 }; 101 102 const result = await useCase.execute(request); 103 104 expect(result.isErr()).toBe(true); 105 if (result.isErr()) { 106 expect(result.error.message).toContain('Card not found'); 107 } 108 }); 109 }); 110 111 describe('Collection handling', () => { 112 it('should add card to specified collections', async () => { 113 // Create and save a card first 114 const card = new CardBuilder() 115 .withCuratorId(curatorId.value) 116 .withType(CardTypeEnum.URL) 117 .build(); 118 119 if (card instanceof Error) { 120 throw new Error(`Failed to create card: ${card.message}`); 121 } 122 123 await cardRepository.save(card); 124 125 // Create a test collection 126 const collection = new CollectionBuilder() 127 .withAuthorId(curatorId.value) 128 .withName('Test Collection') 129 .build(); 130 131 if (collection instanceof Error) { 132 throw new Error(`Failed to create collection: ${collection.message}`); 133 } 134 135 await collectionRepository.save(collection); 136 137 const request = { 138 cardId: card.cardId.getStringValue(), 139 collectionIds: [collection.collectionId.getStringValue()], 140 curatorId: curatorId.value, 141 }; 142 143 const result = await useCase.execute(request); 144 145 expect(result.isOk()).toBe(true); 146 147 // Verify card was published to library 148 const publishedCards = cardPublisher.getPublishedCards(); 149 expect(publishedCards).toHaveLength(1); 150 151 // Verify collection link was published 152 const publishedLinks = collectionPublisher.getPublishedLinksForCollection( 153 collection.collectionId.getStringValue(), 154 ); 155 expect(publishedLinks).toHaveLength(1); 156 expect(publishedLinks[0]?.cardId).toBe(card.cardId.getStringValue()); 157 }); 158 159 it('should add card to multiple collections', async () => { 160 // Create and save a card first 161 const card = new CardBuilder() 162 .withCuratorId(curatorId.value) 163 .withType(CardTypeEnum.NOTE) 164 .build(); 165 166 if (card instanceof Error) { 167 throw new Error(`Failed to create card: ${card.message}`); 168 } 169 170 await cardRepository.save(card); 171 172 // Create test collections 173 const collection1 = new CollectionBuilder() 174 .withAuthorId(curatorId.value) 175 .withName('Test Collection 1') 176 .build(); 177 178 const collection2 = new CollectionBuilder() 179 .withAuthorId(curatorId.value) 180 .withName('Test Collection 2') 181 .build(); 182 183 if (collection1 instanceof Error || collection2 instanceof Error) { 184 throw new Error('Failed to create collections'); 185 } 186 187 await collectionRepository.save(collection1); 188 await collectionRepository.save(collection2); 189 190 const request = { 191 cardId: card.cardId.getStringValue(), 192 collectionIds: [ 193 collection1.collectionId.getStringValue(), 194 collection2.collectionId.getStringValue(), 195 ], 196 curatorId: curatorId.value, 197 }; 198 199 const result = await useCase.execute(request); 200 201 expect(result.isOk()).toBe(true); 202 203 // Verify card was published to library 204 const publishedCards = cardPublisher.getPublishedCards(); 205 expect(publishedCards).toHaveLength(1); 206 207 // Verify collection links were published for both collections 208 const publishedLinks1 = 209 collectionPublisher.getPublishedLinksForCollection( 210 collection1.collectionId.getStringValue(), 211 ); 212 const publishedLinks2 = 213 collectionPublisher.getPublishedLinksForCollection( 214 collection2.collectionId.getStringValue(), 215 ); 216 217 expect(publishedLinks1).toHaveLength(1); 218 expect(publishedLinks2).toHaveLength(1); 219 expect(publishedLinks1[0]?.cardId).toBe(card.cardId.getStringValue()); 220 expect(publishedLinks2[0]?.cardId).toBe(card.cardId.getStringValue()); 221 }); 222 223 it('should work without collections when none are specified', async () => { 224 // Create and save a card first 225 const card = new CardBuilder() 226 .withCuratorId(curatorId.value) 227 .withType(CardTypeEnum.URL) 228 .build(); 229 230 if (card instanceof Error) { 231 throw new Error(`Failed to create card: ${card.message}`); 232 } 233 234 await cardRepository.save(card); 235 236 const request = { 237 cardId: card.cardId.getStringValue(), 238 curatorId: curatorId.value, 239 // No collectionIds specified 240 }; 241 242 const result = await useCase.execute(request); 243 244 expect(result.isOk()).toBe(true); 245 246 // Verify card was published to library 247 const publishedCards = cardPublisher.getPublishedCards(); 248 expect(publishedCards).toHaveLength(1); 249 250 // Verify no collection links were published 251 const allPublishedLinks = collectionPublisher.getAllPublishedLinks(); 252 expect(allPublishedLinks).toHaveLength(0); 253 }); 254 255 it('should fail when collection does not exist', async () => { 256 // Create and save a card first 257 const card = new CardBuilder() 258 .withCuratorId(curatorId.value) 259 .withType(CardTypeEnum.URL) 260 .build(); 261 262 if (card instanceof Error) { 263 throw new Error(`Failed to create card: ${card.message}`); 264 } 265 266 await cardRepository.save(card); 267 268 const request = { 269 cardId: card.cardId.getStringValue(), 270 collectionIds: ['non-existent-collection-id'], 271 curatorId: curatorId.value, 272 }; 273 274 const result = await useCase.execute(request); 275 276 expect(result.isErr()).toBe(true); 277 if (result.isErr()) { 278 expect(result.error.message).toContain('Collection not found'); 279 } 280 }); 281 }); 282 283 describe('Validation', () => { 284 it('should fail with invalid card ID', async () => { 285 const request = { 286 cardId: 'invalid-card-id', 287 curatorId: curatorId.value, 288 }; 289 290 const result = await useCase.execute(request); 291 292 expect(result.isErr()).toBe(true); 293 if (result.isErr()) { 294 expect(result.error.message).toContain('invalid-card-id'); 295 } 296 }); 297 298 it('should fail with invalid curator ID', async () => { 299 // Create and save a card first 300 const card = new CardBuilder() 301 .withCuratorId(curatorId.value) 302 .withType(CardTypeEnum.URL) 303 .build(); 304 305 if (card instanceof Error) { 306 throw new Error(`Failed to create card: ${card.message}`); 307 } 308 309 await cardRepository.save(card); 310 311 const request = { 312 cardId: card.cardId.getStringValue(), 313 curatorId: 'invalid-curator-id', 314 }; 315 316 const result = await useCase.execute(request); 317 318 expect(result.isErr()).toBe(true); 319 if (result.isErr()) { 320 expect(result.error.message).toContain('Invalid curator ID'); 321 } 322 }); 323 324 it('should fail with invalid collection ID', async () => { 325 // Create and save a card first 326 const card = new CardBuilder() 327 .withCuratorId(curatorId.value) 328 .withType(CardTypeEnum.URL) 329 .build(); 330 331 if (card instanceof Error) { 332 throw new Error(`Failed to create card: ${card.message}`); 333 } 334 335 await cardRepository.save(card); 336 337 const request = { 338 cardId: card.cardId.getStringValue(), 339 collectionIds: ['invalid-collection-id'], 340 curatorId: curatorId.value, 341 }; 342 343 const result = await useCase.execute(request); 344 345 expect(result.isErr()).toBe(true); 346 if (result.isErr()) { 347 expect(result.error.message).toContain('invalid-collection-id'); 348 } 349 }); 350 }); 351});