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