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 / UpdateUrlCardAssociationsUseCase.test.ts
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});