A social knowledge tool for researchers built on ATProto
1import { 2 PostgreSqlContainer, 3 StartedPostgreSqlContainer, 4} from '@testcontainers/postgresql'; 5import postgres from 'postgres'; 6import { drizzle, PostgresJsDatabase } from 'drizzle-orm/postgres-js'; 7import { DrizzleCardQueryRepository } from '../../infrastructure/repositories/DrizzleCardQueryRepository'; 8import { DrizzleCardRepository } from '../../infrastructure/repositories/DrizzleCardRepository'; 9import { DrizzleCollectionRepository } from '../../infrastructure/repositories/DrizzleCollectionRepository'; 10import { CuratorId } from '../../domain/value-objects/CuratorId'; 11import { UniqueEntityID } from '../../../../shared/domain/UniqueEntityID'; 12import { cards } from '../../infrastructure/repositories/schema/card.sql'; 13import { 14 collections, 15 collectionCards, 16} from '../../infrastructure/repositories/schema/collection.sql'; 17import { libraryMemberships } from '../../infrastructure/repositories/schema/libraryMembership.sql'; 18import { publishedRecords } from '../../infrastructure/repositories/schema/publishedRecord.sql'; 19import { Collection, CollectionAccessType } from '../../domain/Collection'; 20import { CardBuilder } from '../utils/builders/CardBuilder'; 21import { URL } from '../../domain/value-objects/URL'; 22import { UrlMetadata } from '../../domain/value-objects/UrlMetadata'; 23import { createTestSchema } from '../test-utils/createTestSchema'; 24import { CardTypeEnum } from '../../domain/value-objects/CardType'; 25 26describe('DrizzleCardQueryRepository - getUrlCardView', () => { 27 let container: StartedPostgreSqlContainer; 28 let db: PostgresJsDatabase; 29 let queryRepository: DrizzleCardQueryRepository; 30 let cardRepository: DrizzleCardRepository; 31 let collectionRepository: DrizzleCollectionRepository; 32 33 // Test data 34 let curatorId: CuratorId; 35 let otherCuratorId: CuratorId; 36 let thirdCuratorId: CuratorId; 37 38 // Setup before all tests 39 beforeAll(async () => { 40 // Start PostgreSQL container 41 container = await new PostgreSqlContainer('postgres:14').start(); 42 43 // Create database connection 44 const connectionString = container.getConnectionUri(); 45 process.env.DATABASE_URL = connectionString; 46 const client = postgres(connectionString); 47 db = drizzle(client); 48 49 // Create repositories 50 queryRepository = new DrizzleCardQueryRepository(db); 51 cardRepository = new DrizzleCardRepository(db); 52 collectionRepository = new DrizzleCollectionRepository(db); 53 54 // Create schema using helper function 55 await createTestSchema(db); 56 57 // Create test data 58 curatorId = CuratorId.create('did:plc:testcurator').unwrap(); 59 otherCuratorId = CuratorId.create('did:plc:othercurator').unwrap(); 60 thirdCuratorId = CuratorId.create('did:plc:thirdcurator').unwrap(); 61 }, 60000); // Increase timeout for container startup 62 63 // Cleanup after all tests 64 afterAll(async () => { 65 // Stop container 66 await container.stop(); 67 }); 68 69 // Clear data between tests 70 beforeEach(async () => { 71 await db.delete(collectionCards); 72 await db.delete(collections); 73 await db.delete(libraryMemberships); 74 await db.delete(cards); 75 await db.delete(publishedRecords); 76 }); 77 78 describe('getUrlCardView', () => { 79 it('should return null when card does not exist', async () => { 80 const nonExistentCardId = new UniqueEntityID().toString(); 81 82 const result = await queryRepository.getUrlCardView(nonExistentCardId); 83 84 expect(result).toBeNull(); 85 }); 86 87 it('should return null when card exists but is not a URL card', async () => { 88 // Create a note card 89 const noteCard = new CardBuilder() 90 .withCuratorId(curatorId.value) 91 .withNoteCard('This is a note') 92 .buildOrThrow(); 93 94 await cardRepository.save(noteCard); 95 96 const result = await queryRepository.getUrlCardView( 97 noteCard.cardId.getStringValue(), 98 ); 99 100 expect(result).toBeNull(); 101 }); 102 103 it('should return URL card view with basic metadata', async () => { 104 // Create URL card with metadata 105 const url = URL.create('https://example.com/article').unwrap(); 106 const urlMetadata = UrlMetadata.create({ 107 url: url.value, 108 title: 'Test Article', 109 description: 'A test article description', 110 author: 'John Doe', 111 imageUrl: 'https://example.com/image.jpg', 112 }).unwrap(); 113 114 const urlCard = new CardBuilder() 115 .withCuratorId(curatorId.value) 116 .withUrlCard(url, urlMetadata) 117 .buildOrThrow(); 118 119 await cardRepository.save(urlCard); 120 121 const result = await queryRepository.getUrlCardView( 122 urlCard.cardId.getStringValue(), 123 ); 124 125 expect(result).toBeDefined(); 126 expect(result?.id).toBe(urlCard.cardId.getStringValue()); 127 expect(result?.type).toBe(CardTypeEnum.URL); 128 expect(result?.url).toBe(url.value); 129 expect(result?.cardContent.title).toBe('Test Article'); 130 expect(result?.cardContent.description).toBe( 131 'A test article description', 132 ); 133 expect(result?.cardContent.author).toBe('John Doe'); 134 expect(result?.cardContent.thumbnailUrl).toBe( 135 'https://example.com/image.jpg', 136 ); 137 expect(result?.libraries).toEqual([]); 138 expect(result?.collections).toEqual([]); 139 }); 140 141 it('should return URL card view with minimal metadata', async () => { 142 // Create URL card without metadata 143 const url = URL.create('https://example.com/minimal').unwrap(); 144 const urlCard = new CardBuilder() 145 .withCuratorId(curatorId.value) 146 .withUrlCard(url) 147 .buildOrThrow(); 148 149 await cardRepository.save(urlCard); 150 151 const result = await queryRepository.getUrlCardView( 152 urlCard.cardId.getStringValue(), 153 ); 154 155 expect(result).toBeDefined(); 156 expect(result?.id).toBe(urlCard.cardId.getStringValue()); 157 expect(result?.type).toBe(CardTypeEnum.URL); 158 expect(result?.url).toBe(url.value); 159 expect(result?.cardContent.title).toBeUndefined(); 160 expect(result?.cardContent.description).toBeUndefined(); 161 expect(result?.cardContent.author).toBeUndefined(); 162 expect(result?.cardContent.thumbnailUrl).toBeUndefined(); 163 }); 164 165 it('should include creator in library when URL card is in their library', async () => { 166 // Create URL card 167 const url = URL.create('https://example.com/creator-library').unwrap(); 168 const urlCard = new CardBuilder() 169 .withCuratorId(curatorId.value) 170 .withUrlCard(url) 171 .buildOrThrow(); 172 173 await cardRepository.save(urlCard); 174 175 // Add card to creator's library (URL cards can only be in creator's library) 176 urlCard.addToLibrary(curatorId); 177 await cardRepository.save(urlCard); 178 179 const result = await queryRepository.getUrlCardView( 180 urlCard.cardId.getStringValue(), 181 ); 182 183 expect(result).toBeDefined(); 184 expect(result?.libraries).toHaveLength(1); 185 expect(result?.libraries[0]?.userId).toBe(curatorId.value); 186 }); 187 188 it('should include collections that contain the card', async () => { 189 // Create URL card 190 const url = URL.create('https://example.com/collection-article').unwrap(); 191 const urlCard = new CardBuilder() 192 .withCuratorId(curatorId.value) 193 .withUrlCard(url) 194 .buildOrThrow(); 195 196 await cardRepository.save(urlCard); 197 198 // Create collections 199 const collection1 = Collection.create( 200 { 201 authorId: curatorId, 202 name: 'Reading List', 203 description: 'Articles to read', 204 accessType: CollectionAccessType.OPEN, 205 collaboratorIds: [], 206 createdAt: new Date(), 207 updatedAt: new Date(), 208 }, 209 new UniqueEntityID(), 210 ).unwrap(); 211 212 const collection2 = Collection.create( 213 { 214 authorId: otherCuratorId, 215 name: 'Favorites', 216 accessType: CollectionAccessType.OPEN, 217 collaboratorIds: [], 218 createdAt: new Date(), 219 updatedAt: new Date(), 220 }, 221 new UniqueEntityID(), 222 ).unwrap(); 223 224 // Add card to collections 225 collection1.addCard(urlCard.cardId, curatorId); 226 collection2.addCard(urlCard.cardId, otherCuratorId); 227 228 await collectionRepository.save(collection1); 229 await collectionRepository.save(collection2); 230 231 const result = await queryRepository.getUrlCardView( 232 urlCard.cardId.getStringValue(), 233 ); 234 235 expect(result).toBeDefined(); 236 expect(result?.collections).toHaveLength(2); 237 238 // Check collection details 239 const collectionNames = result?.collections.map((c) => c.name).sort(); 240 expect(collectionNames).toEqual(['Favorites', 'Reading List']); 241 242 const readingListCollection = result?.collections.find( 243 (c) => c.name === 'Reading List', 244 ); 245 expect(readingListCollection?.id).toBe( 246 collection1.collectionId.getStringValue(), 247 ); 248 expect(readingListCollection?.authorId).toBe(curatorId.value); 249 250 const favoritesCollection = result?.collections.find( 251 (c) => c.name === 'Favorites', 252 ); 253 expect(favoritesCollection?.id).toBe( 254 collection2.collectionId.getStringValue(), 255 ); 256 expect(favoritesCollection?.authorId).toBe(otherCuratorId.value); 257 }); 258 259 it('should handle card with both library and collections', async () => { 260 // Create URL card with full metadata 261 const url = URL.create('https://example.com/comprehensive').unwrap(); 262 const urlMetadata = UrlMetadata.create({ 263 url: url.value, 264 title: 'Comprehensive Article', 265 description: 'An article with everything', 266 author: 'Jane Smith', 267 imageUrl: 'https://example.com/comprehensive.jpg', 268 siteName: 'Example Site', 269 }).unwrap(); 270 271 const urlCard = new CardBuilder() 272 .withCuratorId(curatorId.value) 273 .withUrlCard(url, urlMetadata) 274 .buildOrThrow(); 275 276 await cardRepository.save(urlCard); 277 278 // Add to creator's library (URL cards can only be in creator's library) 279 urlCard.addToLibrary(curatorId); 280 await cardRepository.save(urlCard); 281 282 // Create multiple collections 283 const workCollection = Collection.create( 284 { 285 authorId: curatorId, 286 name: 'Work Research', 287 accessType: CollectionAccessType.CLOSED, 288 collaboratorIds: [], 289 createdAt: new Date(), 290 updatedAt: new Date(), 291 }, 292 new UniqueEntityID(), 293 ).unwrap(); 294 295 const personalCollection = Collection.create( 296 { 297 authorId: curatorId, 298 name: 'Personal Reading', 299 accessType: CollectionAccessType.OPEN, 300 collaboratorIds: [], 301 createdAt: new Date(), 302 updatedAt: new Date(), 303 }, 304 new UniqueEntityID(), 305 ).unwrap(); 306 307 const sharedCollection = Collection.create( 308 { 309 authorId: otherCuratorId, 310 name: 'Shared Articles', 311 accessType: CollectionAccessType.OPEN, 312 collaboratorIds: [], 313 createdAt: new Date(), 314 updatedAt: new Date(), 315 }, 316 new UniqueEntityID(), 317 ).unwrap(); 318 319 // Add card to collections 320 workCollection.addCard(urlCard.cardId, curatorId); 321 personalCollection.addCard(urlCard.cardId, curatorId); 322 sharedCollection.addCard(urlCard.cardId, otherCuratorId); 323 324 await collectionRepository.save(workCollection); 325 await collectionRepository.save(personalCollection); 326 await collectionRepository.save(sharedCollection); 327 328 const result = await queryRepository.getUrlCardView( 329 urlCard.cardId.getStringValue(), 330 ); 331 332 expect(result).toBeDefined(); 333 334 // Check URL metadata 335 expect(result?.cardContent.title).toBe('Comprehensive Article'); 336 expect(result?.cardContent.description).toBe( 337 'An article with everything', 338 ); 339 expect(result?.cardContent.author).toBe('Jane Smith'); 340 expect(result?.cardContent.thumbnailUrl).toBe( 341 'https://example.com/comprehensive.jpg', 342 ); 343 344 // Check libraries - URL cards can only be in creator's library 345 expect(result?.libraries).toHaveLength(1); 346 expect(result?.libraries[0]?.userId).toBe(curatorId.value); 347 348 // Check collections 349 expect(result?.collections).toHaveLength(3); 350 const collectionNames = result?.collections.map((c) => c.name).sort(); 351 expect(collectionNames).toEqual([ 352 'Personal Reading', 353 'Shared Articles', 354 'Work Research', 355 ]); 356 357 // Verify collection authors 358 const workColl = result?.collections.find( 359 (c) => c.name === 'Work Research', 360 ); 361 expect(workColl?.authorId).toBe(curatorId.value); 362 363 const sharedColl = result?.collections.find( 364 (c) => c.name === 'Shared Articles', 365 ); 366 expect(sharedColl?.authorId).toBe(otherCuratorId.value); 367 }); 368 369 it('should handle card with no libraries or collections', async () => { 370 // Create URL card that's not in any libraries or collections 371 const url = URL.create('https://example.com/orphaned').unwrap(); 372 const urlCard = new CardBuilder() 373 .withCuratorId(curatorId.value) 374 .withUrlCard(url) 375 .buildOrThrow(); 376 377 await cardRepository.save(urlCard); 378 379 const result = await queryRepository.getUrlCardView( 380 urlCard.cardId.getStringValue(), 381 ); 382 383 expect(result).toBeDefined(); 384 expect(result?.id).toBe(urlCard.cardId.getStringValue()); 385 expect(result?.type).toBe(CardTypeEnum.URL); 386 expect(result?.url).toBe(url.value); 387 expect(result?.libraries).toEqual([]); 388 expect(result?.collections).toEqual([]); 389 }); 390 391 it('should handle card in library but not in any collections', async () => { 392 // Create URL card 393 const url = URL.create('https://example.com/library-only').unwrap(); 394 const urlCard = new CardBuilder() 395 .withCuratorId(curatorId.value) 396 .withUrlCard(url) 397 .buildOrThrow(); 398 399 await cardRepository.save(urlCard); 400 401 // Add to library only 402 urlCard.addToLibrary(curatorId); 403 await cardRepository.save(urlCard); 404 405 const result = await queryRepository.getUrlCardView( 406 urlCard.cardId.getStringValue(), 407 ); 408 409 expect(result).toBeDefined(); 410 expect(result?.libraries).toHaveLength(1); 411 expect(result?.libraries[0]?.userId).toBe(curatorId.value); 412 expect(result?.collections).toEqual([]); 413 }); 414 415 it('should handle card in collections but not in any libraries', async () => { 416 // Create URL card 417 const url = URL.create('https://example.com/collection-only').unwrap(); 418 const urlCard = new CardBuilder() 419 .withCuratorId(curatorId.value) 420 .withUrlCard(url) 421 .buildOrThrow(); 422 423 await cardRepository.save(urlCard); 424 425 // Create collection and add card 426 const collection = Collection.create( 427 { 428 authorId: curatorId, 429 name: 'Collection Only', 430 accessType: CollectionAccessType.OPEN, 431 collaboratorIds: [], 432 createdAt: new Date(), 433 updatedAt: new Date(), 434 }, 435 new UniqueEntityID(), 436 ).unwrap(); 437 438 collection.addCard(urlCard.cardId, curatorId); 439 await collectionRepository.save(collection); 440 441 const result = await queryRepository.getUrlCardView( 442 urlCard.cardId.getStringValue(), 443 ); 444 445 expect(result).toBeDefined(); 446 expect(result?.libraries).toEqual([]); 447 expect(result?.collections).toHaveLength(1); 448 expect(result?.collections[0]?.name).toBe('Collection Only'); 449 expect(result?.collections[0]?.authorId).toBe(curatorId.value); 450 }); 451 452 it('should handle duplicate library memberships gracefully', async () => { 453 // Create URL card 454 const url = URL.create('https://example.com/duplicate-test').unwrap(); 455 const urlCard = new CardBuilder() 456 .withCuratorId(curatorId.value) 457 .withUrlCard(url) 458 .buildOrThrow(); 459 460 await cardRepository.save(urlCard); 461 462 // Add to library multiple times (should be handled by domain logic) 463 urlCard.addToLibrary(curatorId); 464 await cardRepository.save(urlCard); 465 466 // Try to add again (should not create duplicate) 467 urlCard.addToLibrary(curatorId); 468 await cardRepository.save(urlCard); 469 470 const result = await queryRepository.getUrlCardView( 471 urlCard.cardId.getStringValue(), 472 ); 473 474 expect(result).toBeDefined(); 475 expect(result?.libraries).toHaveLength(1); 476 expect(result?.libraries[0]?.userId).toBe(curatorId.value); 477 }); 478 479 it('should handle multiple collections from different users', async () => { 480 // Create URL card 481 const url = URL.create('https://example.com/popular-article').unwrap(); 482 const urlCard = new CardBuilder() 483 .withCuratorId(curatorId.value) 484 .withUrlCard(url) 485 .buildOrThrow(); 486 487 await cardRepository.save(urlCard); 488 489 // Add to creator's library 490 urlCard.addToLibrary(curatorId); 491 await cardRepository.save(urlCard); 492 493 // Create multiple collections from different users 494 const collections = []; 495 for (let i = 1; i <= 5; i++) { 496 const collection = Collection.create( 497 { 498 authorId: curatorId, 499 name: `Collection ${i}`, 500 accessType: CollectionAccessType.OPEN, 501 collaboratorIds: [], 502 createdAt: new Date(), 503 updatedAt: new Date(), 504 }, 505 new UniqueEntityID(), 506 ).unwrap(); 507 508 collection.addCard(urlCard.cardId, curatorId); 509 await collectionRepository.save(collection); 510 collections.push(collection); 511 } 512 513 const result = await queryRepository.getUrlCardView( 514 urlCard.cardId.getStringValue(), 515 ); 516 517 expect(result).toBeDefined(); 518 // URL cards can only be in creator's library 519 expect(result?.libraries).toHaveLength(1); 520 expect(result?.collections).toHaveLength(5); 521 522 // Verify all collections are present 523 const collectionNames = result?.collections.map((c) => c.name).sort(); 524 expect(collectionNames).toEqual([ 525 'Collection 1', 526 'Collection 2', 527 'Collection 3', 528 'Collection 4', 529 'Collection 5', 530 ]); 531 }); 532 533 it('should include connected note card in URL card view', async () => { 534 // Create URL card with metadata 535 const url = URL.create('https://example.com/article-with-note').unwrap(); 536 const urlMetadata = UrlMetadata.create({ 537 url: url.value, 538 title: 'Article with Note', 539 description: 'An article that has a connected note', 540 author: 'Jane Doe', 541 imageUrl: 'https://example.com/note-article.jpg', 542 }).unwrap(); 543 544 const urlCard = new CardBuilder() 545 .withCuratorId(curatorId.value) 546 .withUrlCard(url, urlMetadata) 547 .buildOrThrow(); 548 549 await cardRepository.save(urlCard); 550 551 // Create connected note card 552 const noteCard = new CardBuilder() 553 .withCuratorId(curatorId.value) 554 .withNoteCard( 555 'This is my detailed analysis of the article. It covers several key points and provides additional insights.', 556 ) 557 .withParentCard(urlCard.cardId) 558 .buildOrThrow(); 559 560 await cardRepository.save(noteCard); 561 562 // Add both cards to user's library 563 urlCard.addToLibrary(curatorId); 564 await cardRepository.save(urlCard); 565 566 noteCard.addToLibrary(curatorId); 567 await cardRepository.save(noteCard); 568 569 const result = await queryRepository.getUrlCardView( 570 urlCard.cardId.getStringValue(), 571 ); 572 573 expect(result).toBeDefined(); 574 expect(result?.id).toBe(urlCard.cardId.getStringValue()); 575 expect(result?.type).toBe(CardTypeEnum.URL); 576 expect(result?.url).toBe(url.value); 577 578 // Check URL metadata 579 expect(result?.cardContent.title).toBe('Article with Note'); 580 expect(result?.cardContent.description).toBe( 581 'An article that has a connected note', 582 ); 583 expect(result?.cardContent.author).toBe('Jane Doe'); 584 expect(result?.cardContent.thumbnailUrl).toBe( 585 'https://example.com/note-article.jpg', 586 ); 587 588 // Check that the connected note is included 589 expect(result?.note).toBeDefined(); 590 expect(result?.note?.id).toBe(noteCard.cardId.getStringValue()); 591 expect(result?.note?.text).toBe( 592 'This is my detailed analysis of the article. It covers several key points and provides additional insights.', 593 ); 594 595 // Check libraries 596 expect(result?.libraries).toHaveLength(1); 597 expect(result?.libraries[0]?.userId).toBe(curatorId.value); 598 }); 599 600 it('should not include note cards that belong to different URL cards', async () => { 601 // Create first URL card 602 const url1 = URL.create('https://example.com/article1').unwrap(); 603 const urlCard1 = new CardBuilder() 604 .withCuratorId(curatorId.value) 605 .withUrlCard(url1) 606 .buildOrThrow(); 607 608 await cardRepository.save(urlCard1); 609 610 // Create second URL card 611 const url2 = URL.create('https://example.com/article2').unwrap(); 612 const urlCard2 = new CardBuilder() 613 .withCuratorId(curatorId.value) 614 .withUrlCard(url2) 615 .buildOrThrow(); 616 617 await cardRepository.save(urlCard2); 618 619 // Create note card connected to the SECOND URL card 620 const noteCard = new CardBuilder() 621 .withCuratorId(curatorId.value) 622 .withNoteCard('This note is for article 2') 623 .withParentCard(urlCard2.cardId) 624 .buildOrThrow(); 625 626 await cardRepository.save(noteCard); 627 628 // Add cards to libraries 629 urlCard1.addToLibrary(curatorId); 630 await cardRepository.save(urlCard1); 631 632 urlCard2.addToLibrary(curatorId); 633 await cardRepository.save(urlCard2); 634 635 noteCard.addToLibrary(curatorId); 636 await cardRepository.save(noteCard); 637 638 // Query the FIRST URL card 639 const result = await queryRepository.getUrlCardView( 640 urlCard1.cardId.getStringValue(), 641 ); 642 643 expect(result).toBeDefined(); 644 expect(result?.id).toBe(urlCard1.cardId.getStringValue()); 645 646 // Should NOT have a note since the note belongs to urlCard2 647 expect(result?.note).toBeUndefined(); 648 }); 649 }); 650 651 describe('getLibrariesForCard', () => { 652 it('should return empty array when card has no library memberships', async () => { 653 // Create URL card but don't add to any libraries 654 const url = URL.create('https://example.com/no-libraries').unwrap(); 655 const urlCard = new CardBuilder() 656 .withCuratorId(curatorId.value) 657 .withUrlCard(url) 658 .buildOrThrow(); 659 660 await cardRepository.save(urlCard); 661 662 const result = await queryRepository.getLibrariesForCard( 663 urlCard.cardId.getStringValue(), 664 ); 665 666 expect(result).toEqual([]); 667 }); 668 669 it('should return single user ID when card is in one library', async () => { 670 // Create URL card 671 const url = URL.create('https://example.com/single-library').unwrap(); 672 const urlCard = new CardBuilder() 673 .withCuratorId(curatorId.value) 674 .withUrlCard(url) 675 .buildOrThrow(); 676 677 await cardRepository.save(urlCard); 678 679 // Add to one user's library 680 urlCard.addToLibrary(curatorId); 681 await cardRepository.save(urlCard); 682 683 const result = await queryRepository.getLibrariesForCard( 684 urlCard.cardId.getStringValue(), 685 ); 686 687 expect(result).toHaveLength(1); 688 expect(result).toContain(curatorId.value); 689 }); 690 691 it('should return creator ID for URL card in creator library', async () => { 692 // Create URL card 693 const url = URL.create('https://example.com/creator-library').unwrap(); 694 const urlCard = new CardBuilder() 695 .withCuratorId(curatorId.value) 696 .withUrlCard(url) 697 .buildOrThrow(); 698 699 await cardRepository.save(urlCard); 700 701 // Add to creator's library (URL cards can only be in creator's library) 702 urlCard.addToLibrary(curatorId); 703 await cardRepository.save(urlCard); 704 705 const result = await queryRepository.getLibrariesForCard( 706 urlCard.cardId.getStringValue(), 707 ); 708 709 expect(result).toHaveLength(1); 710 expect(result).toContain(curatorId.value); 711 }); 712 713 it('should return empty array for non-existent card', async () => { 714 const nonExistentCardId = new UniqueEntityID().toString(); 715 716 const result = 717 await queryRepository.getLibrariesForCard(nonExistentCardId); 718 719 expect(result).toEqual([]); 720 }); 721 722 it('should handle duplicate library memberships gracefully', async () => { 723 // Create URL card 724 const url = URL.create('https://example.com/duplicate-test').unwrap(); 725 const urlCard = new CardBuilder() 726 .withCuratorId(curatorId.value) 727 .withUrlCard(url) 728 .buildOrThrow(); 729 730 await cardRepository.save(urlCard); 731 732 // Add to library multiple times (domain logic should prevent duplicates) 733 urlCard.addToLibrary(curatorId); 734 await cardRepository.save(urlCard); 735 736 urlCard.addToLibrary(curatorId); 737 await cardRepository.save(urlCard); 738 739 const result = await queryRepository.getLibrariesForCard( 740 urlCard.cardId.getStringValue(), 741 ); 742 743 expect(result).toHaveLength(1); 744 expect(result).toContain(curatorId.value); 745 }); 746 747 it('should work with both URL and NOTE cards', async () => { 748 // Create URL card 749 const url = URL.create('https://example.com/with-note').unwrap(); 750 const urlCard = new CardBuilder() 751 .withCuratorId(curatorId.value) 752 .withUrlCard(url) 753 .buildOrThrow(); 754 755 await cardRepository.save(urlCard); 756 757 // Create note card 758 const noteCard = new CardBuilder() 759 .withCuratorId(curatorId.value) 760 .withNoteCard('Test note') 761 .buildOrThrow(); 762 763 await cardRepository.save(noteCard); 764 765 // Add both cards to libraries 766 urlCard.addToLibrary(curatorId); 767 await cardRepository.save(urlCard); 768 769 noteCard.addToLibrary(otherCuratorId); 770 await cardRepository.save(noteCard); 771 772 // Test URL card libraries 773 const urlResult = await queryRepository.getLibrariesForCard( 774 urlCard.cardId.getStringValue(), 775 ); 776 expect(urlResult).toEqual([curatorId.value]); 777 778 // Test note card libraries 779 const noteResult = await queryRepository.getLibrariesForCard( 780 noteCard.cardId.getStringValue(), 781 ); 782 expect(noteResult).toEqual([otherCuratorId.value]); 783 }); 784 785 it('should return libraries in consistent order', async () => { 786 // Create URL card 787 const url = URL.create('https://example.com/order-test').unwrap(); 788 const urlCard = new CardBuilder() 789 .withCuratorId(thirdCuratorId.value) 790 .withUrlCard(url) 791 .buildOrThrow(); 792 793 await cardRepository.save(urlCard); 794 795 // Add to creator's library (URL cards can only be in creator's library) 796 urlCard.addToLibrary(thirdCuratorId); 797 await cardRepository.save(urlCard); 798 799 const result = await queryRepository.getLibrariesForCard( 800 urlCard.cardId.getStringValue(), 801 ); 802 803 expect(result).toHaveLength(1); 804 expect(result).toContain(thirdCuratorId.value); 805 }); 806 }); 807});