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});