This repository has no description
29 kB
866 lines
1import { UpdateUrlCardAssociationsUseCase } from '../../application/useCases/commands/UpdateUrlCardAssociationsUseCase';
2import { AddUrlToLibraryUseCase } from '../../application/useCases/commands/AddUrlToLibraryUseCase';
3import { InMemoryCardRepository } from '../utils/InMemoryCardRepository';
4import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository';
5import { FakeCardPublisher } from '../utils/FakeCardPublisher';
6import { FakeCollectionPublisher } from '../utils/FakeCollectionPublisher';
7import { FakeMetadataService } from '../utils/FakeMetadataService';
8import { CardLibraryService } from '../../domain/services/CardLibraryService';
9import { CardCollectionService } from '../../domain/services/CardCollectionService';
10import { CuratorId } from '../../domain/value-objects/CuratorId';
11import { CollectionBuilder } from '../utils/builders/CollectionBuilder';
12import { CardTypeEnum } from '../../domain/value-objects/CardType';
13import { FakeEventPublisher } from '../utils/FakeEventPublisher';
14import { CardId } from '../../domain/value-objects/CardId';
15
16describe('UpdateUrlCardAssociationsUseCase', () => {
17 let useCase: UpdateUrlCardAssociationsUseCase;
18 let addUrlToLibraryUseCase: AddUrlToLibraryUseCase;
19 let cardRepository: InMemoryCardRepository;
20 let collectionRepository: InMemoryCollectionRepository;
21 let cardPublisher: FakeCardPublisher;
22 let collectionPublisher: FakeCollectionPublisher;
23 let metadataService: FakeMetadataService;
24 let cardLibraryService: CardLibraryService;
25 let cardCollectionService: CardCollectionService;
26 let eventPublisher: FakeEventPublisher;
27 let curatorId: CuratorId;
28
29 beforeEach(() => {
30 cardRepository = InMemoryCardRepository.getInstance();
31 collectionRepository = InMemoryCollectionRepository.getInstance();
32 cardPublisher = new FakeCardPublisher();
33 collectionPublisher = new FakeCollectionPublisher();
34 metadataService = new FakeMetadataService();
35 eventPublisher = new FakeEventPublisher();
36
37 cardLibraryService = new CardLibraryService(
38 cardRepository,
39 cardPublisher,
40 collectionRepository,
41 cardCollectionService,
42 );
43 cardCollectionService = new CardCollectionService(
44 collectionRepository,
45 collectionPublisher,
46 cardRepository,
47 );
48
49 useCase = new UpdateUrlCardAssociationsUseCase(
50 cardRepository,
51 cardLibraryService,
52 cardCollectionService,
53 eventPublisher,
54 );
55
56 addUrlToLibraryUseCase = new AddUrlToLibraryUseCase(
57 cardRepository,
58 metadataService,
59 cardLibraryService,
60 cardCollectionService,
61 eventPublisher,
62 );
63
64 curatorId = CuratorId.create('did:plc:testcurator').unwrap();
65 });
66
67 afterEach(() => {
68 cardRepository.clear();
69 collectionRepository.clear();
70 cardPublisher.clear();
71 collectionPublisher.clear();
72 metadataService.clear();
73 eventPublisher.clear();
74 });
75
76 describe('Note management', () => {
77 it('should create a note for an existing URL card', async () => {
78 const url = 'https://example.com/article';
79
80 // First, add the URL to the library
81 const addResult = await addUrlToLibraryUseCase.execute({
82 url,
83 curatorId: curatorId.value,
84 });
85 expect(addResult.isOk()).toBe(true);
86 const urlCardId = addResult.unwrap().urlCardId;
87
88 // Now create a note for it
89 const updateRequest = {
90 cardId: urlCardId,
91 curatorId: curatorId.value,
92 note: 'This is my note',
93 };
94
95 const result = await useCase.execute(updateRequest);
96
97 expect(result.isOk()).toBe(true);
98 const response = result.unwrap();
99 expect(response.noteCardId).toBeDefined();
100
101 // Verify note card was created
102 const savedCards = cardRepository.getAllCards();
103 const noteCard = savedCards.find(
104 (card) => card.content.type === CardTypeEnum.NOTE,
105 );
106 expect(noteCard).toBeDefined();
107 expect(noteCard?.content.noteContent?.text).toBe('This is my note');
108 });
109
110 it('should update an existing note for a URL card', async () => {
111 const url = 'https://example.com/article';
112
113 // First, add the URL with a note
114 const addResult = await addUrlToLibraryUseCase.execute({
115 url,
116 note: 'Original note',
117 curatorId: curatorId.value,
118 });
119 expect(addResult.isOk()).toBe(true);
120 const addResponse = addResult.unwrap();
121
122 // Now update the note
123 const updateRequest = {
124 cardId: addResponse.urlCardId,
125 curatorId: curatorId.value,
126 note: 'Updated note',
127 };
128
129 const result = await useCase.execute(updateRequest);
130
131 expect(result.isOk()).toBe(true);
132 const response = result.unwrap();
133 expect(response.noteCardId).toBe(addResponse.noteCardId);
134
135 // Verify note was updated
136 const savedCards = cardRepository.getAllCards();
137 const noteCard = savedCards.find(
138 (card) => card.content.type === CardTypeEnum.NOTE,
139 );
140 expect(noteCard).toBeDefined();
141 expect(noteCard?.content.noteContent?.text).toBe('Updated note');
142 });
143
144 it('should fail if URL card does not exist', async () => {
145 const request = {
146 cardId: 'nonexistent-card-id',
147 curatorId: curatorId.value,
148 note: 'This should fail',
149 };
150
151 const result = await useCase.execute(request);
152
153 expect(result.isErr()).toBe(true);
154 if (result.isErr()) {
155 expect(result.error.message).toContain(
156 'URL card not found. Please add the URL to your library first.',
157 );
158 }
159 });
160 });
161
162 describe('Collection management', () => {
163 it('should add URL card to open collections', async () => {
164 const url = 'https://example.com/article';
165
166 // Create open collections
167 const collection1 = new CollectionBuilder()
168 .withAuthorId(curatorId.value)
169 .withName('Open Collection 1')
170 .withAccessType('OPEN')
171 .build();
172 const collection2 = new CollectionBuilder()
173 .withAuthorId(curatorId.value)
174 .withName('Open Collection 2')
175 .withAccessType('OPEN')
176 .build();
177
178 if (collection1 instanceof Error || collection2 instanceof Error) {
179 throw new Error('Failed to create collections');
180 }
181
182 await collectionRepository.save(collection1);
183 await collectionRepository.save(collection2);
184
185 // Add URL to library
186 const addResult = await addUrlToLibraryUseCase.execute({
187 url,
188 curatorId: curatorId.value,
189 });
190 expect(addResult.isOk()).toBe(true);
191 const urlCardId = addResult.unwrap().urlCardId;
192
193 // Add to collections
194 const updateRequest = {
195 cardId: urlCardId,
196 curatorId: curatorId.value,
197 addToCollections: [
198 collection1.collectionId.getStringValue(),
199 collection2.collectionId.getStringValue(),
200 ],
201 };
202
203 const result = await useCase.execute(updateRequest);
204
205 expect(result.isOk()).toBe(true);
206 const response = result.unwrap();
207 expect(response.addedToCollections).toHaveLength(2);
208 expect(response.addedToCollections).toContain(
209 collection1.collectionId.getStringValue(),
210 );
211 expect(response.addedToCollections).toContain(
212 collection2.collectionId.getStringValue(),
213 );
214 });
215
216 it('should add URL card to closed collections when user is the author', async () => {
217 const url = 'https://example.com/article';
218
219 // Create closed collection owned by the curator
220 const collection = new CollectionBuilder()
221 .withAuthorId(curatorId.value)
222 .withName('Closed Collection')
223 .withAccessType('CLOSED')
224 .build();
225
226 if (collection instanceof Error) {
227 throw new Error('Failed to create collection');
228 }
229
230 await collectionRepository.save(collection);
231
232 // Add URL to library
233 const addResult = await addUrlToLibraryUseCase.execute({
234 url,
235 curatorId: curatorId.value,
236 });
237 expect(addResult.isOk()).toBe(true);
238 const urlCardId = addResult.unwrap().urlCardId;
239
240 // Add to collection
241 const updateRequest = {
242 cardId: urlCardId,
243 curatorId: curatorId.value,
244 addToCollections: [collection.collectionId.getStringValue()],
245 };
246
247 const result = await useCase.execute(updateRequest);
248
249 expect(result.isOk()).toBe(true);
250 const response = result.unwrap();
251 expect(response.addedToCollections).toHaveLength(1);
252 expect(response.addedToCollections).toContain(
253 collection.collectionId.getStringValue(),
254 );
255 });
256
257 it('should fail to add URL card to closed collections when user lacks permission', async () => {
258 const url = 'https://example.com/article';
259 const otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap();
260
261 // Create closed collection owned by someone else
262 const collection = new CollectionBuilder()
263 .withAuthorId(otherCuratorId.value)
264 .withName('Private Closed Collection')
265 .withAccessType('CLOSED')
266 .build();
267
268 if (collection instanceof Error) {
269 throw new Error('Failed to create collection');
270 }
271
272 await collectionRepository.save(collection);
273
274 // Add URL to library
275 const addResult = await addUrlToLibraryUseCase.execute({
276 url,
277 curatorId: curatorId.value,
278 });
279 expect(addResult.isOk()).toBe(true);
280 const urlCardId = addResult.unwrap().urlCardId;
281
282 // Try to add to collection
283 const updateRequest = {
284 cardId: urlCardId,
285 curatorId: curatorId.value,
286 addToCollections: [collection.collectionId.getStringValue()],
287 };
288
289 const result = await useCase.execute(updateRequest);
290
291 expect(result.isErr()).toBe(true);
292 if (result.isErr()) {
293 expect(result.error.message).toContain('does not have permission');
294 }
295 });
296
297 it('should add URL card to open collections owned by other users', async () => {
298 const url = 'https://example.com/article';
299 const otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap();
300
301 // Create open collection owned by someone else
302 const collection = new CollectionBuilder()
303 .withAuthorId(otherCuratorId.value)
304 .withName('Public Open Collection')
305 .withAccessType('OPEN')
306 .build();
307
308 if (collection instanceof Error) {
309 throw new Error('Failed to create collection');
310 }
311
312 await collectionRepository.save(collection);
313
314 // Add URL to library
315 const addResult = await addUrlToLibraryUseCase.execute({
316 url,
317 curatorId: curatorId.value,
318 });
319 expect(addResult.isOk()).toBe(true);
320 const urlCardId = addResult.unwrap().urlCardId;
321
322 // Add to collection
323 const updateRequest = {
324 cardId: urlCardId,
325 curatorId: curatorId.value,
326 addToCollections: [collection.collectionId.getStringValue()],
327 };
328
329 const result = await useCase.execute(updateRequest);
330
331 expect(result.isOk()).toBe(true);
332 const response = result.unwrap();
333 expect(response.addedToCollections).toHaveLength(1);
334 expect(response.addedToCollections).toContain(
335 collection.collectionId.getStringValue(),
336 );
337 });
338
339 it('should add URL card to collections with viaCardId', async () => {
340 const url = 'https://example.com/article';
341
342 // Create collection
343 const collection = new CollectionBuilder()
344 .withAuthorId(curatorId.value)
345 .withName('Test Collection')
346 .build();
347
348 if (collection instanceof Error) {
349 throw new Error('Failed to create collection');
350 }
351
352 await collectionRepository.save(collection);
353
354 // Create a via card first
355 const viaCardResult = await addUrlToLibraryUseCase.execute({
356 url: 'https://example.com/via-article',
357 curatorId: curatorId.value,
358 });
359 expect(viaCardResult.isOk()).toBe(true);
360 const viaCardId = viaCardResult.unwrap().urlCardId;
361
362 // Add URL to library
363 const addResult = await addUrlToLibraryUseCase.execute({
364 url,
365 curatorId: curatorId.value,
366 });
367 expect(addResult.isOk()).toBe(true);
368 const urlCardId = addResult.unwrap().urlCardId;
369
370 // Add to collection with viaCardId
371 const updateRequest = {
372 cardId: urlCardId,
373 curatorId: curatorId.value,
374 addToCollections: [collection.collectionId.getStringValue()],
375 viaCardId: viaCardId,
376 };
377
378 const result = await useCase.execute(updateRequest);
379
380 expect(result.isOk()).toBe(true);
381 const response = result.unwrap();
382 expect(response.addedToCollections).toHaveLength(1);
383 expect(response.addedToCollections).toContain(
384 collection.collectionId.getStringValue(),
385 );
386
387 // Verify the collection link was published
388 const publishedLinks = collectionPublisher.getPublishedLinksForCollection(
389 collection.collectionId.getStringValue(),
390 );
391 expect(publishedLinks).toHaveLength(1);
392
393 // Verify the published link is for the correct URL card
394 const publishedLink = publishedLinks[0];
395 expect(publishedLink?.cardId).toBe(urlCardId);
396 });
397
398 it('should remove URL card from open collections', async () => {
399 const url = 'https://example.com/article';
400
401 // Create open collection
402 const collection = new CollectionBuilder()
403 .withAuthorId(curatorId.value)
404 .withName('Open Test Collection')
405 .withAccessType('OPEN')
406 .build();
407
408 if (collection instanceof Error) {
409 throw new Error('Failed to create collection');
410 }
411
412 await collectionRepository.save(collection);
413
414 // Add URL to library and collection
415 const addResult = await addUrlToLibraryUseCase.execute({
416 url,
417 collectionIds: [collection.collectionId.getStringValue()],
418 curatorId: curatorId.value,
419 });
420 expect(addResult.isOk()).toBe(true);
421 const urlCardId = addResult.unwrap().urlCardId;
422
423 // Remove from collection
424 const updateRequest = {
425 cardId: urlCardId,
426 curatorId: curatorId.value,
427 removeFromCollections: [collection.collectionId.getStringValue()],
428 };
429
430 const result = await useCase.execute(updateRequest);
431
432 expect(result.isOk()).toBe(true);
433 const response = result.unwrap();
434 expect(response.removedFromCollections).toHaveLength(1);
435 expect(response.removedFromCollections).toContain(
436 collection.collectionId.getStringValue(),
437 );
438 });
439
440 it('should remove URL card from closed collections when user is the author', async () => {
441 const url = 'https://example.com/article';
442
443 // Create closed collection owned by the curator
444 const collection = new CollectionBuilder()
445 .withAuthorId(curatorId.value)
446 .withName('Closed Test Collection')
447 .withAccessType('CLOSED')
448 .build();
449
450 if (collection instanceof Error) {
451 throw new Error('Failed to create collection');
452 }
453
454 await collectionRepository.save(collection);
455
456 // Add URL to library and collection
457 const addResult = await addUrlToLibraryUseCase.execute({
458 url,
459 collectionIds: [collection.collectionId.getStringValue()],
460 curatorId: curatorId.value,
461 });
462 expect(addResult.isOk()).toBe(true);
463 const urlCardId = addResult.unwrap().urlCardId;
464
465 // Remove from collection
466 const updateRequest = {
467 cardId: urlCardId,
468 curatorId: curatorId.value,
469 removeFromCollections: [collection.collectionId.getStringValue()],
470 };
471
472 const result = await useCase.execute(updateRequest);
473
474 expect(result.isOk()).toBe(true);
475 const response = result.unwrap();
476 expect(response.removedFromCollections).toHaveLength(1);
477 expect(response.removedFromCollections).toContain(
478 collection.collectionId.getStringValue(),
479 );
480 });
481
482 it('should fail to remove URL card from closed collections when user lacks permission', async () => {
483 const url = 'https://example.com/article';
484 const otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap();
485
486 // Create closed collection owned by someone else
487 const collection = new CollectionBuilder()
488 .withAuthorId(otherCuratorId.value)
489 .withName('Private Closed Collection')
490 .withAccessType('CLOSED')
491 .build();
492
493 if (collection instanceof Error) {
494 throw new Error('Failed to create collection');
495 }
496
497 await collectionRepository.save(collection);
498
499 // Add URL to library first
500 const addResult = await addUrlToLibraryUseCase.execute({
501 url,
502 curatorId: curatorId.value,
503 });
504 expect(addResult.isOk()).toBe(true);
505 const urlCardId = addResult.unwrap().urlCardId;
506
507 // Manually add card to collection (as collection owner) to simulate existing state
508 const cardResult = await cardRepository.findById(
509 CardId.createFromString(urlCardId).unwrap(),
510 );
511 expect(cardResult.isOk()).toBe(true);
512 const card = cardResult.unwrap()!;
513
514 const addCardResult = collection.addCard(card.cardId, otherCuratorId);
515 expect(addCardResult.isOk()).toBe(true);
516 await collectionRepository.save(collection);
517
518 // Try to remove from collection
519 const updateRequest = {
520 cardId: urlCardId,
521 curatorId: curatorId.value,
522 removeFromCollections: [collection.collectionId.getStringValue()],
523 };
524
525 const result = await useCase.execute(updateRequest);
526
527 expect(result.isErr()).toBe(true);
528 if (result.isErr()) {
529 expect(result.error.message).toContain('does not have permission');
530 }
531 });
532
533 it('should remove URL card from open collections owned by other users', async () => {
534 const url = 'https://example.com/article';
535 const otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap();
536
537 // Create open collection owned by someone else
538 const collection = new CollectionBuilder()
539 .withAuthorId(otherCuratorId.value)
540 .withName('Public Open Collection')
541 .withAccessType('OPEN')
542 .build();
543
544 if (collection instanceof Error) {
545 throw new Error('Failed to create collection');
546 }
547
548 await collectionRepository.save(collection);
549
550 // Add URL to library first
551 const addResult = await addUrlToLibraryUseCase.execute({
552 url,
553 curatorId: curatorId.value,
554 });
555 expect(addResult.isOk()).toBe(true);
556 const urlCardId = addResult.unwrap().urlCardId;
557
558 // Add card to collection as the current user (curatorId)
559 // In an OPEN collection, users can only remove cards they added themselves
560 const cardResult = await cardRepository.findById(
561 CardId.createFromString(urlCardId).unwrap(),
562 );
563 expect(cardResult.isOk()).toBe(true);
564 const card = cardResult.unwrap()!;
565
566 const addCardResult = collection.addCard(card.cardId, curatorId);
567 expect(addCardResult.isOk()).toBe(true);
568 await collectionRepository.save(collection);
569
570 // Remove from collection - should succeed because curatorId added the card
571 const updateRequest = {
572 cardId: urlCardId,
573 curatorId: curatorId.value,
574 removeFromCollections: [collection.collectionId.getStringValue()],
575 };
576
577 const result = await useCase.execute(updateRequest);
578
579 expect(result.isOk()).toBe(true);
580 const response = result.unwrap();
581 expect(response.removedFromCollections).toHaveLength(1);
582 expect(response.removedFromCollections).toContain(
583 collection.collectionId.getStringValue(),
584 );
585 });
586
587 it('should add and remove from different collections in same request', async () => {
588 const url = 'https://example.com/article';
589
590 // Create collections
591 const collection1 = new CollectionBuilder()
592 .withAuthorId(curatorId.value)
593 .withName('Collection 1')
594 .build();
595 const collection2 = new CollectionBuilder()
596 .withAuthorId(curatorId.value)
597 .withName('Collection 2')
598 .build();
599 const collection3 = new CollectionBuilder()
600 .withAuthorId(curatorId.value)
601 .withName('Collection 3')
602 .build();
603
604 if (
605 collection1 instanceof Error ||
606 collection2 instanceof Error ||
607 collection3 instanceof Error
608 ) {
609 throw new Error('Failed to create collections');
610 }
611
612 await collectionRepository.save(collection1);
613 await collectionRepository.save(collection2);
614 await collectionRepository.save(collection3);
615
616 // Add URL to library with collection1
617 const addResult = await addUrlToLibraryUseCase.execute({
618 url,
619 collectionIds: [collection1.collectionId.getStringValue()],
620 curatorId: curatorId.value,
621 });
622 expect(addResult.isOk()).toBe(true);
623 const urlCardId = addResult.unwrap().urlCardId;
624
625 // Add to collection2 and collection3, remove from collection1
626 const updateRequest = {
627 cardId: urlCardId,
628 curatorId: curatorId.value,
629 addToCollections: [
630 collection2.collectionId.getStringValue(),
631 collection3.collectionId.getStringValue(),
632 ],
633 removeFromCollections: [collection1.collectionId.getStringValue()],
634 };
635
636 const result = await useCase.execute(updateRequest);
637
638 expect(result.isOk()).toBe(true);
639 const response = result.unwrap();
640 expect(response.addedToCollections).toHaveLength(2);
641 expect(response.removedFromCollections).toHaveLength(1);
642 expect(response.addedToCollections).toContain(
643 collection2.collectionId.getStringValue(),
644 );
645 expect(response.addedToCollections).toContain(
646 collection3.collectionId.getStringValue(),
647 );
648 expect(response.removedFromCollections).toContain(
649 collection1.collectionId.getStringValue(),
650 );
651 });
652 });
653
654 describe('Combined operations', () => {
655 it('should update note and manage collections in same request', async () => {
656 const url = 'https://example.com/article';
657
658 // Create collection
659 const collection = new CollectionBuilder()
660 .withAuthorId(curatorId.value)
661 .withName('Test Collection')
662 .build();
663
664 if (collection instanceof Error) {
665 throw new Error('Failed to create collection');
666 }
667
668 await collectionRepository.save(collection);
669
670 // Add URL to library
671 const addResult = await addUrlToLibraryUseCase.execute({
672 url,
673 curatorId: curatorId.value,
674 });
675 expect(addResult.isOk()).toBe(true);
676 const urlCardId = addResult.unwrap().urlCardId;
677
678 // Update note and add to collection
679 const updateRequest = {
680 cardId: urlCardId,
681 curatorId: curatorId.value,
682 note: 'My note about this article',
683 addToCollections: [collection.collectionId.getStringValue()],
684 };
685
686 const result = await useCase.execute(updateRequest);
687
688 expect(result.isOk()).toBe(true);
689 const response = result.unwrap();
690 expect(response.noteCardId).toBeDefined();
691 expect(response.addedToCollections).toHaveLength(1);
692 expect(response.addedToCollections).toContain(
693 collection.collectionId.getStringValue(),
694 );
695
696 // Verify note was created
697 const savedCards = cardRepository.getAllCards();
698 const noteCard = savedCards.find(
699 (card) => card.content.type === CardTypeEnum.NOTE,
700 );
701 expect(noteCard).toBeDefined();
702 expect(noteCard?.content.noteContent?.text).toBe(
703 'My note about this article',
704 );
705 });
706 });
707
708 describe('Publishing failures', () => {
709 it('should not save note when card publishing fails', async () => {
710 const url = 'https://example.com/article';
711
712 // First, add the URL to the library
713 const addResult = await addUrlToLibraryUseCase.execute({
714 url,
715 curatorId: curatorId.value,
716 });
717 expect(addResult.isOk()).toBe(true);
718 const urlCardId = addResult.unwrap().urlCardId;
719
720 // Get initial card count
721 const initialCards = cardRepository.getAllCards();
722 const initialCardCount = initialCards.length;
723
724 // Configure card publisher to fail
725 cardPublisher.setShouldFail(true);
726
727 const updateRequest = {
728 cardId: urlCardId,
729 curatorId: curatorId.value,
730 note: 'This note should not be saved',
731 };
732
733 const result = await useCase.execute(updateRequest);
734
735 expect(result.isErr()).toBe(true);
736 if (result.isErr()) {
737 expect(result.error.message).toContain('Failed to publish');
738 }
739
740 // Verify no new cards were saved when publishing failed
741 const finalCards = cardRepository.getAllCards();
742 expect(finalCards).toHaveLength(initialCardCount);
743
744 // Verify no note card was created
745 const noteCards = finalCards.filter(
746 (card) => card.content.type === CardTypeEnum.NOTE,
747 );
748 expect(noteCards).toHaveLength(0);
749 });
750
751 it('should not save collection associations when collection publishing fails', async () => {
752 const url = 'https://example.com/article';
753
754 // Create collection
755 const collection = new CollectionBuilder()
756 .withAuthorId(curatorId.value)
757 .withName('Test Collection')
758 .build();
759
760 if (collection instanceof Error) {
761 throw new Error('Failed to create collection');
762 }
763
764 await collectionRepository.save(collection);
765
766 // Add URL to library
767 const addResult = await addUrlToLibraryUseCase.execute({
768 url,
769 curatorId: curatorId.value,
770 });
771 expect(addResult.isOk()).toBe(true);
772 const urlCardId = addResult.unwrap().urlCardId;
773
774 // Configure collection publisher to fail
775 collectionPublisher.setShouldFail(true);
776
777 const updateRequest = {
778 cardId: urlCardId,
779 curatorId: curatorId.value,
780 addToCollections: [collection.collectionId.getStringValue()],
781 };
782
783 const result = await useCase.execute(updateRequest);
784
785 expect(result.isErr()).toBe(true);
786 if (result.isErr()) {
787 expect(result.error.message).toContain('Failed to publish');
788 }
789
790 // Verify no collection links were published
791 const publishedLinks = collectionPublisher.getPublishedLinksForCollection(
792 collection.collectionId.getStringValue(),
793 );
794 expect(publishedLinks).toHaveLength(0);
795 });
796 });
797
798 describe('Validation', () => {
799 it('should fail when card does not exist', async () => {
800 const request = {
801 cardId: 'nonexistent-card-id',
802 curatorId: curatorId.value,
803 note: 'This should fail',
804 };
805
806 const result = await useCase.execute(request);
807
808 expect(result.isErr()).toBe(true);
809 if (result.isErr()) {
810 expect(result.error.message).toContain(
811 'URL card not found. Please add the URL to your library first.',
812 );
813 }
814 });
815
816 it('should fail with invalid curator ID', async () => {
817 const url = 'https://example.com/article';
818
819 // Add URL to library first
820 const addResult = await addUrlToLibraryUseCase.execute({
821 url,
822 curatorId: curatorId.value,
823 });
824 expect(addResult.isOk()).toBe(true);
825 const urlCardId = addResult.unwrap().urlCardId;
826
827 const request = {
828 cardId: urlCardId,
829 curatorId: 'invalid-curator-id',
830 note: 'This should fail',
831 };
832
833 const result = await useCase.execute(request);
834
835 expect(result.isErr()).toBe(true);
836 if (result.isErr()) {
837 expect(result.error.message).toContain('Invalid curator ID');
838 }
839 });
840
841 it('should fail with invalid collection ID', async () => {
842 const url = 'https://example.com/article';
843
844 // Add URL to library
845 const addResult = await addUrlToLibraryUseCase.execute({
846 url,
847 curatorId: curatorId.value,
848 });
849 expect(addResult.isOk()).toBe(true);
850 const urlCardId = addResult.unwrap().urlCardId;
851
852 const request = {
853 cardId: urlCardId,
854 curatorId: curatorId.value,
855 addToCollections: ['invalid-collection-id'],
856 };
857
858 const result = await useCase.execute(request);
859
860 expect(result.isErr()).toBe(true);
861 if (result.isErr()) {
862 expect(result.error.message).toContain('Collection not found');
863 }
864 });
865 });
866});