ATlast — you'll never need to find your favorites on another platform again. Find your favs in the ATmosphere.
atproto
16
fork

Configure Feed

Select the types of activity you want to include in your feed.

test(api): add deterministic error scenarios to search, follow

byarielm.fyi 9e341e4f 0cbcef3f

verified
+743 -2
+366 -1
packages/api/__tests__/routes/follow.test.ts
··· 4 4 * Tests batch follow operations and follow status checking. 5 5 */ 6 6 7 - import { describe, it, expect, beforeAll, afterAll } from 'vitest'; 7 + import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; 8 8 import { 9 9 request, 10 10 authRequest, ··· 14 14 import { 15 15 createTestSession, 16 16 cleanupAllTestSessions, 17 + createMockAgent, 18 + createTimeoutAgent, 19 + createRateLimitAgent, 20 + createServiceUnavailableAgent, 21 + createFollowAgent, 17 22 } from '../fixtures'; 23 + import { SessionService } from '../../src/services/SessionService'; 18 24 19 25 describe('Follow API', () => { 20 26 let validSession: string; ··· 710 716 711 717 // Status check should return results for all DIDs that could be checked 712 718 expect(typeof body.data.followStatus).toBe('object'); 719 + } 720 + }); 721 + }); 722 + }); 723 + 724 + // ========================================================================== 725 + // Mocked AT Protocol Agent Tests 726 + // Deterministic tests using mock agents to verify specific error scenarios 727 + // ========================================================================== 728 + 729 + describe('Mocked Error Scenarios', () => { 730 + describe('Batch Follow - Network Timeout Handling', () => { 731 + it('reports individual follow failures when AT Protocol times out', async () => { 732 + const mockAgent = createTimeoutAgent(); 733 + 734 + const originalMethod = SessionService.getAgentForSession; 735 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 736 + agent: mockAgent, 737 + did: 'did:plc:test-standard-user-001', 738 + client: {}, 739 + }); 740 + 741 + try { 742 + const res = await requestWithSession( 743 + '/api/follow/batch-follow-users', 744 + validSession, 745 + { 746 + method: 'POST', 747 + body: JSON.stringify({ 748 + dids: ['did:plc:user1', 'did:plc:user2'], 749 + }), 750 + }, 751 + ); 752 + 753 + expect(res.status).toBe(200); 754 + const body = await parseResponse(res); 755 + expect(body.success).toBe(true); 756 + expect(body.data.total).toBe(2); 757 + 758 + // listRecords (for checking already-following) throws timeout, 759 + // but FollowService.checkFollowStatus catches and returns all false. 760 + // Then createRecord also throws timeout, so all follows fail. 761 + expect(body.data.failed).toBe(2); 762 + expect(body.data.succeeded).toBe(0); 763 + 764 + for (const result of body.data.results) { 765 + expect(result.success).toBe(false); 766 + expect(result.error).toBeDefined(); 767 + expect(result.error).toContain('timed out'); 768 + } 769 + } finally { 770 + SessionService.getAgentForSession = originalMethod; 771 + } 772 + }); 773 + }); 774 + 775 + describe('Batch Follow - Rate Limit Handling', () => { 776 + it('reports rate limit errors in individual follow results', async () => { 777 + const mockAgent = createRateLimitAgent(); 778 + 779 + const originalMethod = SessionService.getAgentForSession; 780 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 781 + agent: mockAgent, 782 + did: 'did:plc:test-standard-user-001', 783 + client: {}, 784 + }); 785 + 786 + try { 787 + const res = await requestWithSession( 788 + '/api/follow/batch-follow-users', 789 + validSession, 790 + { 791 + method: 'POST', 792 + body: JSON.stringify({ 793 + dids: ['did:plc:user1'], 794 + }), 795 + }, 796 + ); 797 + 798 + expect(res.status).toBe(200); 799 + const body = await parseResponse(res); 800 + expect(body.success).toBe(true); 801 + expect(body.data.failed).toBe(1); 802 + 803 + const result = body.data.results[0]; 804 + expect(result.success).toBe(false); 805 + expect(result.error).toContain('rate limit'); 806 + } finally { 807 + SessionService.getAgentForSession = originalMethod; 808 + } 809 + }); 810 + }); 811 + 812 + describe('Batch Follow - Service Unavailable Handling', () => { 813 + it('reports service unavailable errors in follow results', async () => { 814 + const mockAgent = createServiceUnavailableAgent(); 815 + 816 + const originalMethod = SessionService.getAgentForSession; 817 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 818 + agent: mockAgent, 819 + did: 'did:plc:test-standard-user-001', 820 + client: {}, 821 + }); 822 + 823 + try { 824 + const res = await requestWithSession( 825 + '/api/follow/batch-follow-users', 826 + validSession, 827 + { 828 + method: 'POST', 829 + body: JSON.stringify({ 830 + dids: ['did:plc:user1'], 831 + }), 832 + }, 833 + ); 834 + 835 + expect(res.status).toBe(200); 836 + const body = await parseResponse(res); 837 + expect(body.success).toBe(true); 838 + expect(body.data.failed).toBe(1); 839 + 840 + const result = body.data.results[0]; 841 + expect(result.success).toBe(false); 842 + expect(result.error).toContain('Service Unavailable'); 843 + } finally { 844 + SessionService.getAgentForSession = originalMethod; 845 + } 846 + }); 847 + }); 848 + 849 + describe('Batch Follow - Already Following Detection', () => { 850 + it('detects already-followed users and skips them', async () => { 851 + const mockAgent = createFollowAgent({ 852 + alreadyFollowing: ['did:plc:already1', 'did:plc:already2'], 853 + }); 854 + 855 + const originalMethod = SessionService.getAgentForSession; 856 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 857 + agent: mockAgent, 858 + did: 'did:plc:test-standard-user-001', 859 + client: {}, 860 + }); 861 + 862 + try { 863 + const res = await requestWithSession( 864 + '/api/follow/batch-follow-users', 865 + validSession, 866 + { 867 + method: 'POST', 868 + body: JSON.stringify({ 869 + dids: ['did:plc:already1', 'did:plc:newuser', 'did:plc:already2'], 870 + }), 871 + }, 872 + ); 873 + 874 + expect(res.status).toBe(200); 875 + const body = await parseResponse(res); 876 + expect(body.success).toBe(true); 877 + expect(body.data.total).toBe(3); 878 + expect(body.data.alreadyFollowing).toBe(2); 879 + expect(body.data.succeeded).toBe(3); // already-following counts as success 880 + 881 + // Verify already-following results 882 + const already1 = body.data.results.find( 883 + (r: { did: string }) => r.did === 'did:plc:already1', 884 + ); 885 + expect(already1.success).toBe(true); 886 + expect(already1.alreadyFollowing).toBe(true); 887 + 888 + // Verify new follow result 889 + const newUser = body.data.results.find( 890 + (r: { did: string }) => r.did === 'did:plc:newuser', 891 + ); 892 + expect(newUser.success).toBe(true); 893 + expect(newUser.alreadyFollowing).toBe(false); 894 + } finally { 895 + SessionService.getAgentForSession = originalMethod; 896 + } 897 + }); 898 + }); 899 + 900 + describe('Batch Follow - Partial Failures', () => { 901 + it('continues following remaining users when some fail', async () => { 902 + const mockAgent = createFollowAgent({ 903 + failDids: ['did:plc:fail1'], 904 + }); 905 + 906 + const originalMethod = SessionService.getAgentForSession; 907 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 908 + agent: mockAgent, 909 + did: 'did:plc:test-standard-user-001', 910 + client: {}, 911 + }); 912 + 913 + try { 914 + const res = await requestWithSession( 915 + '/api/follow/batch-follow-users', 916 + validSession, 917 + { 918 + method: 'POST', 919 + body: JSON.stringify({ 920 + dids: ['did:plc:ok1', 'did:plc:fail1', 'did:plc:ok2'], 921 + }), 922 + }, 923 + ); 924 + 925 + expect(res.status).toBe(200); 926 + const body = await parseResponse(res); 927 + expect(body.success).toBe(true); 928 + expect(body.data.total).toBe(3); 929 + expect(body.data.succeeded).toBe(2); 930 + expect(body.data.failed).toBe(1); 931 + 932 + // Verify the failed follow 933 + const failedResult = body.data.results.find( 934 + (r: { did: string }) => r.did === 'did:plc:fail1', 935 + ); 936 + expect(failedResult.success).toBe(false); 937 + expect(failedResult.error).toBeDefined(); 938 + 939 + // Verify the successful follows 940 + const ok1 = body.data.results.find( 941 + (r: { did: string }) => r.did === 'did:plc:ok1', 942 + ); 943 + expect(ok1.success).toBe(true); 944 + expect(ok1.error).toBeNull(); 945 + 946 + const ok2 = body.data.results.find( 947 + (r: { did: string }) => r.did === 'did:plc:ok2', 948 + ); 949 + expect(ok2.success).toBe(true); 950 + expect(ok2.error).toBeNull(); 951 + } finally { 952 + SessionService.getAgentForSession = originalMethod; 953 + } 954 + }); 955 + }); 956 + 957 + describe('Batch Follow - Concurrent Operations', () => { 958 + it('processes follows in chunks with controlled concurrency', async () => { 959 + const callOrder: string[] = []; 960 + const mockAgent = createMockAgent({ 961 + listRecords: async () => ({ 962 + data: { records: [], cursor: undefined }, 963 + }), 964 + createRecord: async (params) => { 965 + const targetDid = (params.record as { subject?: string }).subject ?? ''; 966 + callOrder.push(targetDid); 967 + // Simulate some async work 968 + await new Promise((resolve) => setTimeout(resolve, 10)); 969 + return { 970 + uri: `at://did:plc:mock/app.bsky.graph.follow/${Date.now()}`, 971 + cid: 'mock-cid', 972 + }; 973 + }, 974 + }); 975 + 976 + const originalMethod = SessionService.getAgentForSession; 977 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 978 + agent: mockAgent, 979 + did: 'did:plc:test-standard-user-001', 980 + client: {}, 981 + }); 982 + 983 + try { 984 + const dids = Array.from({ length: 8 }, (_, i) => `did:plc:user${i}`); 985 + 986 + const res = await requestWithSession( 987 + '/api/follow/batch-follow-users', 988 + validSession, 989 + { 990 + method: 'POST', 991 + body: JSON.stringify({ dids }), 992 + }, 993 + ); 994 + 995 + expect(res.status).toBe(200); 996 + const body = await parseResponse(res); 997 + expect(body.success).toBe(true); 998 + expect(body.data.total).toBe(8); 999 + expect(body.data.succeeded).toBe(8); 1000 + 1001 + // All DIDs should have been processed 1002 + expect(callOrder).toHaveLength(8); 1003 + } finally { 1004 + SessionService.getAgentForSession = originalMethod; 1005 + } 1006 + }); 1007 + }); 1008 + 1009 + describe('Check Status - Mocked Scenarios', () => { 1010 + it('returns follow status from AT Protocol records', async () => { 1011 + const mockAgent = createFollowAgent({ 1012 + alreadyFollowing: ['did:plc:followed1', 'did:plc:followed2'], 1013 + }); 1014 + 1015 + const originalMethod = SessionService.getAgentForSession; 1016 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 1017 + agent: mockAgent, 1018 + did: 'did:plc:test-standard-user-001', 1019 + client: {}, 1020 + }); 1021 + 1022 + try { 1023 + const res = await requestWithSession( 1024 + '/api/follow/check-status', 1025 + validSession, 1026 + { 1027 + method: 'POST', 1028 + body: JSON.stringify({ 1029 + dids: ['did:plc:followed1', 'did:plc:notfollowed', 'did:plc:followed2'], 1030 + }), 1031 + }, 1032 + ); 1033 + 1034 + expect(res.status).toBe(200); 1035 + const body = await parseResponse(res); 1036 + expect(body.success).toBe(true); 1037 + expect(body.data.followStatus).toBeDefined(); 1038 + 1039 + expect(body.data.followStatus['did:plc:followed1']).toBe(true); 1040 + expect(body.data.followStatus['did:plc:notfollowed']).toBe(false); 1041 + expect(body.data.followStatus['did:plc:followed2']).toBe(true); 1042 + } finally { 1043 + SessionService.getAgentForSession = originalMethod; 1044 + } 1045 + }); 1046 + 1047 + it('returns all false when listRecords fails', async () => { 1048 + const mockAgent = createTimeoutAgent(); 1049 + 1050 + const originalMethod = SessionService.getAgentForSession; 1051 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 1052 + agent: mockAgent, 1053 + did: 'did:plc:test-standard-user-001', 1054 + client: {}, 1055 + }); 1056 + 1057 + try { 1058 + const res = await requestWithSession( 1059 + '/api/follow/check-status', 1060 + validSession, 1061 + { 1062 + method: 'POST', 1063 + body: JSON.stringify({ 1064 + dids: ['did:plc:user1', 'did:plc:user2'], 1065 + }), 1066 + }, 1067 + ); 1068 + 1069 + expect(res.status).toBe(200); 1070 + const body = await parseResponse(res); 1071 + expect(body.success).toBe(true); 1072 + 1073 + // FollowService.checkFollowStatus catches errors and returns all false 1074 + expect(body.data.followStatus['did:plc:user1']).toBe(false); 1075 + expect(body.data.followStatus['did:plc:user2']).toBe(false); 1076 + } finally { 1077 + SessionService.getAgentForSession = originalMethod; 713 1078 } 714 1079 }); 715 1080 });
+377 -1
packages/api/__tests__/routes/search.test.ts
··· 4 4 * Tests batch actor search functionality on AT Protocol. 5 5 */ 6 6 7 - import { describe, it, expect, beforeAll, afterAll } from 'vitest'; 7 + import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'; 8 8 import { 9 9 request, 10 10 authRequest, ··· 14 14 import { 15 15 createTestSession, 16 16 cleanupAllTestSessions, 17 + createTimeoutAgent, 18 + createRateLimitAgent, 19 + createServiceUnavailableAgent, 20 + createPartialFailureSearchAgent, 21 + createSuccessfulSearchAgent, 22 + createMalformedResponseAgent, 17 23 } from '../fixtures'; 24 + import { SessionService } from '../../src/services/SessionService'; 18 25 19 26 describe('Search API', () => { 20 27 let validSession: string; ··· 438 445 // Search endpoint might not require DB, so could still return 200 439 446 // If DB is needed and fails, should get 500 440 447 expect([200, 401, 500]).toContain(res.status); 448 + }); 449 + }); 450 + }); 451 + 452 + // ========================================================================== 453 + // Mocked AT Protocol Agent Tests 454 + // Deterministic tests using mock agents to verify specific error scenarios 455 + // ========================================================================== 456 + 457 + describe('Mocked Error Scenarios', () => { 458 + /** 459 + * These tests mock SessionService.getAgentForSession to inject 460 + * controlled AT Protocol agent behavior. This enables deterministic 461 + * testing of error handling without real API calls. 462 + */ 463 + 464 + describe('Network Timeout Handling', () => { 465 + it('returns results with error messages when AT Protocol times out', async () => { 466 + const mockAgent = createTimeoutAgent(); 467 + 468 + const originalMethod = SessionService.getAgentForSession; 469 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 470 + agent: mockAgent, 471 + did: 'did:plc:test-standard-user-001', 472 + client: {}, 473 + }); 474 + 475 + try { 476 + const res = await requestWithSession( 477 + '/api/search/batch-search-actors', 478 + validSession, 479 + { 480 + method: 'POST', 481 + body: JSON.stringify({ 482 + usernames: ['user1', 'user2'], 483 + }), 484 + }, 485 + ); 486 + 487 + expect(res.status).toBe(200); 488 + const body = await parseResponse(res); 489 + expect(body.success).toBe(true); 490 + expect(body.data.results).toHaveLength(2); 491 + 492 + // Each search should have captured the timeout error 493 + for (const result of body.data.results) { 494 + expect(result.actors).toHaveLength(0); 495 + expect(result.error).toBeDefined(); 496 + expect(result.error).toContain('timed out'); 497 + } 498 + } finally { 499 + SessionService.getAgentForSession = originalMethod; 500 + } 501 + }); 502 + }); 503 + 504 + describe('AT Protocol Rate Limit (429) Handling', () => { 505 + it('captures rate limit errors per-username in results', async () => { 506 + const mockAgent = createRateLimitAgent(); 507 + 508 + const originalMethod = SessionService.getAgentForSession; 509 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 510 + agent: mockAgent, 511 + did: 'did:plc:test-standard-user-001', 512 + client: {}, 513 + }); 514 + 515 + try { 516 + const res = await requestWithSession( 517 + '/api/search/batch-search-actors', 518 + validSession, 519 + { 520 + method: 'POST', 521 + body: JSON.stringify({ 522 + usernames: ['testuser'], 523 + }), 524 + }, 525 + ); 526 + 527 + expect(res.status).toBe(200); 528 + const body = await parseResponse(res); 529 + expect(body.success).toBe(true); 530 + 531 + const result = body.data.results[0]; 532 + expect(result.actors).toHaveLength(0); 533 + expect(result.error).toBeDefined(); 534 + expect(result.error).toContain('Rate Limit'); 535 + } finally { 536 + SessionService.getAgentForSession = originalMethod; 537 + } 538 + }); 539 + }); 540 + 541 + describe('Service Unavailable (503) Handling', () => { 542 + it('captures service unavailable errors in results', async () => { 543 + const mockAgent = createServiceUnavailableAgent(); 544 + 545 + const originalMethod = SessionService.getAgentForSession; 546 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 547 + agent: mockAgent, 548 + did: 'did:plc:test-standard-user-001', 549 + client: {}, 550 + }); 551 + 552 + try { 553 + const res = await requestWithSession( 554 + '/api/search/batch-search-actors', 555 + validSession, 556 + { 557 + method: 'POST', 558 + body: JSON.stringify({ 559 + usernames: ['testuser'], 560 + }), 561 + }, 562 + ); 563 + 564 + expect(res.status).toBe(200); 565 + const body = await parseResponse(res); 566 + expect(body.success).toBe(true); 567 + 568 + const result = body.data.results[0]; 569 + expect(result.actors).toHaveLength(0); 570 + expect(result.error).toContain('Service Unavailable'); 571 + } finally { 572 + SessionService.getAgentForSession = originalMethod; 573 + } 574 + }); 575 + }); 576 + 577 + describe('Partial Batch Failure Handling', () => { 578 + it('returns mixed success/error results when some searches fail', async () => { 579 + const mockAgent = createPartialFailureSearchAgent(); 580 + 581 + const originalMethod = SessionService.getAgentForSession; 582 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 583 + agent: mockAgent, 584 + did: 'did:plc:test-standard-user-001', 585 + client: {}, 586 + }); 587 + 588 + try { 589 + const res = await requestWithSession( 590 + '/api/search/batch-search-actors', 591 + validSession, 592 + { 593 + method: 'POST', 594 + body: JSON.stringify({ 595 + usernames: ['gooduser1', 'baduser', 'gooduser2'], 596 + }), 597 + }, 598 + ); 599 + 600 + expect(res.status).toBe(200); 601 + const body = await parseResponse(res); 602 + expect(body.success).toBe(true); 603 + expect(body.data.results).toHaveLength(3); 604 + 605 + // gooduser1 should succeed 606 + const result1 = body.data.results[0]; 607 + expect(result1.username).toBe('gooduser1'); 608 + expect(result1.actors.length).toBeGreaterThan(0); 609 + expect(result1.error).toBeNull(); 610 + 611 + // baduser should fail with error 612 + const result2 = body.data.results[1]; 613 + expect(result2.username).toBe('baduser'); 614 + expect(result2.actors).toHaveLength(0); 615 + expect(result2.error).toBeDefined(); 616 + expect(result2.error).toContain('baduser'); 617 + 618 + // gooduser2 should succeed 619 + const result3 = body.data.results[2]; 620 + expect(result3.username).toBe('gooduser2'); 621 + expect(result3.actors.length).toBeGreaterThan(0); 622 + expect(result3.error).toBeNull(); 623 + } finally { 624 + SessionService.getAgentForSession = originalMethod; 625 + } 626 + }); 627 + }); 628 + 629 + describe('Successful Search with Actor Ranking', () => { 630 + it('returns ranked actors with enriched profile data', async () => { 631 + const mockAgent = createSuccessfulSearchAgent({ 632 + testuser: [ 633 + { did: 'did:plc:exact', handle: 'testuser.bsky.social', displayName: 'Test User' }, 634 + { did: 'did:plc:partial', handle: 'testuser123.bsky.social', displayName: 'Other' }, 635 + { did: 'did:plc:unrelated', handle: 'someone.bsky.social', displayName: 'Unrelated' }, 636 + ], 637 + }); 638 + 639 + const originalMethod = SessionService.getAgentForSession; 640 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 641 + agent: mockAgent, 642 + did: 'did:plc:test-standard-user-001', 643 + client: {}, 644 + }); 645 + 646 + try { 647 + const res = await requestWithSession( 648 + '/api/search/batch-search-actors', 649 + validSession, 650 + { 651 + method: 'POST', 652 + body: JSON.stringify({ 653 + usernames: ['testuser'], 654 + }), 655 + }, 656 + ); 657 + 658 + expect(res.status).toBe(200); 659 + const body = await parseResponse(res); 660 + expect(body.success).toBe(true); 661 + 662 + const result = body.data.results[0]; 663 + expect(result.username).toBe('testuser'); 664 + expect(result.error).toBeNull(); 665 + 666 + // Should have matched actors (unrelated one filtered out by score > 0) 667 + expect(result.actors.length).toBeGreaterThanOrEqual(1); 668 + 669 + // Exact match should be first (highest score) 670 + const topActor = result.actors[0]; 671 + expect(topActor.did).toBe('did:plc:exact'); 672 + expect(topActor.matchScore).toBe(100); 673 + 674 + // Actors should be enriched with profile data 675 + expect(topActor.postCount).toBeDefined(); 676 + expect(topActor.followerCount).toBeDefined(); 677 + expect(topActor.followStatus).toBeDefined(); 678 + } finally { 679 + SessionService.getAgentForSession = originalMethod; 680 + } 681 + }); 682 + }); 683 + 684 + describe('Malformed API Response Handling', () => { 685 + it('handles actors with missing/empty fields without crashing', async () => { 686 + const mockAgent = createMalformedResponseAgent(); 687 + 688 + const originalMethod = SessionService.getAgentForSession; 689 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 690 + agent: mockAgent, 691 + did: 'did:plc:test-standard-user-001', 692 + client: {}, 693 + }); 694 + 695 + try { 696 + const res = await requestWithSession( 697 + '/api/search/batch-search-actors', 698 + validSession, 699 + { 700 + method: 'POST', 701 + body: JSON.stringify({ 702 + usernames: ['testuser'], 703 + }), 704 + }, 705 + ); 706 + 707 + // Should not crash - returns 200 with results 708 + expect(res.status).toBe(200); 709 + const body = await parseResponse(res); 710 + expect(body.success).toBe(true); 711 + expect(body.data.results).toHaveLength(1); 712 + 713 + // Actors with empty handles still pass through with low score 714 + // ('testuser'.includes('') === true -> score 30) 715 + const result = body.data.results[0]; 716 + expect(result.error).toBeNull(); 717 + expect(Array.isArray(result.actors)).toBe(true); 718 + // Verify it doesn't crash - actors come through with matchScore 30 719 + for (const actor of result.actors) { 720 + expect(actor.matchScore).toBe(30); 721 + } 722 + } finally { 723 + SessionService.getAgentForSession = originalMethod; 724 + } 725 + }); 726 + }); 727 + 728 + describe('Profile Enrichment Failure Handling', () => { 729 + it('returns actors with default counts when profile fetch fails', async () => { 730 + const mockAgent = createSuccessfulSearchAgent({ 731 + testuser: [ 732 + { did: 'did:plc:user1', handle: 'testuser.bsky.social' }, 733 + ], 734 + }); 735 + 736 + // Override getProfiles to fail 737 + (mockAgent.app.bsky.actor.getProfiles as ReturnType<typeof vi.fn>) 738 + .mockRejectedValue(new Error('Profile service down')); 739 + 740 + const originalMethod = SessionService.getAgentForSession; 741 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 742 + agent: mockAgent, 743 + did: 'did:plc:test-standard-user-001', 744 + client: {}, 745 + }); 746 + 747 + try { 748 + const res = await requestWithSession( 749 + '/api/search/batch-search-actors', 750 + validSession, 751 + { 752 + method: 'POST', 753 + body: JSON.stringify({ 754 + usernames: ['testuser'], 755 + }), 756 + }, 757 + ); 758 + 759 + expect(res.status).toBe(200); 760 + const body = await parseResponse(res); 761 + expect(body.success).toBe(true); 762 + 763 + const result = body.data.results[0]; 764 + expect(result.actors.length).toBeGreaterThanOrEqual(1); 765 + 766 + // Profile data should default to 0 when fetch fails 767 + const actor = result.actors[0]; 768 + expect(actor.postCount).toBe(0); 769 + expect(actor.followerCount).toBe(0); 770 + } finally { 771 + SessionService.getAgentForSession = originalMethod; 772 + } 773 + }); 774 + }); 775 + 776 + describe('Follow Status Check Failure During Search', () => { 777 + it('returns actors without follow status when check fails', async () => { 778 + const mockAgent = createSuccessfulSearchAgent({ 779 + testuser: [ 780 + { did: 'did:plc:user1', handle: 'testuser.bsky.social' }, 781 + ], 782 + }); 783 + 784 + // Override listRecords to fail (used by FollowService.checkFollowStatus) 785 + (mockAgent.api.com.atproto.repo.listRecords as ReturnType<typeof vi.fn>) 786 + .mockRejectedValue(new Error('Follow status check failed')); 787 + 788 + const originalMethod = SessionService.getAgentForSession; 789 + SessionService.getAgentForSession = vi.fn().mockResolvedValue({ 790 + agent: mockAgent, 791 + did: 'did:plc:test-standard-user-001', 792 + client: {}, 793 + }); 794 + 795 + try { 796 + const res = await requestWithSession( 797 + '/api/search/batch-search-actors', 798 + validSession, 799 + { 800 + method: 'POST', 801 + body: JSON.stringify({ 802 + usernames: ['testuser'], 803 + }), 804 + }, 805 + ); 806 + 807 + expect(res.status).toBe(200); 808 + const body = await parseResponse(res); 809 + expect(body.success).toBe(true); 810 + 811 + // Should still return results even if follow status fails 812 + const result = body.data.results[0]; 813 + expect(result.actors.length).toBeGreaterThanOrEqual(1); 814 + } finally { 815 + SessionService.getAgentForSession = originalMethod; 816 + } 441 817 }); 442 818 }); 443 819 });