A social knowledge tool for researchers built on ATProto
1import { GetCollectionPageUseCase } from '../../application/useCases/queries/GetCollectionPageUseCase'; 2import { InMemoryCardQueryRepository } from '../utils/InMemoryCardQueryRepository'; 3import { InMemoryCardRepository } from '../utils/InMemoryCardRepository'; 4import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository'; 5import { FakeProfileService } from '../utils/FakeProfileService'; 6import { CuratorId } from '../../domain/value-objects/CuratorId'; 7import { CollectionId } from '../../domain/value-objects/CollectionId'; 8import { Collection, CollectionAccessType } from '../../domain/Collection'; 9import { Card } from '../../domain/Card'; 10import { CardType, CardTypeEnum } from '../../domain/value-objects/CardType'; 11import { CardContent } from '../../domain/value-objects/CardContent'; 12import { UrlMetadata } from '../../domain/value-objects/UrlMetadata'; 13import { URL } from '../../domain/value-objects/URL'; 14import { CardSortField, SortOrder } from '../../domain/ICardQueryRepository'; 15import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID'; 16import { ICollectionRepository } from '../../domain/ICollectionRepository'; 17 18describe('GetCollectionPageUseCase', () => { 19 let useCase: GetCollectionPageUseCase; 20 let collectionRepo: InMemoryCollectionRepository; 21 let cardRepo: InMemoryCardRepository; 22 let cardQueryRepo: InMemoryCardQueryRepository; 23 let profileService: FakeProfileService; 24 let curatorId: CuratorId; 25 let collectionId: CollectionId; 26 27 beforeEach(() => { 28 collectionRepo = new InMemoryCollectionRepository(); 29 cardRepo = new InMemoryCardRepository(); 30 cardQueryRepo = new InMemoryCardQueryRepository(cardRepo, collectionRepo); 31 profileService = new FakeProfileService(); 32 useCase = new GetCollectionPageUseCase( 33 collectionRepo, 34 cardQueryRepo, 35 profileService, 36 ); 37 38 curatorId = CuratorId.create('did:plc:testcurator').unwrap(); 39 collectionId = CollectionId.create(new UniqueEntityID()).unwrap(); 40 41 // Set up profile for the curator 42 profileService.addProfile({ 43 id: curatorId.value, 44 name: 'Test Curator', 45 handle: 'testcurator', 46 avatarUrl: 'https://example.com/avatar.jpg', 47 bio: 'Test curator bio', 48 }); 49 }); 50 51 afterEach(() => { 52 collectionRepo.clear(); 53 cardRepo.clear(); 54 cardQueryRepo.clear(); 55 profileService.clear(); 56 }); 57 58 describe('Basic functionality', () => { 59 it('should return collection page with empty cards when collection has no cards', async () => { 60 // Create collection 61 const collection = Collection.create( 62 { 63 authorId: curatorId, 64 name: 'Empty Collection', 65 description: 'A collection with no cards', 66 accessType: CollectionAccessType.OPEN, 67 collaboratorIds: [], 68 createdAt: new Date(), 69 updatedAt: new Date(), 70 }, 71 collectionId.getValue(), 72 ).unwrap(); 73 74 await collectionRepo.save(collection); 75 76 const query = { 77 collectionId: collectionId.getStringValue(), 78 }; 79 80 const result = await useCase.execute(query); 81 82 expect(result.isOk()).toBe(true); 83 const response = result.unwrap(); 84 expect(response.id).toBe(collectionId.getStringValue()); 85 expect(response.name).toBe('Empty Collection'); 86 expect(response.description).toBe('A collection with no cards'); 87 expect(response.author.id).toBe(curatorId.value); 88 expect(response.author.name).toBe('Test Curator'); 89 expect(response.author.handle).toBe('testcurator'); 90 expect(response.author.avatarUrl).toBe('https://example.com/avatar.jpg'); 91 expect(response.urlCards).toHaveLength(0); 92 expect(response.pagination.totalCount).toBe(0); 93 expect(response.pagination.currentPage).toBe(1); 94 expect(response.pagination.hasMore).toBe(false); 95 }); 96 97 it('should return collection page with URL cards', async () => { 98 // Create collection 99 const collection = Collection.create( 100 { 101 authorId: curatorId, 102 name: 'Test Collection', 103 accessType: CollectionAccessType.OPEN, 104 collaboratorIds: [], 105 createdAt: new Date(), 106 updatedAt: new Date(), 107 }, 108 collectionId.getValue(), 109 ).unwrap(); 110 111 // Create first URL card 112 const urlMetadata1 = UrlMetadata.create({ 113 url: 'https://example.com/article1', 114 title: 'First Article', 115 description: 'Description of first article', 116 author: 'John Doe', 117 imageUrl: 'https://example.com/thumb1.jpg', 118 }).unwrap(); 119 120 const url1 = URL.create('https://example.com/article1').unwrap(); 121 const cardType1 = CardType.create(CardTypeEnum.URL).unwrap(); 122 const cardContent1 = CardContent.createUrlContent( 123 url1, 124 urlMetadata1, 125 ).unwrap(); 126 127 const card1Result = Card.create({ 128 curatorId: curatorId, 129 type: cardType1, 130 content: cardContent1, 131 url: url1, 132 libraryMemberships: [], 133 libraryCount: 0, 134 createdAt: new Date(), 135 updatedAt: new Date(), 136 }); 137 138 if (card1Result.isErr()) { 139 throw card1Result.error; 140 } 141 142 const card1 = card1Result.value; 143 await cardRepo.save(card1); 144 145 // Create second URL card 146 const urlMetadata2 = UrlMetadata.create({ 147 url: 'https://example.com/article2', 148 title: 'Second Article', 149 description: 'Description of second article', 150 author: 'Jane Smith', 151 }).unwrap(); 152 153 const url2 = URL.create('https://example.com/article2').unwrap(); 154 const cardType2 = CardType.create(CardTypeEnum.URL).unwrap(); 155 const cardContent2 = CardContent.createUrlContent( 156 url2, 157 urlMetadata2, 158 ).unwrap(); 159 160 const card2Result = Card.create({ 161 curatorId: curatorId, 162 type: cardType2, 163 content: cardContent2, 164 url: url2, 165 libraryMemberships: [], 166 libraryCount: 0, 167 createdAt: new Date(), 168 updatedAt: new Date(), 169 }); 170 171 if (card2Result.isErr()) { 172 throw card2Result.error; 173 } 174 175 const card2 = card2Result.value; 176 await cardRepo.save(card2); 177 178 // Add cards to collection 179 collection.addCard(card1.cardId, curatorId); 180 collection.addCard(card2.cardId, curatorId); 181 await collectionRepo.save(collection); 182 183 const query = { 184 collectionId: collectionId.getStringValue(), 185 }; 186 187 const result = await useCase.execute(query); 188 189 expect(result.isOk()).toBe(true); 190 const response = result.unwrap(); 191 expect(response.urlCards).toHaveLength(2); 192 expect(response.pagination.totalCount).toBe(2); 193 194 // Verify card data 195 const firstCard = response.urlCards.find( 196 (card) => card.url === 'https://example.com/article1', 197 ); 198 const secondCard = response.urlCards.find( 199 (card) => card.url === 'https://example.com/article2', 200 ); 201 202 expect(firstCard).toBeDefined(); 203 expect(firstCard?.cardContent.title).toBe('First Article'); 204 expect(firstCard?.cardContent.author).toBe('John Doe'); 205 206 expect(secondCard).toBeDefined(); 207 expect(secondCard?.cardContent.title).toBe('Second Article'); 208 }); 209 210 it('should include notes in URL cards', async () => { 211 // Create collection 212 const collection = Collection.create( 213 { 214 authorId: curatorId, 215 name: 'Collection with Notes', 216 accessType: CollectionAccessType.OPEN, 217 collaboratorIds: [], 218 createdAt: new Date(), 219 updatedAt: new Date(), 220 }, 221 collectionId.getValue(), 222 ).unwrap(); 223 224 // Create URL card 225 const urlMetadata = UrlMetadata.create({ 226 url: 'https://example.com/article-with-note', 227 title: 'Article with Note', 228 description: 'An article with an associated note', 229 }).unwrap(); 230 231 const url = URL.create('https://example.com/article-with-note').unwrap(); 232 const cardType = CardType.create(CardTypeEnum.URL).unwrap(); 233 const cardContent = CardContent.createUrlContent( 234 url, 235 urlMetadata, 236 ).unwrap(); 237 238 const cardResult = Card.create({ 239 curatorId: curatorId, 240 type: cardType, 241 content: cardContent, 242 url: url, 243 libraryMemberships: [], 244 libraryCount: 0, 245 createdAt: new Date(), 246 updatedAt: new Date(), 247 }); 248 249 if (cardResult.isErr()) { 250 throw cardResult.error; 251 } 252 253 const card = cardResult.value; 254 await cardRepo.save(card); 255 256 // Create a note card that references the same URL 257 const noteCardResult = Card.create({ 258 curatorId: curatorId, 259 type: CardType.create(CardTypeEnum.NOTE).unwrap(), 260 content: CardContent.createNoteContent( 261 'This is my note about the article', 262 ).unwrap(), 263 parentCardId: card.cardId, 264 url: url, 265 libraryMemberships: [], 266 libraryCount: 0, 267 createdAt: new Date(), 268 updatedAt: new Date(), 269 }); 270 271 if (noteCardResult.isErr()) { 272 throw noteCardResult.error; 273 } 274 275 const noteCard = noteCardResult.value; 276 await cardRepo.save(noteCard); 277 278 // Add card to collection 279 collection.addCard(card.cardId, curatorId); 280 await collectionRepo.save(collection); 281 282 const query = { 283 collectionId: collectionId.getStringValue(), 284 }; 285 286 const result = await useCase.execute(query); 287 288 expect(result.isOk()).toBe(true); 289 const response = result.unwrap(); 290 expect(response.urlCards).toHaveLength(1); 291 292 const responseCard = response.urlCards[0]!; 293 expect(responseCard.cardContent.title).toBe('Article with Note'); 294 expect(responseCard.note).toBeDefined(); 295 expect(responseCard.note?.text).toBe('This is my note about the article'); 296 }); 297 298 it('should handle collection without description', async () => { 299 // Create collection without description 300 const collection = Collection.create( 301 { 302 authorId: curatorId, 303 name: 'No Description Collection', 304 accessType: CollectionAccessType.OPEN, 305 collaboratorIds: [], 306 createdAt: new Date(), 307 updatedAt: new Date(), 308 }, 309 collectionId.getValue(), 310 ).unwrap(); 311 312 await collectionRepo.save(collection); 313 314 const query = { 315 collectionId: collectionId.getStringValue(), 316 }; 317 318 const result = await useCase.execute(query); 319 320 expect(result.isOk()).toBe(true); 321 const response = result.unwrap(); 322 expect(response.description).toBeUndefined(); 323 }); 324 }); 325 326 describe('Pagination', () => { 327 beforeEach(async () => { 328 // Create collection 329 const collection = Collection.create( 330 { 331 authorId: curatorId, 332 name: 'Large Collection', 333 accessType: CollectionAccessType.OPEN, 334 collaboratorIds: [], 335 createdAt: new Date(), 336 updatedAt: new Date(), 337 }, 338 collectionId.getValue(), 339 ).unwrap(); 340 341 await collectionRepo.save(collection); 342 343 // Create multiple URL cards for pagination testing 344 for (let i = 1; i <= 5; i++) { 345 const urlMetadata = UrlMetadata.create({ 346 url: `https://example.com/article${i}`, 347 title: `Article ${i}`, 348 description: `Description of article ${i}`, 349 }).unwrap(); 350 351 const url = URL.create(`https://example.com/article${i}`).unwrap(); 352 const cardType = CardType.create(CardTypeEnum.URL).unwrap(); 353 const cardContent = CardContent.createUrlContent( 354 url, 355 urlMetadata, 356 ).unwrap(); 357 358 const cardResult = Card.create({ 359 curatorId: curatorId, 360 type: cardType, 361 content: cardContent, 362 url: url, 363 libraryMemberships: [], 364 libraryCount: 0, 365 createdAt: new Date(), 366 updatedAt: new Date(), 367 }); 368 369 if (cardResult.isErr()) { 370 throw cardResult.error; 371 } 372 373 const card = cardResult.value; 374 await cardRepo.save(card); 375 376 // Add card to collection 377 collection.addCard(card.cardId, curatorId); 378 } 379 380 await collectionRepo.save(collection); 381 }); 382 383 it('should handle pagination correctly', async () => { 384 const query = { 385 collectionId: collectionId.getStringValue(), 386 page: 1, 387 limit: 2, 388 }; 389 390 const result = await useCase.execute(query); 391 392 expect(result.isOk()).toBe(true); 393 const response = result.unwrap(); 394 expect(response.urlCards).toHaveLength(2); 395 expect(response.pagination.currentPage).toBe(1); 396 expect(response.pagination.totalPages).toBe(3); 397 expect(response.pagination.totalCount).toBe(5); 398 expect(response.pagination.hasMore).toBe(true); 399 expect(response.pagination.limit).toBe(2); 400 }); 401 402 it('should handle second page correctly', async () => { 403 const query = { 404 collectionId: collectionId.getStringValue(), 405 page: 2, 406 limit: 2, 407 }; 408 409 const result = await useCase.execute(query); 410 411 expect(result.isOk()).toBe(true); 412 const response = result.unwrap(); 413 expect(response.urlCards).toHaveLength(2); 414 expect(response.pagination.currentPage).toBe(2); 415 expect(response.pagination.hasMore).toBe(true); 416 }); 417 418 it('should handle last page correctly', async () => { 419 const query = { 420 collectionId: collectionId.getStringValue(), 421 page: 3, 422 limit: 2, 423 }; 424 425 const result = await useCase.execute(query); 426 427 expect(result.isOk()).toBe(true); 428 const response = result.unwrap(); 429 expect(response.urlCards).toHaveLength(1); 430 expect(response.pagination.currentPage).toBe(3); 431 expect(response.pagination.hasMore).toBe(false); 432 }); 433 434 it('should cap limit at 100', async () => { 435 const query = { 436 collectionId: collectionId.getStringValue(), 437 limit: 150, // Should be capped at 100 438 }; 439 440 const result = await useCase.execute(query); 441 442 expect(result.isOk()).toBe(true); 443 const response = result.unwrap(); 444 expect(response.pagination.limit).toBe(100); 445 }); 446 }); 447 448 describe('Sorting', () => { 449 beforeEach(async () => { 450 // Create collection 451 const collection = Collection.create( 452 { 453 authorId: curatorId, 454 name: 'Sortable Collection', 455 accessType: CollectionAccessType.OPEN, 456 collaboratorIds: [], 457 createdAt: new Date(), 458 updatedAt: new Date(), 459 }, 460 collectionId.getValue(), 461 ).unwrap(); 462 463 await collectionRepo.save(collection); 464 465 // Create URL cards with different properties for sorting 466 const now = new Date(); 467 468 // Create Alpha card (oldest created, middle updated) 469 const alphaMetadata = UrlMetadata.create({ 470 url: 'https://example.com/alpha', 471 title: 'Alpha Article', 472 }).unwrap(); 473 474 const alphaUrl = URL.create('https://example.com/alpha').unwrap(); 475 const alphaCardType = CardType.create(CardTypeEnum.URL).unwrap(); 476 const alphaCardContent = CardContent.createUrlContent( 477 alphaUrl, 478 alphaMetadata, 479 ).unwrap(); 480 481 const alphaCardResult = Card.create({ 482 curatorId: curatorId, 483 type: alphaCardType, 484 content: alphaCardContent, 485 url: alphaUrl, 486 libraryMemberships: [ 487 { curatorId: curatorId, addedAt: new Date(now.getTime() - 5000) }, 488 ], 489 libraryCount: 1, 490 createdAt: new Date(now.getTime() - 3000), // oldest 491 updatedAt: new Date(now.getTime() - 1000), // middle 492 }); 493 494 if (alphaCardResult.isErr()) { 495 throw alphaCardResult.error; 496 } 497 498 await cardRepo.save(alphaCardResult.value); 499 500 // Create Beta card (middle created, oldest updated) 501 const betaMetadata = UrlMetadata.create({ 502 url: 'https://example.com/beta', 503 title: 'Beta Article', 504 }).unwrap(); 505 506 const betaUrl = URL.create('https://example.com/beta').unwrap(); 507 const betaCardType = CardType.create(CardTypeEnum.URL).unwrap(); 508 const betaCardContent = CardContent.createUrlContent( 509 betaUrl, 510 betaMetadata, 511 ).unwrap(); 512 513 const betaCardResult = Card.create({ 514 curatorId: curatorId, 515 type: betaCardType, 516 content: betaCardContent, 517 url: betaUrl, 518 libraryMemberships: [ 519 { curatorId: curatorId, addedAt: new Date(now.getTime() - 5000) }, 520 ], 521 libraryCount: 1, 522 createdAt: new Date(now.getTime() - 2000), // middle 523 updatedAt: new Date(now.getTime() - 3000), // oldest 524 }); 525 526 if (betaCardResult.isErr()) { 527 throw betaCardResult.error; 528 } 529 530 await cardRepo.save(betaCardResult.value); 531 532 // Create Gamma card (newest created, newest updated) 533 const gammaMetadata = UrlMetadata.create({ 534 url: 'https://example.com/gamma', 535 title: 'Gamma Article', 536 }).unwrap(); 537 538 const gammaUrl = URL.create('https://example.com/gamma').unwrap(); 539 const gammaCardType = CardType.create(CardTypeEnum.URL).unwrap(); 540 const gammaCardContent = CardContent.createUrlContent( 541 gammaUrl, 542 gammaMetadata, 543 ).unwrap(); 544 545 const gammaCardResult = Card.create({ 546 curatorId: curatorId, 547 type: gammaCardType, 548 content: gammaCardContent, 549 url: gammaUrl, 550 libraryMemberships: [ 551 { curatorId: curatorId, addedAt: new Date(now.getTime() - 3000) }, 552 ], 553 libraryCount: 1, 554 createdAt: new Date(now.getTime() - 1000), // newest 555 updatedAt: new Date(now.getTime()), // newest 556 }); 557 558 if (gammaCardResult.isErr()) { 559 throw gammaCardResult.error; 560 } 561 562 await cardRepo.save(gammaCardResult.value); 563 564 // Add all cards to collection 565 collection.addCard(alphaCardResult.value.cardId, curatorId); 566 collection.addCard(betaCardResult.value.cardId, curatorId); 567 collection.addCard(gammaCardResult.value.cardId, curatorId); 568 await collectionRepo.save(collection); 569 }); 570 571 it('should sort by updated date descending', async () => { 572 const query = { 573 collectionId: collectionId.getStringValue(), 574 sortBy: CardSortField.UPDATED_AT, 575 sortOrder: SortOrder.DESC, 576 }; 577 578 const result = await useCase.execute(query); 579 580 expect(result.isOk()).toBe(true); 581 const response = result.unwrap(); 582 expect(response.urlCards[0]?.cardContent.title).toBe('Gamma Article'); // newest updated 583 expect(response.urlCards[1]?.cardContent.title).toBe('Alpha Article'); // middle updated 584 expect(response.urlCards[2]?.cardContent.title).toBe('Beta Article'); // oldest updated 585 }); 586 587 it('should sort by created date ascending', async () => { 588 const query = { 589 collectionId: collectionId.getStringValue(), 590 sortBy: CardSortField.CREATED_AT, 591 sortOrder: SortOrder.ASC, 592 }; 593 594 const result = await useCase.execute(query); 595 596 expect(result.isOk()).toBe(true); 597 const response = result.unwrap(); 598 expect(response.urlCards[0]?.cardContent.title).toBe('Alpha Article'); // oldest created 599 expect(response.urlCards[1]?.cardContent.title).toBe('Beta Article'); // middle created 600 expect(response.urlCards[2]?.cardContent.title).toBe('Gamma Article'); // newest created 601 }); 602 603 it('should sort by library count (all URL cards have same count)', async () => { 604 const query = { 605 collectionId: collectionId.getStringValue(), 606 sortBy: CardSortField.LIBRARY_COUNT, 607 sortOrder: SortOrder.DESC, 608 }; 609 610 const result = await useCase.execute(query); 611 612 expect(result.isOk()).toBe(true); 613 const response = result.unwrap(); 614 expect(response.urlCards).toHaveLength(3); 615 // All URL cards have library count of 1 (only creator's library) 616 expect(response.urlCards[0]?.libraryCount).toBe(1); 617 expect(response.urlCards[1]?.libraryCount).toBe(1); 618 expect(response.urlCards[2]?.libraryCount).toBe(1); 619 expect(response.sorting.sortBy).toBe(CardSortField.LIBRARY_COUNT); 620 expect(response.sorting.sortOrder).toBe(SortOrder.DESC); 621 }); 622 623 it('should use default sorting when not specified', async () => { 624 const query = { 625 collectionId: collectionId.getStringValue(), 626 }; 627 628 const result = await useCase.execute(query); 629 630 expect(result.isOk()).toBe(true); 631 const response = result.unwrap(); 632 expect(response.sorting.sortBy).toBe(CardSortField.UPDATED_AT); 633 expect(response.sorting.sortOrder).toBe(SortOrder.DESC); 634 }); 635 }); 636 637 describe('Error handling', () => { 638 it('should fail with invalid collection ID', async () => { 639 const query = { 640 collectionId: 'invalid-collection-id', 641 }; 642 643 const result = await useCase.execute(query); 644 645 expect(result.isErr()).toBe(true); 646 if (result.isErr()) { 647 expect(result.error.message).toContain('Collection not found'); 648 } 649 }); 650 651 it('should fail when collection not found', async () => { 652 const nonExistentCollectionId = CollectionId.create( 653 new UniqueEntityID(), 654 ).unwrap(); 655 656 const query = { 657 collectionId: nonExistentCollectionId.getStringValue(), 658 }; 659 660 const result = await useCase.execute(query); 661 662 expect(result.isErr()).toBe(true); 663 if (result.isErr()) { 664 expect(result.error.message).toContain('Collection not found'); 665 } 666 }); 667 668 it('should fail when author profile not found', async () => { 669 // Create collection with author that has no profile 670 const unknownCuratorId = CuratorId.create( 671 'did:plc:unknowncurator', 672 ).unwrap(); 673 const collection = Collection.create( 674 { 675 authorId: unknownCuratorId, 676 name: 'Orphaned Collection', 677 accessType: CollectionAccessType.OPEN, 678 collaboratorIds: [], 679 createdAt: new Date(), 680 updatedAt: new Date(), 681 }, 682 collectionId.getValue(), 683 ).unwrap(); 684 685 await collectionRepo.save(collection); 686 687 const query = { 688 collectionId: collectionId.getStringValue(), 689 }; 690 691 const result = await useCase.execute(query); 692 693 expect(result.isErr()).toBe(true); 694 if (result.isErr()) { 695 expect(result.error.message).toContain( 696 'Failed to fetch author profile', 697 ); 698 } 699 }); 700 701 it('should handle repository errors gracefully', async () => { 702 // Create a mock collection repository that throws an error 703 const errorCollectionRepo: ICollectionRepository = { 704 findById: jest 705 .fn() 706 .mockRejectedValue(new Error('Database connection failed')), 707 save: jest.fn(), 708 delete: jest.fn(), 709 findByCuratorId: jest.fn(), 710 findByCardId: jest.fn(), 711 findByCuratorIdContainingCard: jest.fn(), 712 }; 713 714 const errorUseCase = new GetCollectionPageUseCase( 715 errorCollectionRepo, 716 cardQueryRepo, 717 profileService, 718 ); 719 720 const query = { 721 collectionId: collectionId.getStringValue(), 722 }; 723 724 const result = await errorUseCase.execute(query); 725 726 expect(result.isErr()).toBe(true); 727 if (result.isErr()) { 728 expect(result.error.message).toContain( 729 'Failed to retrieve collection page', 730 ); 731 expect(result.error.message).toContain('Database connection failed'); 732 } 733 }); 734 735 it('should handle card query repository errors gracefully', async () => { 736 // Create collection 737 const collection = Collection.create( 738 { 739 authorId: curatorId, 740 name: 'Test Collection', 741 accessType: CollectionAccessType.OPEN, 742 collaboratorIds: [], 743 createdAt: new Date(), 744 updatedAt: new Date(), 745 }, 746 collectionId.getValue(), 747 ).unwrap(); 748 749 await collectionRepo.save(collection); 750 751 // Create a mock card query repository that throws an error 752 const errorCardQueryRepo = { 753 getUrlCardsOfUser: jest.fn(), 754 getCardsInCollection: jest 755 .fn() 756 .mockRejectedValue(new Error('Query failed')), 757 getUrlCardView: jest.fn(), 758 getLibrariesForCard: jest.fn(), 759 getLibrariesForUrl: jest.fn(), 760 getNoteCardsForUrl: jest.fn(), 761 }; 762 763 const errorUseCase = new GetCollectionPageUseCase( 764 collectionRepo, 765 errorCardQueryRepo, 766 profileService, 767 ); 768 769 const query = { 770 collectionId: collectionId.getStringValue(), 771 }; 772 773 const result = await errorUseCase.execute(query); 774 775 expect(result.isErr()).toBe(true); 776 if (result.isErr()) { 777 expect(result.error.message).toContain( 778 'Failed to retrieve collection page', 779 ); 780 expect(result.error.message).toContain('Query failed'); 781 } 782 }); 783 }); 784 785 describe('Edge cases', () => { 786 it('should handle URL cards with minimal metadata', async () => { 787 // Create collection 788 const collection = Collection.create( 789 { 790 authorId: curatorId, 791 name: 'Minimal Collection', 792 accessType: CollectionAccessType.OPEN, 793 collaboratorIds: [], 794 createdAt: new Date(), 795 updatedAt: new Date(), 796 }, 797 collectionId.getValue(), 798 ).unwrap(); 799 800 await collectionRepo.save(collection); 801 802 // Create URL card with minimal metadata 803 const url = URL.create('https://example.com/minimal').unwrap(); 804 const cardType = CardType.create(CardTypeEnum.URL).unwrap(); 805 const cardContent = CardContent.createUrlContent(url).unwrap(); 806 807 const cardResult = Card.create({ 808 curatorId: curatorId, 809 type: cardType, 810 content: cardContent, 811 url: url, 812 libraryMemberships: [{ curatorId: curatorId, addedAt: new Date() }], 813 libraryCount: 1, 814 createdAt: new Date(), 815 updatedAt: new Date(), 816 }); 817 818 if (cardResult.isErr()) { 819 throw cardResult.error; 820 } 821 822 const card = cardResult.value; 823 await cardRepo.save(card); 824 825 // Add card to collection 826 collection.addCard(card.cardId, curatorId); 827 await collectionRepo.save(collection); 828 829 const query = { 830 collectionId: collectionId.getStringValue(), 831 }; 832 833 const result = await useCase.execute(query); 834 835 expect(result.isOk()).toBe(true); 836 const response = result.unwrap(); 837 expect(response.urlCards).toHaveLength(1); 838 expect(response.urlCards[0]?.cardContent.title).toBeUndefined(); 839 expect(response.urlCards[0]?.cardContent.description).toBeUndefined(); 840 expect(response.urlCards[0]?.cardContent.author).toBeUndefined(); 841 expect(response.urlCards[0]?.cardContent.thumbnailUrl).toBeUndefined(); 842 }); 843 844 it('should handle author profile with minimal data', async () => { 845 // Create curator with minimal profile 846 const minimalCuratorId = CuratorId.create( 847 'did:plc:minimalcurator', 848 ).unwrap(); 849 profileService.addProfile({ 850 id: minimalCuratorId.value, 851 name: 'Minimal Curator', 852 handle: 'minimal', 853 // No avatarUrl or bio 854 }); 855 856 const collection = Collection.create( 857 { 858 authorId: minimalCuratorId, 859 name: 'Minimal Author Collection', 860 accessType: CollectionAccessType.OPEN, 861 collaboratorIds: [], 862 createdAt: new Date(), 863 updatedAt: new Date(), 864 }, 865 collectionId.getValue(), 866 ).unwrap(); 867 868 await collectionRepo.save(collection); 869 870 const query = { 871 collectionId: collectionId.getStringValue(), 872 }; 873 874 const result = await useCase.execute(query); 875 876 expect(result.isOk()).toBe(true); 877 const response = result.unwrap(); 878 expect(response.author.name).toBe('Minimal Curator'); 879 expect(response.author.handle).toBe('minimal'); 880 expect(response.author.avatarUrl).toBeUndefined(); 881 }); 882 }); 883});