A social knowledge tool for researchers built on ATProto
at main 802 lines 25 kB view raw
1import { GetCollectionsForUrlUseCase } from '../../application/useCases/queries/GetCollectionsForUrlUseCase'; 2import { InMemoryCardRepository } from '../utils/InMemoryCardRepository'; 3import { InMemoryCollectionRepository } from '../utils/InMemoryCollectionRepository'; 4import { InMemoryCollectionQueryRepository } from '../utils/InMemoryCollectionQueryRepository'; 5import { CuratorId } from '../../domain/value-objects/CuratorId'; 6import { CardBuilder } from '../utils/builders/CardBuilder'; 7import { CollectionBuilder } from '../utils/builders/CollectionBuilder'; 8import { CardTypeEnum } from '../../domain/value-objects/CardType'; 9import { URL } from '../../domain/value-objects/URL'; 10import { PublishedRecordId } from '../../domain/value-objects/PublishedRecordId'; 11import { 12 CollectionSortField, 13 SortOrder, 14} from '../../domain/ICollectionQueryRepository'; 15import { FakeProfileService } from '../utils/FakeProfileService'; 16 17describe('GetCollectionsForUrlUseCase', () => { 18 let useCase: GetCollectionsForUrlUseCase; 19 let cardRepository: InMemoryCardRepository; 20 let collectionRepository: InMemoryCollectionRepository; 21 let collectionQueryRepository: InMemoryCollectionQueryRepository; 22 let profileService: FakeProfileService; 23 let curator1: CuratorId; 24 let curator2: CuratorId; 25 let curator3: CuratorId; 26 27 beforeEach(() => { 28 cardRepository = InMemoryCardRepository.getInstance(); 29 collectionRepository = InMemoryCollectionRepository.getInstance(); 30 collectionQueryRepository = new InMemoryCollectionQueryRepository( 31 collectionRepository, 32 cardRepository, 33 ); 34 profileService = new FakeProfileService(); 35 36 useCase = new GetCollectionsForUrlUseCase( 37 collectionQueryRepository, 38 profileService, 39 collectionRepository, 40 ); 41 42 curator1 = CuratorId.create('did:plc:curator1').unwrap(); 43 curator2 = CuratorId.create('did:plc:curator2').unwrap(); 44 curator3 = CuratorId.create('did:plc:curator3').unwrap(); 45 46 // Add profiles for test curators 47 profileService.addProfile({ 48 id: curator1.value, 49 name: 'Curator One', 50 handle: 'curator1', 51 avatarUrl: 'https://example.com/avatar1.jpg', 52 }); 53 54 profileService.addProfile({ 55 id: curator2.value, 56 name: 'Curator Two', 57 handle: 'curator2', 58 avatarUrl: 'https://example.com/avatar2.jpg', 59 }); 60 61 profileService.addProfile({ 62 id: curator3.value, 63 name: 'Curator Three', 64 handle: 'curator3', 65 avatarUrl: 'https://example.com/avatar3.jpg', 66 }); 67 }); 68 69 afterEach(() => { 70 cardRepository.clear(); 71 collectionRepository.clear(); 72 collectionQueryRepository.clear(); 73 profileService.clear(); 74 }); 75 76 describe('Collections with URL cards', () => { 77 it('should return all collections containing cards with the specified URL', async () => { 78 const testUrl = 'https://example.com/shared-article'; 79 const url = URL.create(testUrl).unwrap(); 80 81 // Create URL cards for different users with the same URL 82 const card1 = new CardBuilder() 83 .withCuratorId(curator1.value) 84 .withType(CardTypeEnum.URL) 85 .withUrl(url) 86 .build(); 87 88 const card2 = new CardBuilder() 89 .withCuratorId(curator2.value) 90 .withType(CardTypeEnum.URL) 91 .withUrl(url) 92 .build(); 93 94 const card3 = new CardBuilder() 95 .withCuratorId(curator3.value) 96 .withType(CardTypeEnum.URL) 97 .withUrl(url) 98 .build(); 99 100 if ( 101 card1 instanceof Error || 102 card2 instanceof Error || 103 card3 instanceof Error 104 ) { 105 throw new Error('Failed to create cards'); 106 } 107 108 // Add cards to their respective libraries 109 card1.addToLibrary(curator1); 110 card2.addToLibrary(curator2); 111 card3.addToLibrary(curator3); 112 113 await cardRepository.save(card1); 114 await cardRepository.save(card2); 115 await cardRepository.save(card3); 116 117 // Create collections for each user and add their cards 118 const collection1 = new CollectionBuilder() 119 .withAuthorId(curator1.value) 120 .withName('Tech Articles') 121 .withDescription('My tech articles') 122 .build(); 123 124 const collection2 = new CollectionBuilder() 125 .withAuthorId(curator2.value) 126 .withName('Reading List') 127 .withDescription('Articles to read') 128 .build(); 129 130 const collection3 = new CollectionBuilder() 131 .withAuthorId(curator3.value) 132 .withName('Favorites') 133 .build(); 134 135 if ( 136 collection1 instanceof Error || 137 collection2 instanceof Error || 138 collection3 instanceof Error 139 ) { 140 throw new Error('Failed to create collections'); 141 } 142 143 // Add cards to collections 144 collection1.addCard(card1.cardId, curator1); 145 collection2.addCard(card2.cardId, curator2); 146 collection3.addCard(card3.cardId, curator3); 147 148 // Mark collections as published 149 const publishedRecordId1 = PublishedRecordId.create({ 150 uri: 'at://did:plc:curator1/network.cosmik.collection/collection1', 151 cid: 'bafyreicollection1', 152 }); 153 const publishedRecordId2 = PublishedRecordId.create({ 154 uri: 'at://did:plc:curator2/network.cosmik.collection/collection2', 155 cid: 'bafyreicollection2', 156 }); 157 const publishedRecordId3 = PublishedRecordId.create({ 158 uri: 'at://did:plc:curator3/network.cosmik.collection/collection3', 159 cid: 'bafyreicollection3', 160 }); 161 162 collection1.markAsPublished(publishedRecordId1); 163 collection2.markAsPublished(publishedRecordId2); 164 collection3.markAsPublished(publishedRecordId3); 165 166 await collectionRepository.save(collection1); 167 await collectionRepository.save(collection2); 168 await collectionRepository.save(collection3); 169 170 // Execute the use case 171 const query = { 172 url: testUrl, 173 }; 174 175 const result = await useCase.execute(query); 176 177 // Verify the result 178 expect(result.isOk()).toBe(true); 179 const response = result.unwrap(); 180 181 expect(response.collections).toHaveLength(3); 182 expect(response.pagination.totalCount).toBe(3); 183 184 // Check that all three collections are included 185 const collectionIds = response.collections.map((c) => c.id); 186 expect(collectionIds).toContain( 187 collection1.collectionId.getStringValue(), 188 ); 189 expect(collectionIds).toContain( 190 collection2.collectionId.getStringValue(), 191 ); 192 expect(collectionIds).toContain( 193 collection3.collectionId.getStringValue(), 194 ); 195 196 // Verify collection details 197 const techArticles = response.collections.find( 198 (c) => c.name === 'Tech Articles', 199 ); 200 expect(techArticles).toBeDefined(); 201 expect(techArticles?.description).toBe('My tech articles'); 202 expect(techArticles?.author.id).toBe(curator1.value); 203 expect(techArticles?.uri).toBe( 204 'at://did:plc:curator1/network.cosmik.collection/collection1', 205 ); 206 207 const readingList = response.collections.find( 208 (c) => c.name === 'Reading List', 209 ); 210 expect(readingList).toBeDefined(); 211 expect(readingList?.description).toBe('Articles to read'); 212 expect(readingList?.author.id).toBe(curator2.value); 213 214 const favorites = response.collections.find( 215 (c) => c.name === 'Favorites', 216 ); 217 expect(favorites).toBeDefined(); 218 expect(favorites?.description).toBeUndefined(); 219 expect(favorites?.author.id).toBe(curator3.value); 220 }); 221 222 it('should return empty array when no collections contain cards with the specified URL', async () => { 223 const testUrl = 'https://example.com/nonexistent-article'; 224 225 const query = { 226 url: testUrl, 227 }; 228 229 const result = await useCase.execute(query); 230 231 expect(result.isOk()).toBe(true); 232 const response = result.unwrap(); 233 234 expect(response.collections).toHaveLength(0); 235 expect(response.pagination.totalCount).toBe(0); 236 }); 237 238 it('should not return collections that contain cards with different URLs', async () => { 239 const testUrl1 = 'https://example.com/article1'; 240 const testUrl2 = 'https://example.com/article2'; 241 const url1 = URL.create(testUrl1).unwrap(); 242 const url2 = URL.create(testUrl2).unwrap(); 243 244 // Create cards with different URLs 245 const card1 = new CardBuilder() 246 .withCuratorId(curator1.value) 247 .withType(CardTypeEnum.URL) 248 .withUrl(url1) 249 .build(); 250 251 const card2 = new CardBuilder() 252 .withCuratorId(curator2.value) 253 .withType(CardTypeEnum.URL) 254 .withUrl(url2) 255 .build(); 256 257 if (card1 instanceof Error || card2 instanceof Error) { 258 throw new Error('Failed to create cards'); 259 } 260 261 card1.addToLibrary(curator1); 262 card2.addToLibrary(curator2); 263 264 await cardRepository.save(card1); 265 await cardRepository.save(card2); 266 267 // Create collections 268 const collection1 = new CollectionBuilder() 269 .withAuthorId(curator1.value) 270 .withName('Collection 1') 271 .build(); 272 273 const collection2 = new CollectionBuilder() 274 .withAuthorId(curator2.value) 275 .withName('Collection 2') 276 .build(); 277 278 if (collection1 instanceof Error || collection2 instanceof Error) { 279 throw new Error('Failed to create collections'); 280 } 281 282 collection1.addCard(card1.cardId, curator1); 283 collection2.addCard(card2.cardId, curator2); 284 285 await collectionRepository.save(collection1); 286 await collectionRepository.save(collection2); 287 288 // Query for testUrl1 289 const query = { 290 url: testUrl1, 291 }; 292 293 const result = await useCase.execute(query); 294 295 expect(result.isOk()).toBe(true); 296 const response = result.unwrap(); 297 298 expect(response.collections).toHaveLength(1); 299 expect(response.collections[0]!.name).toBe('Collection 1'); 300 expect(response.collections[0]!.author.id).toBe(curator1.value); 301 }); 302 303 it('should return multiple collections from the same user if they contain the URL', async () => { 304 const testUrl = 'https://example.com/popular-article'; 305 const url = URL.create(testUrl).unwrap(); 306 307 // Create URL card 308 const card = new CardBuilder() 309 .withCuratorId(curator1.value) 310 .withType(CardTypeEnum.URL) 311 .withUrl(url) 312 .build(); 313 314 if (card instanceof Error) { 315 throw new Error('Failed to create card'); 316 } 317 318 card.addToLibrary(curator1); 319 await cardRepository.save(card); 320 321 // Create multiple collections for the same user 322 const collection1 = new CollectionBuilder() 323 .withAuthorId(curator1.value) 324 .withName('Tech') 325 .build(); 326 327 const collection2 = new CollectionBuilder() 328 .withAuthorId(curator1.value) 329 .withName('Favorites') 330 .build(); 331 332 const collection3 = new CollectionBuilder() 333 .withAuthorId(curator1.value) 334 .withName('To Read') 335 .build(); 336 337 if ( 338 collection1 instanceof Error || 339 collection2 instanceof Error || 340 collection3 instanceof Error 341 ) { 342 throw new Error('Failed to create collections'); 343 } 344 345 // Add the same card to all collections 346 collection1.addCard(card.cardId, curator1); 347 collection2.addCard(card.cardId, curator1); 348 collection3.addCard(card.cardId, curator1); 349 350 await collectionRepository.save(collection1); 351 await collectionRepository.save(collection2); 352 await collectionRepository.save(collection3); 353 354 // Execute the use case 355 const query = { 356 url: testUrl, 357 }; 358 359 const result = await useCase.execute(query); 360 361 expect(result.isOk()).toBe(true); 362 const response = result.unwrap(); 363 364 expect(response.collections).toHaveLength(3); 365 366 const collectionNames = response.collections.map((c) => c.name); 367 expect(collectionNames).toContain('Tech'); 368 expect(collectionNames).toContain('Favorites'); 369 expect(collectionNames).toContain('To Read'); 370 371 // All should have the same author 372 response.collections.forEach((collection) => { 373 expect(collection.author.id).toBe(curator1.value); 374 }); 375 }); 376 377 it('should handle collections without published record IDs', async () => { 378 const testUrl = 'https://example.com/unpublished-article'; 379 const url = URL.create(testUrl).unwrap(); 380 381 // Create URL card 382 const card = new CardBuilder() 383 .withCuratorId(curator1.value) 384 .withType(CardTypeEnum.URL) 385 .withUrl(url) 386 .build(); 387 388 if (card instanceof Error) { 389 throw new Error('Failed to create card'); 390 } 391 392 card.addToLibrary(curator1); 393 await cardRepository.save(card); 394 395 // Create collection without publishing it 396 const collection = new CollectionBuilder() 397 .withAuthorId(curator1.value) 398 .withName('Unpublished Collection') 399 .build(); 400 401 if (collection instanceof Error) { 402 throw new Error('Failed to create collection'); 403 } 404 405 collection.addCard(card.cardId, curator1); 406 await collectionRepository.save(collection); 407 408 // Execute the use case 409 const query = { 410 url: testUrl, 411 }; 412 413 const result = await useCase.execute(query); 414 415 expect(result.isOk()).toBe(true); 416 const response = result.unwrap(); 417 418 expect(response.collections).toHaveLength(1); 419 expect(response.collections[0]!.name).toBe('Unpublished Collection'); 420 expect(response.collections[0]!.uri).toBeUndefined(); 421 }); 422 }); 423 424 describe('Pagination', () => { 425 it('should paginate results correctly', async () => { 426 const testUrl = 'https://example.com/popular-article'; 427 const url = URL.create(testUrl).unwrap(); 428 429 // Create 5 cards with the same URL from different users 430 const cards = []; 431 const curators = []; 432 const collections = []; 433 434 for (let i = 1; i <= 5; i++) { 435 const curator = CuratorId.create(`did:plc:curator${i}`).unwrap(); 436 curators.push(curator); 437 438 // Add profile for this curator 439 profileService.addProfile({ 440 id: curator.value, 441 name: `Curator ${i}`, 442 handle: `curator${i}`, 443 avatarUrl: `https://example.com/avatar${i}.jpg`, 444 }); 445 446 const card = new CardBuilder() 447 .withCuratorId(curator.value) 448 .withType(CardTypeEnum.URL) 449 .withUrl(url) 450 .build(); 451 452 if (card instanceof Error) { 453 throw new Error(`Failed to create card ${i}`); 454 } 455 456 card.addToLibrary(curator); 457 cards.push(card); 458 await cardRepository.save(card); 459 460 // Create collection for each user 461 const collection = new CollectionBuilder() 462 .withAuthorId(curator.value) 463 .withName(`Collection ${i}`) 464 .build(); 465 466 if (collection instanceof Error) { 467 throw new Error(`Failed to create collection ${i}`); 468 } 469 470 collection.addCard(card.cardId, curator); 471 collections.push(collection); 472 await collectionRepository.save(collection); 473 } 474 475 // Test first page with limit 2 476 const query1 = { 477 url: testUrl, 478 page: 1, 479 limit: 2, 480 }; 481 482 const result1 = await useCase.execute(query1); 483 expect(result1.isOk()).toBe(true); 484 const response1 = result1.unwrap(); 485 486 expect(response1.collections).toHaveLength(2); 487 expect(response1.pagination.currentPage).toBe(1); 488 expect(response1.pagination.totalCount).toBe(5); 489 expect(response1.pagination.totalPages).toBe(3); 490 expect(response1.pagination.hasMore).toBe(true); 491 492 // Test second page 493 const query2 = { 494 url: testUrl, 495 page: 2, 496 limit: 2, 497 }; 498 499 const result2 = await useCase.execute(query2); 500 expect(result2.isOk()).toBe(true); 501 const response2 = result2.unwrap(); 502 503 expect(response2.collections).toHaveLength(2); 504 expect(response2.pagination.currentPage).toBe(2); 505 expect(response2.pagination.hasMore).toBe(true); 506 507 // Test last page 508 const query3 = { 509 url: testUrl, 510 page: 3, 511 limit: 2, 512 }; 513 514 const result3 = await useCase.execute(query3); 515 expect(result3.isOk()).toBe(true); 516 const response3 = result3.unwrap(); 517 518 expect(response3.collections).toHaveLength(1); 519 expect(response3.pagination.currentPage).toBe(3); 520 expect(response3.pagination.hasMore).toBe(false); 521 }); 522 523 it('should respect limit cap of 100', async () => { 524 const query = { 525 url: 'https://example.com/test', 526 limit: 200, // Should be capped at 100 527 }; 528 529 const result = await useCase.execute(query); 530 expect(result.isOk()).toBe(true); 531 const response = result.unwrap(); 532 533 expect(response.pagination.limit).toBe(100); 534 }); 535 536 it('should use default pagination values', async () => { 537 const testUrl = 'https://example.com/test-article'; 538 539 const query = { 540 url: testUrl, 541 }; 542 543 const result = await useCase.execute(query); 544 expect(result.isOk()).toBe(true); 545 const response = result.unwrap(); 546 547 expect(response.pagination.currentPage).toBe(1); 548 expect(response.pagination.limit).toBe(20); 549 }); 550 }); 551 552 describe('Sorting', () => { 553 it('should use default sorting parameters (NAME, ASC)', async () => { 554 const testUrl = 'https://example.com/test-article'; 555 556 const query = { 557 url: testUrl, 558 }; 559 560 const result = await useCase.execute(query); 561 expect(result.isOk()).toBe(true); 562 const response = result.unwrap(); 563 564 expect(response.sorting.sortBy).toBe(CollectionSortField.NAME); 565 expect(response.sorting.sortOrder).toBe(SortOrder.ASC); 566 }); 567 568 it('should use provided sorting parameters', async () => { 569 const testUrl = 'https://example.com/test-article'; 570 571 const query = { 572 url: testUrl, 573 sortBy: CollectionSortField.CREATED_AT, 574 sortOrder: SortOrder.DESC, 575 }; 576 577 const result = await useCase.execute(query); 578 expect(result.isOk()).toBe(true); 579 const response = result.unwrap(); 580 581 expect(response.sorting.sortBy).toBe(CollectionSortField.CREATED_AT); 582 expect(response.sorting.sortOrder).toBe(SortOrder.DESC); 583 }); 584 585 it('should sort collections by name in ascending order by default', async () => { 586 const testUrl = 'https://example.com/article'; 587 const url = URL.create(testUrl).unwrap(); 588 589 // Create URL cards 590 const card1 = new CardBuilder() 591 .withCuratorId(curator1.value) 592 .withType(CardTypeEnum.URL) 593 .withUrl(url) 594 .build(); 595 596 const card2 = new CardBuilder() 597 .withCuratorId(curator2.value) 598 .withType(CardTypeEnum.URL) 599 .withUrl(url) 600 .build(); 601 602 const card3 = new CardBuilder() 603 .withCuratorId(curator3.value) 604 .withType(CardTypeEnum.URL) 605 .withUrl(url) 606 .build(); 607 608 if ( 609 card1 instanceof Error || 610 card2 instanceof Error || 611 card3 instanceof Error 612 ) { 613 throw new Error('Failed to create cards'); 614 } 615 616 card1.addToLibrary(curator1); 617 card2.addToLibrary(curator2); 618 card3.addToLibrary(curator3); 619 620 await cardRepository.save(card1); 621 await cardRepository.save(card2); 622 await cardRepository.save(card3); 623 624 // Create collections with names that should be sorted 625 const collectionZ = new CollectionBuilder() 626 .withAuthorId(curator1.value) 627 .withName('Zebra Collection') 628 .build(); 629 630 const collectionA = new CollectionBuilder() 631 .withAuthorId(curator2.value) 632 .withName('Apple Collection') 633 .build(); 634 635 const collectionM = new CollectionBuilder() 636 .withAuthorId(curator3.value) 637 .withName('Mango Collection') 638 .build(); 639 640 if ( 641 collectionZ instanceof Error || 642 collectionA instanceof Error || 643 collectionM instanceof Error 644 ) { 645 throw new Error('Failed to create collections'); 646 } 647 648 collectionZ.addCard(card1.cardId, curator1); 649 collectionA.addCard(card2.cardId, curator2); 650 collectionM.addCard(card3.cardId, curator3); 651 652 await collectionRepository.save(collectionZ); 653 await collectionRepository.save(collectionA); 654 await collectionRepository.save(collectionM); 655 656 const query = { 657 url: testUrl, 658 }; 659 660 const result = await useCase.execute(query); 661 expect(result.isOk()).toBe(true); 662 const response = result.unwrap(); 663 664 expect(response.collections).toHaveLength(3); 665 expect(response.collections[0]!.name).toBe('Apple Collection'); 666 expect(response.collections[1]!.name).toBe('Mango Collection'); 667 expect(response.collections[2]!.name).toBe('Zebra Collection'); 668 }); 669 670 it('should sort collections by name in descending order', async () => { 671 const testUrl = 'https://example.com/article'; 672 const url = URL.create(testUrl).unwrap(); 673 674 // Create URL cards 675 const card1 = new CardBuilder() 676 .withCuratorId(curator1.value) 677 .withType(CardTypeEnum.URL) 678 .withUrl(url) 679 .build(); 680 681 const card2 = new CardBuilder() 682 .withCuratorId(curator2.value) 683 .withType(CardTypeEnum.URL) 684 .withUrl(url) 685 .build(); 686 687 const card3 = new CardBuilder() 688 .withCuratorId(curator3.value) 689 .withType(CardTypeEnum.URL) 690 .withUrl(url) 691 .build(); 692 693 if ( 694 card1 instanceof Error || 695 card2 instanceof Error || 696 card3 instanceof Error 697 ) { 698 throw new Error('Failed to create cards'); 699 } 700 701 card1.addToLibrary(curator1); 702 card2.addToLibrary(curator2); 703 card3.addToLibrary(curator3); 704 705 await cardRepository.save(card1); 706 await cardRepository.save(card2); 707 await cardRepository.save(card3); 708 709 // Create collections with names that should be sorted 710 const collectionZ = new CollectionBuilder() 711 .withAuthorId(curator1.value) 712 .withName('Zebra Collection') 713 .build(); 714 715 const collectionA = new CollectionBuilder() 716 .withAuthorId(curator2.value) 717 .withName('Apple Collection') 718 .build(); 719 720 const collectionM = new CollectionBuilder() 721 .withAuthorId(curator3.value) 722 .withName('Mango Collection') 723 .build(); 724 725 if ( 726 collectionZ instanceof Error || 727 collectionA instanceof Error || 728 collectionM instanceof Error 729 ) { 730 throw new Error('Failed to create collections'); 731 } 732 733 collectionZ.addCard(card1.cardId, curator1); 734 collectionA.addCard(card2.cardId, curator2); 735 collectionM.addCard(card3.cardId, curator3); 736 737 await collectionRepository.save(collectionZ); 738 await collectionRepository.save(collectionA); 739 await collectionRepository.save(collectionM); 740 741 const query = { 742 url: testUrl, 743 sortBy: CollectionSortField.NAME, 744 sortOrder: SortOrder.DESC, 745 }; 746 747 const result = await useCase.execute(query); 748 expect(result.isOk()).toBe(true); 749 const response = result.unwrap(); 750 751 expect(response.collections).toHaveLength(3); 752 expect(response.collections[0]!.name).toBe('Zebra Collection'); 753 expect(response.collections[1]!.name).toBe('Mango Collection'); 754 expect(response.collections[2]!.name).toBe('Apple Collection'); 755 }); 756 }); 757 758 describe('Validation', () => { 759 it('should fail with invalid URL', async () => { 760 const query = { 761 url: 'not-a-valid-url', 762 }; 763 764 const result = await useCase.execute(query); 765 766 expect(result.isErr()).toBe(true); 767 if (result.isErr()) { 768 expect(result.error.message).toContain('Invalid URL'); 769 } 770 }); 771 }); 772 773 describe('Error handling', () => { 774 it('should handle repository errors gracefully', async () => { 775 // Create a mock repository that throws an error 776 const errorCollectionQueryRepository = { 777 findByCreator: jest.fn(), 778 getCollectionsContainingCardForUser: jest.fn(), 779 getCollectionsWithUrl: jest 780 .fn() 781 .mockRejectedValue(new Error('Database error')), 782 }; 783 784 const errorUseCase = new GetCollectionsForUrlUseCase( 785 errorCollectionQueryRepository, 786 profileService, 787 collectionRepository, 788 ); 789 790 const query = { 791 url: 'https://example.com/test-url', 792 }; 793 794 const result = await errorUseCase.execute(query); 795 796 expect(result.isErr()).toBe(true); 797 if (result.isErr()) { 798 expect(result.error.message).toContain('Database error'); 799 } 800 }); 801 }); 802});