This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

semble / src / modules / cards / tests / application / GetUrlStatusForMyLibraryUseCase.test.ts
20 kB 603 lines
1import { GetUrlStatusForMyLibraryUseCase } from '../../application/useCases/queries/GetUrlStatusForMyLibraryUseCase'; 2import { InMemoryCardRepository } from '../utils/InMemoryCardRepository'; 3import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository'; 4import { InMemoryCollectionQueryRepository } from '../utils/InMemoryCollectionQueryRepository'; 5import { FakeCardPublisher } from '../utils/FakeCardPublisher'; 6import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher'; 7import { FakeEventPublisher } from '../utils/FakeEventPublisher'; 8import { FakeProfileService } from '../utils/FakeProfileService'; 9import { CuratorId } from '../../domain/value-objects/CuratorId'; 10import { CardBuilder } from '../utils/builders/CardBuilder'; 11import { CollectionBuilder } from '../utils/builders/CollectionBuilder'; 12import { CardTypeEnum } from '../../domain/value-objects/CardType'; 13import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId'; 14import { URL } from '../../domain/value-objects/URL'; 15import { err } from 'src/shared/core/Result'; 16import { ICardRepository } from '../../domain/ICardRepository'; 17 18describe('GetUrlStatusForMyLibraryUseCase', () => { 19 let useCase: GetUrlStatusForMyLibraryUseCase; 20 let cardRepository: InMemoryCardRepository; 21 let collectionRepository: InMemoryCollectionRepository; 22 let collectionQueryRepository: InMemoryCollectionQueryRepository; 23 let cardPublisher: FakeCardPublisher; 24 let collectionPublisher: FakeCollectionPublisher; 25 let eventPublisher: FakeEventPublisher; 26 let profileService: FakeProfileService; 27 let curatorId: CuratorId; 28 let otherCuratorId: CuratorId; 29 30 beforeEach(() => { 31 cardRepository = InMemoryCardRepository.getInstance(); 32 collectionRepository = InMemoryCollectionRepository.getInstance(); 33 collectionQueryRepository = new InMemoryCollectionQueryRepository( 34 collectionRepository, 35 ); 36 cardPublisher = new FakeCardPublisher(); 37 collectionPublisher = new FakeCollectionPublisher(); 38 eventPublisher = new FakeEventPublisher(); 39 profileService = new FakeProfileService(); 40 41 useCase = new GetUrlStatusForMyLibraryUseCase( 42 cardRepository, 43 collectionQueryRepository, 44 collectionRepository, 45 profileService, 46 eventPublisher, 47 ); 48 49 curatorId = CuratorId.create('did:plc:testcurator').unwrap(); 50 otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap(); 51 52 // Set up profiles 53 profileService.addProfile({ 54 id: curatorId.value, 55 name: 'Test Curator', 56 handle: 'testcurator', 57 avatarUrl: 'https://example.com/avatar1.jpg', 58 bio: 'Test curator bio', 59 }); 60 61 profileService.addProfile({ 62 id: otherCuratorId.value, 63 name: 'Other Curator', 64 handle: 'othercurator', 65 avatarUrl: 'https://example.com/avatar2.jpg', 66 bio: 'Other curator bio', 67 }); 68 }); 69 70 afterEach(() => { 71 cardRepository.clear(); 72 collectionRepository.clear(); 73 collectionQueryRepository.clear(); 74 cardPublisher.clear(); 75 collectionPublisher.clear(); 76 eventPublisher.clear(); 77 profileService.clear(); 78 }); 79 80 describe('URL card in collections', () => { 81 it('should return card ID and collections when user has URL card in multiple collections', async () => { 82 const testUrl = 'https://example.com/test-article'; 83 84 // Create a URL card 85 const url = URL.create(testUrl).unwrap(); 86 const card = new CardBuilder() 87 .withCuratorId(curatorId.value) 88 .withType(CardTypeEnum.URL) 89 .withUrl(url) 90 .build(); 91 92 if (card instanceof Error) { 93 throw new Error(`Failed to create card: ${card.message}`); 94 } 95 96 // Add card to library 97 const addToLibResult = card.addToLibrary(curatorId); 98 if (addToLibResult.isErr()) { 99 throw new Error( 100 `Failed to add card to library: ${addToLibResult.error.message}`, 101 ); 102 } 103 104 await cardRepository.save(card); 105 106 // Publish the card to simulate it being published 107 cardPublisher.publishCardToLibrary(card, curatorId); 108 109 // Create first collection 110 const collection1 = new CollectionBuilder() 111 .withAuthorId(curatorId.value) 112 .withName('Tech Articles') 113 .withDescription('Collection of technology articles') 114 .build(); 115 116 if (collection1 instanceof Error) { 117 throw new Error(`Failed to create collection1: ${collection1.message}`); 118 } 119 120 // Create second collection 121 const collection2 = new CollectionBuilder() 122 .withAuthorId(curatorId.value) 123 .withName('Reading List') 124 .withDescription('My personal reading list') 125 .build(); 126 127 if (collection2 instanceof Error) { 128 throw new Error(`Failed to create collection2: ${collection2.message}`); 129 } 130 131 // Add card to both collections 132 const addToCollection1Result = collection1.addCard( 133 card.cardId, 134 curatorId, 135 ); 136 if (addToCollection1Result.isErr()) { 137 throw new Error( 138 `Failed to add card to collection1: ${addToCollection1Result.error.message}`, 139 ); 140 } 141 142 const addToCollection2Result = collection2.addCard( 143 card.cardId, 144 curatorId, 145 ); 146 if (addToCollection2Result.isErr()) { 147 throw new Error( 148 `Failed to add card to collection2: ${addToCollection2Result.error.message}`, 149 ); 150 } 151 152 // Mark collections as published 153 const collection1PublishedRecordId = PublishedRecordId.create({ 154 uri: 'at://did:plc:testcurator/network.cosmik.collection/collection1', 155 cid: 'bafyreicollection1cid', 156 }); 157 158 const collection2PublishedRecordId = PublishedRecordId.create({ 159 uri: 'at://did:plc:testcurator/network.cosmik.collection/collection2', 160 cid: 'bafyreicollection2cid', 161 }); 162 163 collection1.markAsPublished(collection1PublishedRecordId); 164 collection2.markAsPublished(collection2PublishedRecordId); 165 166 // Mark card links as published in collections 167 const cardLinkPublishedRecordId1 = PublishedRecordId.create({ 168 uri: 'at://did:plc:testcurator/network.cosmik.collection/collection1/link1', 169 cid: 'bafyreilink1cid', 170 }); 171 172 const cardLinkPublishedRecordId2 = PublishedRecordId.create({ 173 uri: 'at://did:plc:testcurator/network.cosmik.collection/collection2/link2', 174 cid: 'bafyreilink2cid', 175 }); 176 177 collection1.markCardLinkAsPublished( 178 card.cardId, 179 cardLinkPublishedRecordId1, 180 ); 181 collection2.markCardLinkAsPublished( 182 card.cardId, 183 cardLinkPublishedRecordId2, 184 ); 185 186 // Save collections 187 await collectionRepository.save(collection1); 188 await collectionRepository.save(collection2); 189 190 // Publish collections and links 191 collectionPublisher.publish(collection1); 192 collectionPublisher.publish(collection2); 193 collectionPublisher.publishCardAddedToCollection( 194 card, 195 collection1, 196 curatorId, 197 ); 198 collectionPublisher.publishCardAddedToCollection( 199 card, 200 collection2, 201 curatorId, 202 ); 203 204 // Execute the use case 205 const query = { 206 url: testUrl, 207 curatorId: curatorId.value, 208 }; 209 210 const result = await useCase.execute(query); 211 212 // Verify the result 213 expect(result.isOk()).toBe(true); 214 const response = result.unwrap(); 215 216 expect(response.cardId).toBe(card.cardId.getStringValue()); 217 expect(response.collections).toHaveLength(2); 218 219 // Verify collection details 220 const techArticlesCollection = response.collections?.find( 221 (c) => c.name === 'Tech Articles', 222 ); 223 const readingListCollection = response.collections?.find( 224 (c) => c.name === 'Reading List', 225 ); 226 227 expect(techArticlesCollection).toBeDefined(); 228 expect(techArticlesCollection?.id).toBe( 229 collection1.collectionId.getStringValue(), 230 ); 231 expect(techArticlesCollection?.uri).toBe( 232 'at://did:plc:testcurator/network.cosmik.collection/collection1', 233 ); 234 expect(techArticlesCollection?.name).toBe('Tech Articles'); 235 expect(techArticlesCollection?.description).toBe( 236 'Collection of technology articles', 237 ); 238 239 expect(readingListCollection).toBeDefined(); 240 expect(readingListCollection?.id).toBe( 241 collection2.collectionId.getStringValue(), 242 ); 243 expect(readingListCollection?.uri).toBe( 244 'at://did:plc:testcurator/network.cosmik.collection/collection2', 245 ); 246 expect(readingListCollection?.name).toBe('Reading List'); 247 expect(readingListCollection?.description).toBe( 248 'My personal reading list', 249 ); 250 }); 251 252 it('should return card ID and empty collections when user has URL card but not in any collections', async () => { 253 const testUrl = 'https://example.com/standalone-article'; 254 255 // Create a URL card 256 const url = URL.create(testUrl).unwrap(); 257 const card = new CardBuilder() 258 .withCuratorId(curatorId.value) 259 .withType(CardTypeEnum.URL) 260 .withUrl(url) 261 .build(); 262 263 if (card instanceof Error) { 264 throw new Error(`Failed to create card: ${card.message}`); 265 } 266 267 // Add card to library 268 const addToLibResult = card.addToLibrary(curatorId); 269 if (addToLibResult.isErr()) { 270 throw new Error( 271 `Failed to add card to library: ${addToLibResult.error.message}`, 272 ); 273 } 274 275 await cardRepository.save(card); 276 277 // Publish the card 278 cardPublisher.publishCardToLibrary(card, curatorId); 279 280 // Execute the use case 281 const query = { 282 url: testUrl, 283 curatorId: curatorId.value, 284 }; 285 286 const result = await useCase.execute(query); 287 288 // Verify the result 289 expect(result.isOk()).toBe(true); 290 const response = result.unwrap(); 291 292 expect(response.cardId).toBe(card.cardId.getStringValue()); 293 expect(response.collections).toHaveLength(0); 294 }); 295 296 it('should return empty result when user does not have URL card for the URL', async () => { 297 const testUrl = 'https://example.com/nonexistent-article'; 298 299 // Execute the use case without creating any cards 300 const query = { 301 url: testUrl, 302 curatorId: curatorId.value, 303 }; 304 305 const result = await useCase.execute(query); 306 307 // Verify the result 308 expect(result.isOk()).toBe(true); 309 const response = result.unwrap(); 310 311 expect(response.cardId).toBeUndefined(); 312 expect(response.collections).toBeUndefined(); 313 }); 314 315 it('should not return collections from other users even if they have the same URL', async () => { 316 const testUrl = 'https://example.com/shared-article'; 317 318 // Create URL card for first user 319 const url = URL.create(testUrl).unwrap(); 320 const card1 = new CardBuilder() 321 .withCuratorId(curatorId.value) 322 .withType(CardTypeEnum.URL) 323 .withUrl(url) 324 .build(); 325 326 if (card1 instanceof Error) { 327 throw new Error(`Failed to create card1: ${card1.message}`); 328 } 329 330 const addToLibResult1 = card1.addToLibrary(curatorId); 331 if (addToLibResult1.isErr()) { 332 throw new Error( 333 `Failed to add card1 to library: ${addToLibResult1.error.message}`, 334 ); 335 } 336 337 await cardRepository.save(card1); 338 339 // Create URL card for second user (different card, same URL) 340 const card2 = new CardBuilder() 341 .withCuratorId(otherCuratorId.value) 342 .withType(CardTypeEnum.URL) 343 .withUrl(url) 344 .build(); 345 346 if (card2 instanceof Error) { 347 throw new Error(`Failed to create card2: ${card2.message}`); 348 } 349 350 const addToLibResult2 = card2.addToLibrary(otherCuratorId); 351 if (addToLibResult2.isErr()) { 352 throw new Error( 353 `Failed to add card2 to library: ${addToLibResult2.error.message}`, 354 ); 355 } 356 357 await cardRepository.save(card2); 358 359 // Create collection for second user and add their card 360 const otherUserCollection = new CollectionBuilder() 361 .withAuthorId(otherCuratorId.value) 362 .withName('Other User Collection') 363 .build(); 364 365 if (otherUserCollection instanceof Error) { 366 throw new Error( 367 `Failed to create other user collection: ${otherUserCollection.message}`, 368 ); 369 } 370 371 const addToOtherCollectionResult = otherUserCollection.addCard( 372 card2.cardId, 373 otherCuratorId, 374 ); 375 if (addToOtherCollectionResult.isErr()) { 376 throw new Error( 377 `Failed to add card2 to other collection: ${addToOtherCollectionResult.error.message}`, 378 ); 379 } 380 381 await collectionRepository.save(otherUserCollection); 382 383 // Execute the use case for first user 384 const query = { 385 url: testUrl, 386 curatorId: curatorId.value, 387 }; 388 389 const result = await useCase.execute(query); 390 391 // Verify the result - should only see first user's card, no collections 392 expect(result.isOk()).toBe(true); 393 const response = result.unwrap(); 394 395 expect(response.cardId).toBe(card1.cardId.getStringValue()); 396 expect(response.collections).toHaveLength(0); // No collections for first user 397 }); 398 399 it('should only return collections owned by the requesting user', async () => { 400 const testUrl = 'https://example.com/multi-user-article'; 401 402 // Create URL card for the user 403 const url = URL.create(testUrl).unwrap(); 404 const card = new CardBuilder() 405 .withCuratorId(curatorId.value) 406 .withType(CardTypeEnum.URL) 407 .withUrl(url) 408 .build(); 409 410 if (card instanceof Error) { 411 throw new Error(`Failed to create card: ${card.message}`); 412 } 413 414 const addToLibResult = card.addToLibrary(curatorId); 415 if (addToLibResult.isErr()) { 416 throw new Error( 417 `Failed to add card to library: ${addToLibResult.error.message}`, 418 ); 419 } 420 421 await cardRepository.save(card); 422 423 // Create user's own collection 424 const userCollection = new CollectionBuilder() 425 .withAuthorId(curatorId.value) 426 .withName('My Collection') 427 .build(); 428 429 if (userCollection instanceof Error) { 430 throw new Error( 431 `Failed to create user collection: ${userCollection.message}`, 432 ); 433 } 434 435 const addToUserCollectionResult = userCollection.addCard( 436 card.cardId, 437 curatorId, 438 ); 439 if (addToUserCollectionResult.isErr()) { 440 throw new Error( 441 `Failed to add card to user collection: ${addToUserCollectionResult.error.message}`, 442 ); 443 } 444 445 await collectionRepository.save(userCollection); 446 447 // Create another user's collection (this should not appear in results) 448 const otherUserCollection = new CollectionBuilder() 449 .withAuthorId(otherCuratorId.value) 450 .withName('Other User Collection') 451 .build(); 452 453 if (otherUserCollection instanceof Error) { 454 throw new Error( 455 `Failed to create other user collection: ${otherUserCollection.message}`, 456 ); 457 } 458 459 // Note: We don't add the card to the other user's collection since they can't add 460 // another user's card to their collection in this domain model 461 462 await collectionRepository.save(otherUserCollection); 463 464 // Execute the use case 465 const query = { 466 url: testUrl, 467 curatorId: curatorId.value, 468 }; 469 470 const result = await useCase.execute(query); 471 472 // Verify the result - should only see user's own collection 473 expect(result.isOk()).toBe(true); 474 const response = result.unwrap(); 475 476 expect(response.cardId).toBe(card.cardId.getStringValue()); 477 expect(response.collections).toHaveLength(1); 478 expect(response.collections?.[0]?.name).toBe('My Collection'); 479 expect(response.collections?.[0]?.id).toBe( 480 userCollection.collectionId.getStringValue(), 481 ); 482 }); 483 }); 484 485 describe('Validation', () => { 486 it('should fail with invalid URL', async () => { 487 const query = { 488 url: 'not-a-valid-url', 489 curatorId: curatorId.value, 490 }; 491 492 const result = await useCase.execute(query); 493 494 expect(result.isErr()).toBe(true); 495 if (result.isErr()) { 496 expect(result.error.message).toContain('Invalid URL'); 497 } 498 }); 499 500 it('should fail with invalid curator ID', async () => { 501 const query = { 502 url: 'https://example.com/valid-url', 503 curatorId: 'invalid-curator-id', 504 }; 505 506 const result = await useCase.execute(query); 507 508 expect(result.isErr()).toBe(true); 509 if (result.isErr()) { 510 expect(result.error.message).toContain('Invalid curator ID'); 511 } 512 }); 513 }); 514 515 describe('Error handling', () => { 516 it('should handle repository errors gracefully', async () => { 517 // Create a mock repository that returns an error Result 518 const errorCardRepository: ICardRepository = { 519 findUsersUrlCardByUrl: jest 520 .fn() 521 .mockResolvedValue(err(new Error('Database error'))), 522 save: jest.fn(), 523 findById: jest.fn(), 524 delete: jest.fn(), 525 findUsersNoteCardByUrl: jest.fn(), 526 }; 527 528 const errorUseCase = new GetUrlStatusForMyLibraryUseCase( 529 errorCardRepository, 530 collectionQueryRepository, 531 collectionRepository, 532 profileService, 533 eventPublisher, 534 ); 535 536 const query = { 537 url: 'https://example.com/test-url', 538 curatorId: curatorId.value, 539 }; 540 541 const result = await errorUseCase.execute(query); 542 543 expect(result.isErr()).toBe(true); 544 if (result.isErr()) { 545 expect(result.error.message).toContain('Database error'); 546 } 547 }); 548 549 it('should handle collection query repository errors gracefully', async () => { 550 const testUrl = 'https://example.com/error-test'; 551 552 // Create a URL card 553 const url = URL.create(testUrl).unwrap(); 554 const card = new CardBuilder() 555 .withCuratorId(curatorId.value) 556 .withType(CardTypeEnum.URL) 557 .withUrl(url) 558 .build(); 559 560 if (card instanceof Error) { 561 throw new Error(`Failed to create card: ${card.message}`); 562 } 563 564 const addToLibResult = card.addToLibrary(curatorId); 565 if (addToLibResult.isErr()) { 566 throw new Error( 567 `Failed to add card to library: ${addToLibResult.error.message}`, 568 ); 569 } 570 571 await cardRepository.save(card); 572 573 // Create a mock collection query repository that throws an error 574 const errorCollectionQueryRepository = { 575 findByCreator: jest.fn(), 576 getCollectionsContainingCardForUser: jest 577 .fn() 578 .mockRejectedValue(new Error('Collection query error')), 579 getCollectionsWithUrl: jest.fn(), 580 }; 581 582 const errorUseCase = new GetUrlStatusForMyLibraryUseCase( 583 cardRepository, 584 errorCollectionQueryRepository, 585 collectionRepository, 586 profileService, 587 eventPublisher, 588 ); 589 590 const query = { 591 url: testUrl, 592 curatorId: curatorId.value, 593 }; 594 595 const result = await errorUseCase.execute(query); 596 597 expect(result.isErr()).toBe(true); 598 if (result.isErr()) { 599 expect(result.error.message).toContain('Collection query error'); 600 } 601 }); 602 }); 603});