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

- test follow and status endpoints
- validate DID format
- test array bounds, type check, error handling, etc.

byarielm.fyi c8821e1b ba3de8ac

verified
+387
+387
packages/api/__tests__/routes/follow.test.ts
··· 1 + /** 2 + * Follow API Integration Tests 3 + * 4 + * Tests batch follow operations and follow status checking. 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('Follow 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 + // Valid test DIDs for input validation tests 31 + const VALID_DID = 'did:plc:test123'; 32 + const VALID_DIDS = [ 33 + 'did:plc:test1', 34 + 'did:plc:test2', 35 + 'did:plc:test3', 36 + ]; 37 + 38 + describe('POST /api/follow/batch-follow-users', () => { 39 + it('returns 401 without authentication', async () => { 40 + const res = await request('/api/follow/batch-follow-users', { 41 + method: 'POST', 42 + body: JSON.stringify({ 43 + dids: VALID_DIDS, 44 + }), 45 + }); 46 + expect(res.status).toBe(401); 47 + }); 48 + 49 + it('returns 400 with empty dids array', async () => { 50 + const res = await requestWithSession('/api/follow/batch-follow-users', validSession, { 51 + method: 'POST', 52 + body: JSON.stringify({ 53 + dids: [], 54 + }), 55 + }); 56 + expect(res.status).toBe(400); 57 + 58 + const body = await parseResponse(res); 59 + expect(body.success).toBe(false); 60 + expect(body.error).toContain('valid array of DIDs'); 61 + }); 62 + 63 + it('returns 400 with too many dids (>100)', async () => { 64 + const tooManyDids = Array.from({ length: 101 }, (_, i) => `did:plc:test${i}`); 65 + 66 + const res = await requestWithSession('/api/follow/batch-follow-users', validSession, { 67 + method: 'POST', 68 + body: JSON.stringify({ 69 + dids: tooManyDids, 70 + }), 71 + }); 72 + expect(res.status).toBe(400); 73 + 74 + const body = await parseResponse(res); 75 + expect(body.success).toBe(false); 76 + expect(body.error).toContain('max 100'); 77 + }); 78 + 79 + it('returns 400 with invalid DID format (missing did: prefix)', async () => { 80 + const res = await requestWithSession('/api/follow/batch-follow-users', validSession, { 81 + method: 'POST', 82 + body: JSON.stringify({ 83 + dids: ['invalid-did', 'another-invalid'], 84 + }), 85 + }); 86 + expect(res.status).toBe(400); 87 + 88 + const body = await parseResponse(res); 89 + expect(body.success).toBe(false); 90 + }); 91 + 92 + it('returns 400 with non-array dids', async () => { 93 + const res = await requestWithSession('/api/follow/batch-follow-users', validSession, { 94 + method: 'POST', 95 + body: JSON.stringify({ 96 + dids: 'not-an-array', 97 + }), 98 + }); 99 + expect(res.status).toBe(400); 100 + 101 + const body = await parseResponse(res); 102 + expect(body.success).toBe(false); 103 + }); 104 + 105 + it('returns 400 with invalid request body', async () => { 106 + const res = await requestWithSession('/api/follow/batch-follow-users', validSession, { 107 + method: 'POST', 108 + body: JSON.stringify({ 109 + invalid: 'data', 110 + }), 111 + }); 112 + expect(res.status).toBe(400); 113 + 114 + const body = await parseResponse(res); 115 + expect(body.success).toBe(false); 116 + }); 117 + 118 + it('accepts valid request with single DID', async () => { 119 + const res = await requestWithSession('/api/follow/batch-follow-users', validSession, { 120 + method: 'POST', 121 + body: JSON.stringify({ 122 + dids: [VALID_DID], 123 + }), 124 + }); 125 + 126 + // Note: This will likely fail in test environment without real AT Protocol agent 127 + // The test verifies the request structure is valid and passes validation 128 + expect([200, 401, 500]).toContain(res.status); 129 + 130 + if (res.status === 200) { 131 + const body = await parseResponse(res); 132 + expect(body.success).toBe(true); 133 + expect(body.data).toBeDefined(); 134 + expect(body.data).toHaveProperty('total'); 135 + expect(body.data).toHaveProperty('succeeded'); 136 + expect(body.data).toHaveProperty('failed'); 137 + expect(body.data).toHaveProperty('alreadyFollowing'); 138 + expect(body.data).toHaveProperty('results'); 139 + expect(Array.isArray(body.data.results)).toBe(true); 140 + } 141 + }); 142 + 143 + it('accepts valid request with multiple DIDs', async () => { 144 + const res = await requestWithSession('/api/follow/batch-follow-users', validSession, { 145 + method: 'POST', 146 + body: JSON.stringify({ 147 + dids: VALID_DIDS, 148 + }), 149 + }); 150 + 151 + expect([200, 401, 500]).toContain(res.status); 152 + 153 + if (res.status === 200) { 154 + const body = await parseResponse(res); 155 + expect(body.success).toBe(true); 156 + expect(body.data.total).toBe(VALID_DIDS.length); 157 + } 158 + }); 159 + 160 + it('accepts optional followLexicon parameter', async () => { 161 + const res = await requestWithSession('/api/follow/batch-follow-users', validSession, { 162 + method: 'POST', 163 + body: JSON.stringify({ 164 + dids: [VALID_DID], 165 + followLexicon: 'app.bsky.graph.follow', 166 + }), 167 + }); 168 + 169 + expect([200, 401, 500]).toContain(res.status); 170 + }); 171 + 172 + it('accepts different DID formats (did:plc, did:web)', async () => { 173 + const res = await requestWithSession('/api/follow/batch-follow-users', validSession, { 174 + method: 'POST', 175 + body: JSON.stringify({ 176 + dids: [ 177 + 'did:plc:abc123', 178 + 'did:web:example.com', 179 + ], 180 + }), 181 + }); 182 + 183 + expect([200, 401, 500]).toContain(res.status); 184 + }); 185 + 186 + describe('Response Structure (when successful)', () => { 187 + it('returns properly structured follow results', async () => { 188 + const res = await requestWithSession('/api/follow/batch-follow-users', validSession, { 189 + method: 'POST', 190 + body: JSON.stringify({ 191 + dids: VALID_DIDS, 192 + }), 193 + }); 194 + 195 + if (res.status === 200) { 196 + const body = await parseResponse(res); 197 + expect(body.success).toBe(true); 198 + expect(body.data).toBeDefined(); 199 + 200 + // Verify summary counts 201 + expect(body.data.total).toBe(VALID_DIDS.length); 202 + expect(typeof body.data.succeeded).toBe('number'); 203 + expect(typeof body.data.failed).toBe('number'); 204 + expect(typeof body.data.alreadyFollowing).toBe('number'); 205 + 206 + // Verify counts add up 207 + expect(body.data.succeeded + body.data.failed).toBe(body.data.total); 208 + 209 + // Verify results array 210 + expect(Array.isArray(body.data.results)).toBe(true); 211 + expect(body.data.results.length).toBe(VALID_DIDS.length); 212 + 213 + // Verify individual result structure 214 + const result = body.data.results[0]; 215 + expect(result).toHaveProperty('did'); 216 + expect(result).toHaveProperty('success'); 217 + expect(result).toHaveProperty('alreadyFollowing'); 218 + expect(result).toHaveProperty('error'); 219 + expect(typeof result.success).toBe('boolean'); 220 + expect(typeof result.alreadyFollowing).toBe('boolean'); 221 + } 222 + }); 223 + }); 224 + 225 + // Note: Tests for actual follow behavior (rate limiting, already following 226 + // detection, database updates) would require mocking the AT Protocol agent 227 + // and database. These are better tested with: 228 + // 1. Unit tests for FollowService 229 + // 2. E2E tests with test AT Protocol instance 230 + // 3. Manual testing 231 + }); 232 + 233 + describe('POST /api/follow/check-status', () => { 234 + it('returns 401 without authentication', async () => { 235 + const res = await request('/api/follow/check-status', { 236 + method: 'POST', 237 + body: JSON.stringify({ 238 + dids: VALID_DIDS, 239 + }), 240 + }); 241 + expect(res.status).toBe(401); 242 + }); 243 + 244 + it('returns 400 with empty dids array', async () => { 245 + const res = await requestWithSession('/api/follow/check-status', validSession, { 246 + method: 'POST', 247 + body: JSON.stringify({ 248 + dids: [], 249 + }), 250 + }); 251 + // May get 401 if session not found (test DID issue) 252 + expect([400, 401]).toContain(res.status); 253 + 254 + if (res.status === 400) { 255 + const body = await parseResponse(res); 256 + expect(body.success).toBe(false); 257 + expect(body.error).toContain('valid array of DIDs'); 258 + } 259 + }); 260 + 261 + it('returns 400 with too many dids (>100)', async () => { 262 + const tooManyDids = Array.from({ length: 101 }, (_, i) => `did:plc:test${i}`); 263 + 264 + const res = await requestWithSession('/api/follow/check-status', validSession, { 265 + method: 'POST', 266 + body: JSON.stringify({ 267 + dids: tooManyDids, 268 + }), 269 + }); 270 + // May get 401 if session not found (test DID issue) 271 + expect([400, 401]).toContain(res.status); 272 + 273 + if (res.status === 400) { 274 + const body = await parseResponse(res); 275 + expect(body.success).toBe(false); 276 + expect(body.error).toContain('max 100'); 277 + } 278 + }); 279 + 280 + it('returns 400 with invalid DID format', async () => { 281 + const res = await requestWithSession('/api/follow/check-status', validSession, { 282 + method: 'POST', 283 + body: JSON.stringify({ 284 + dids: ['invalid-did'], 285 + }), 286 + }); 287 + // May get 401 if session not found (test DID issue) 288 + expect([400, 401]).toContain(res.status); 289 + 290 + if (res.status === 400) { 291 + const body = await parseResponse(res); 292 + expect(body.success).toBe(false); 293 + } 294 + }); 295 + 296 + it('returns 400 with non-array dids', async () => { 297 + const res = await requestWithSession('/api/follow/check-status', validSession, { 298 + method: 'POST', 299 + body: JSON.stringify({ 300 + dids: 'not-an-array', 301 + }), 302 + }); 303 + // May get 401 if session not found (test DID issue) 304 + expect([400, 401]).toContain(res.status); 305 + 306 + if (res.status === 400) { 307 + const body = await parseResponse(res); 308 + expect(body.success).toBe(false); 309 + } 310 + }); 311 + 312 + it('accepts valid request with single DID', async () => { 313 + const res = await requestWithSession('/api/follow/check-status', validSession, { 314 + method: 'POST', 315 + body: JSON.stringify({ 316 + dids: [VALID_DID], 317 + }), 318 + }); 319 + 320 + expect([200, 401, 500]).toContain(res.status); 321 + 322 + if (res.status === 200) { 323 + const body = await parseResponse(res); 324 + expect(body.success).toBe(true); 325 + expect(body.data).toHaveProperty('followStatus'); 326 + expect(typeof body.data.followStatus).toBe('object'); 327 + } 328 + }); 329 + 330 + it('accepts valid request with multiple DIDs', async () => { 331 + const res = await requestWithSession('/api/follow/check-status', validSession, { 332 + method: 'POST', 333 + body: JSON.stringify({ 334 + dids: VALID_DIDS, 335 + }), 336 + }); 337 + 338 + expect([200, 401, 500]).toContain(res.status); 339 + 340 + if (res.status === 200) { 341 + const body = await parseResponse(res); 342 + expect(body.success).toBe(true); 343 + expect(body.data.followStatus).toBeDefined(); 344 + } 345 + }); 346 + 347 + it('accepts optional followLexicon parameter', async () => { 348 + const res = await requestWithSession('/api/follow/check-status', validSession, { 349 + method: 'POST', 350 + body: JSON.stringify({ 351 + dids: [VALID_DID], 352 + followLexicon: 'app.bsky.graph.follow', 353 + }), 354 + }); 355 + 356 + expect([200, 401, 500]).toContain(res.status); 357 + }); 358 + 359 + describe('Response Structure (when successful)', () => { 360 + it('returns properly structured follow status', async () => { 361 + const res = await requestWithSession('/api/follow/check-status', validSession, { 362 + method: 'POST', 363 + body: JSON.stringify({ 364 + dids: VALID_DIDS, 365 + }), 366 + }); 367 + 368 + if (res.status === 200) { 369 + const body = await parseResponse(res); 370 + expect(body.success).toBe(true); 371 + expect(body.data).toBeDefined(); 372 + expect(body.data.followStatus).toBeDefined(); 373 + 374 + // followStatus should be a Record<string, boolean> 375 + expect(typeof body.data.followStatus).toBe('object'); 376 + 377 + // Each DID should have a boolean status 378 + for (const did of VALID_DIDS) { 379 + if (did in body.data.followStatus) { 380 + expect(typeof body.data.followStatus[did]).toBe('boolean'); 381 + } 382 + } 383 + } 384 + }); 385 + }); 386 + }); 387 + });