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 search integration tests

- validate input, option follow param
- test auth requirements
- test error handling

byarielm.fyi ba3de8ac 28526743

verified
+232
+232
packages/api/__tests__/routes/search.test.ts
··· 1 + /** 2 + * Search API Integration Tests 3 + * 4 + * Tests batch actor search functionality on AT Protocol. 5 + */ 6 + 7 + import { describe, it, expect, beforeAll, afterAll } from 'vitest'; 8 + import { 9 + request, 10 + authRequest, 11 + requestWithSession, 12 + parseResponse, 13 + } from '../helpers'; 14 + import { 15 + createTestSession, 16 + cleanupAllTestSessions, 17 + } from '../fixtures'; 18 + 19 + describe('Search API', () => { 20 + let validSession: string; 21 + 22 + beforeAll(async () => { 23 + validSession = await createTestSession('standard'); 24 + }); 25 + 26 + afterAll(async () => { 27 + await cleanupAllTestSessions(); 28 + }); 29 + 30 + describe('POST /api/search/batch-search-actors', () => { 31 + it('returns 401 without authentication', async () => { 32 + const res = await request('/api/search/batch-search-actors', { 33 + method: 'POST', 34 + body: JSON.stringify({ 35 + usernames: ['testuser'], 36 + }), 37 + }); 38 + expect(res.status).toBe(401); 39 + }); 40 + 41 + it('returns 400 with empty usernames array', async () => { 42 + const res = await authRequest('/api/search/batch-search-actors', { 43 + method: 'POST', 44 + body: JSON.stringify({ 45 + usernames: [], 46 + }), 47 + }); 48 + expect(res.status).toBe(400); 49 + 50 + const body = await parseResponse(res); 51 + expect(body.success).toBe(false); 52 + expect(body.error).toContain('valid array of usernames'); 53 + }); 54 + 55 + it('returns 400 with too many usernames (>50)', async () => { 56 + const tooManyUsernames = Array.from({ length: 51 }, (_, i) => `user${i}`); 57 + 58 + const res = await authRequest('/api/search/batch-search-actors', { 59 + method: 'POST', 60 + body: JSON.stringify({ 61 + usernames: tooManyUsernames, 62 + }), 63 + }); 64 + expect(res.status).toBe(400); 65 + 66 + const body = await parseResponse(res); 67 + expect(body.success).toBe(false); 68 + expect(body.error).toContain('max 50'); 69 + }); 70 + 71 + it('returns 400 with invalid request body', async () => { 72 + const res = await authRequest('/api/search/batch-search-actors', { 73 + method: 'POST', 74 + body: JSON.stringify({ 75 + invalid: 'data', 76 + }), 77 + }); 78 + expect(res.status).toBe(400); 79 + 80 + const body = await parseResponse(res); 81 + expect(body.success).toBe(false); 82 + }); 83 + 84 + it('returns 400 with non-array usernames', async () => { 85 + const res = await authRequest('/api/search/batch-search-actors', { 86 + method: 'POST', 87 + body: JSON.stringify({ 88 + usernames: 'not-an-array', 89 + }), 90 + }); 91 + expect(res.status).toBe(400); 92 + 93 + const body = await parseResponse(res); 94 + expect(body.success).toBe(false); 95 + }); 96 + 97 + it('accepts valid request with single username', async () => { 98 + const res = await requestWithSession( 99 + '/api/search/batch-search-actors', 100 + validSession, 101 + { 102 + method: 'POST', 103 + body: JSON.stringify({ 104 + usernames: ['testuser'], 105 + }), 106 + }, 107 + ); 108 + 109 + // Note: This will fail in test environment without real AT Protocol agent 110 + // Test DIDs don't pass OAuth validation (need 32-char DIDs) 111 + // 401 = OAuth agent creation failed (expected with test DIDs) 112 + // 200 = success (with real OAuth), 500 = agent error 113 + expect([200, 401, 500]).toContain(res.status); 114 + 115 + if (res.status === 200) { 116 + const body = await parseResponse(res); 117 + expect(body.success).toBe(true); 118 + expect(body.data).toHaveProperty('results'); 119 + expect(Array.isArray(body.data.results)).toBe(true); 120 + } 121 + }); 122 + 123 + it('accepts valid request with multiple usernames', async () => { 124 + const res = await requestWithSession( 125 + '/api/search/batch-search-actors', 126 + validSession, 127 + { 128 + method: 'POST', 129 + body: JSON.stringify({ 130 + usernames: ['user1', 'user2', 'user3'], 131 + }), 132 + }, 133 + ); 134 + 135 + expect([200, 401, 500]).toContain(res.status); 136 + 137 + if (res.status === 200) { 138 + const body = await parseResponse(res); 139 + expect(body.success).toBe(true); 140 + expect(body.data.results.length).toBe(3); 141 + } 142 + }); 143 + 144 + it('accepts optional followLexicon parameter', async () => { 145 + const res = await requestWithSession( 146 + '/api/search/batch-search-actors', 147 + validSession, 148 + { 149 + method: 'POST', 150 + body: JSON.stringify({ 151 + usernames: ['testuser'], 152 + followLexicon: 'app.bsky.graph.follow', 153 + }), 154 + }, 155 + ); 156 + 157 + expect([200, 401, 500]).toContain(res.status); 158 + }); 159 + 160 + it('uses default followLexicon when not provided', async () => { 161 + const res = await requestWithSession( 162 + '/api/search/batch-search-actors', 163 + validSession, 164 + { 165 + method: 'POST', 166 + body: JSON.stringify({ 167 + usernames: ['testuser'], 168 + }), 169 + }, 170 + ); 171 + 172 + expect([200, 401, 500]).toContain(res.status); 173 + 174 + // If successful, verify results have followStatus 175 + if (res.status === 200) { 176 + const body = await parseResponse(res); 177 + const result = body.data.results[0]; 178 + if (result.actors && result.actors.length > 0) { 179 + expect(result.actors[0]).toHaveProperty('followStatus'); 180 + } 181 + } 182 + }); 183 + 184 + // Note: Comprehensive search functionality tests (ranking algorithm, 185 + // profile enrichment, follow status) would require mocking the AT Protocol 186 + // agent and services. These are better tested with: 187 + // 1. Unit tests for the ranking/matching logic 188 + // 2. E2E tests with a test AT Protocol instance 189 + // 3. Manual testing against real Bluesky accounts 190 + 191 + describe('Response Structure (when successful)', () => { 192 + it('returns properly structured search results', async () => { 193 + const res = await requestWithSession( 194 + '/api/search/batch-search-actors', 195 + validSession, 196 + { 197 + method: 'POST', 198 + body: JSON.stringify({ 199 + usernames: ['testuser'], 200 + }), 201 + }, 202 + ); 203 + 204 + if (res.status === 200) { 205 + const body = await parseResponse(res); 206 + expect(body.success).toBe(true); 207 + expect(body.data).toBeDefined(); 208 + expect(body.data.results).toBeDefined(); 209 + expect(Array.isArray(body.data.results)).toBe(true); 210 + 211 + const result = body.data.results[0]; 212 + expect(result).toHaveProperty('username'); 213 + expect(result).toHaveProperty('actors'); 214 + expect(result).toHaveProperty('error'); 215 + expect(Array.isArray(result.actors)).toBe(true); 216 + 217 + // If actors found, verify structure 218 + if (result.actors.length > 0) { 219 + const actor = result.actors[0]; 220 + expect(actor).toHaveProperty('did'); 221 + expect(actor).toHaveProperty('handle'); 222 + expect(actor).toHaveProperty('matchScore'); 223 + expect(actor).toHaveProperty('postCount'); 224 + expect(actor).toHaveProperty('followerCount'); 225 + expect(actor).toHaveProperty('followStatus'); 226 + expect(typeof actor.matchScore).toBe('number'); 227 + } 228 + } 229 + }); 230 + }); 231 + }); 232 + });