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