Image CDN for atproto built on cloudflare
at main 24 kB view raw
1import { env, createExecutionContext, waitOnExecutionContext, SELF } from 'cloudflare:test'; 2import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; 3import { CID } from 'multiformats/cid'; 4import worker, { 5 base62ToBytes, 6 detectIdentifierFormat, 7 resolveHandleToDID, 8 resolvePDSHost, 9 fetchBlobCidFromRecord, 10 downloadBlobUnauthenticated, 11} from '../src/index'; 12 13const IncomingRequest = Request<unknown, IncomingRequestCfProperties>; 14 15// Test constants 16const TEST_DID = 'did:plc:ewvi7nxzyoun6zhxrhs64oiz'; 17const TEST_HANDLE = 'bsky.app'; 18const TEST_CID = 'bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku'; 19const TEST_TID = '3jui7kd5354sr'; 20const TEST_PDS = 'https://bsky.social'; 21 22describe('base62ToBytes', () => { 23 it('converts empty string to empty Uint8Array', () => { 24 const result = base62ToBytes(''); 25 expect(result).toEqual(new Uint8Array([])); 26 }); 27 28 it('converts single character correctly', () => { 29 const result = base62ToBytes('1'); 30 expect(result).toEqual(new Uint8Array([1])); 31 }); 32 33 it('converts known base62 to correct bytes', () => { 34 // Test with "10" which is 62 in decimal (1*62 + 0) 35 const result = base62ToBytes('10'); 36 expect(result).toEqual(new Uint8Array([62])); 37 }); 38 39 it('handles larger numbers correctly', () => { 40 // "100" = 1*62^2 + 0*62 + 0 = 3844 41 const result = base62ToBytes('100'); 42 // 3844 = 0x0F04, so bytes are [15, 4] 43 expect(result).toEqual(new Uint8Array([15, 4])); 44 }); 45 46 it('round-trips through CID decode', () => { 47 // Create a valid CID, encode to base62-ish bytes, then decode 48 const cid = CID.parse(TEST_CID); 49 const bytes = cid.bytes; 50 51 // Encode bytes to base62 52 const BASE62_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; 53 let num = 0n; 54 for (const byte of bytes) { 55 num = num * 256n + BigInt(byte); 56 } 57 let base62 = ''; 58 while (num > 0n) { 59 base62 = BASE62_CHARS[Number(num % 62n)] + base62; 60 num = num / 62n; 61 } 62 63 // Now decode back using our function 64 const decoded = base62ToBytes(base62); 65 const decodedCid = CID.decode(decoded); 66 expect(decodedCid.toString()).toBe(TEST_CID); 67 }); 68}); 69 70describe('detectIdentifierFormat', () => { 71 it('detects base32 CID format (bafkrei prefix)', () => { 72 expect(detectIdentifierFormat('bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku')).toBe('base32'); 73 expect(detectIdentifierFormat('bafkreiabc')).toBe('base32'); 74 }); 75 76 it('detects TID format (13 chars matching pattern)', () => { 77 expect(detectIdentifierFormat('3jui7kd5354sr')).toBe('tid'); 78 expect(detectIdentifierFormat('3kfg2b5fyjk2i')).toBe('tid'); 79 }); 80 81 it('returns base62 for other formats', () => { 82 expect(detectIdentifierFormat('abc123XYZ')).toBe('base62'); 83 expect(detectIdentifierFormat('shortCID')).toBe('base62'); 84 }); 85 86 it('returns base62 for invalid TID length', () => { 87 // Too short 88 expect(detectIdentifierFormat('3jui7kd535')).toBe('base62'); 89 // Too long 90 expect(detectIdentifierFormat('3jui7kd5354srx')).toBe('base62'); 91 }); 92 93 it('returns base62 for invalid TID characters', () => { 94 // Contains invalid first character (0, 1, or other invalid) 95 expect(detectIdentifierFormat('0jui7kd5354sr')).toBe('base62'); 96 expect(detectIdentifierFormat('1jui7kd5354sr')).toBe('base62'); 97 }); 98}); 99 100describe('resolveHandleToDID', () => { 101 beforeEach(() => { 102 vi.stubGlobal('fetch', vi.fn()); 103 }); 104 105 afterEach(() => { 106 vi.unstubAllGlobals(); 107 }); 108 109 it('resolves DID via DNS-over-HTTPS', async () => { 110 const mockFetch = vi.fn().mockResolvedValueOnce({ 111 ok: true, 112 json: async () => ({ 113 Answer: [ 114 { type: 16, data: `"did=${TEST_DID}"` } 115 ] 116 }) 117 }); 118 vi.stubGlobal('fetch', mockFetch); 119 120 const did = await resolveHandleToDID(TEST_HANDLE); 121 expect(did).toBe(TEST_DID); 122 expect(mockFetch).toHaveBeenCalledWith( 123 expect.stringContaining(`_atproto.${TEST_HANDLE}`), 124 expect.any(Object) 125 ); 126 }); 127 128 it('falls back to HTTPS well-known when DNS fails', async () => { 129 const mockFetch = vi.fn() 130 // First call (DNS) fails 131 .mockResolvedValueOnce({ 132 ok: false 133 }) 134 // Second call (well-known) succeeds 135 .mockResolvedValueOnce({ 136 status: 200, 137 text: async () => TEST_DID 138 }); 139 vi.stubGlobal('fetch', mockFetch); 140 141 const did = await resolveHandleToDID(TEST_HANDLE); 142 expect(did).toBe(TEST_DID); 143 expect(mockFetch).toHaveBeenCalledTimes(2); 144 }); 145 146 it('returns null when all resolution methods fail', async () => { 147 const mockFetch = vi.fn() 148 .mockResolvedValueOnce({ ok: false }) 149 .mockResolvedValueOnce({ status: 404 }); 150 vi.stubGlobal('fetch', mockFetch); 151 152 const did = await resolveHandleToDID('nonexistent.handle'); 153 expect(did).toBeNull(); 154 }); 155 156 it('extracts DID from DNS response with quotes', async () => { 157 const mockFetch = vi.fn().mockResolvedValueOnce({ 158 ok: true, 159 json: async () => ({ 160 Answer: [ 161 { type: 16, data: '"did=did:plc:z72i7hdynmk6r22z27h6tvur"' } 162 ] 163 }) 164 }); 165 vi.stubGlobal('fetch', mockFetch); 166 167 const did = await resolveHandleToDID('example.com'); 168 expect(did).toBe('did:plc:z72i7hdynmk6r22z27h6tvur'); 169 }); 170 171 it('resolves DID via native DNS when available', async () => { 172 const mockResolveDns = vi.fn().mockResolvedValue(['did=did:plc:nativedns123']); 173 vi.stubGlobal('resolveDns', mockResolveDns); 174 175 const did = await resolveHandleToDID('native.test'); 176 expect(did).toBe('did:plc:nativedns123'); 177 expect(mockResolveDns).toHaveBeenCalledWith('_atproto.native.test', 'TXT'); 178 179 vi.unstubAllGlobals(); 180 }); 181 182 it('handles native DNS throwing exception', async () => { 183 const mockResolveDns = vi.fn().mockRejectedValue(new Error('DNS error')); 184 vi.stubGlobal('resolveDns', mockResolveDns); 185 186 // DNS throws, should fall back to DNS-over-HTTPS 187 const mockFetch = vi.fn().mockResolvedValueOnce({ 188 ok: true, 189 json: async () => ({ 190 Answer: [{ type: 16, data: '"did=did:plc:fallback123"' }] 191 }) 192 }); 193 vi.stubGlobal('fetch', mockFetch); 194 195 const did = await resolveHandleToDID('fallback.test'); 196 expect(did).toBe('did:plc:fallback123'); 197 198 vi.unstubAllGlobals(); 199 }); 200 201 it('handles DNS-over-HTTPS fetch exception', async () => { 202 const mockFetch = vi.fn() 203 // DNS-over-HTTPS throws 204 .mockRejectedValueOnce(new Error('Network error')) 205 // HTTPS well-known succeeds 206 .mockResolvedValueOnce({ 207 status: 200, 208 text: async () => 'did:plc:wellknown123' 209 }); 210 vi.stubGlobal('fetch', mockFetch); 211 212 const did = await resolveHandleToDID('wellknown.test'); 213 expect(did).toBe('did:plc:wellknown123'); 214 }); 215 216 it('handles HTTPS well-known fetch exception', async () => { 217 const mockFetch = vi.fn() 218 // DNS-over-HTTPS fails 219 .mockResolvedValueOnce({ ok: false }) 220 // HTTPS well-known throws 221 .mockRejectedValueOnce(new Error('Connection refused')); 222 vi.stubGlobal('fetch', mockFetch); 223 224 const did = await resolveHandleToDID('error.test'); 225 expect(did).toBeNull(); 226 }); 227}); 228 229describe('resolvePDSHost', () => { 230 beforeEach(() => { 231 vi.stubGlobal('fetch', vi.fn()); 232 }); 233 234 afterEach(() => { 235 vi.unstubAllGlobals(); 236 }); 237 238 it('resolves PDS host for did:plc via PLC directory', async () => { 239 const mockFetch = vi.fn().mockResolvedValueOnce({ 240 ok: true, 241 json: async () => ({ 242 service: [ 243 { id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: TEST_PDS } 244 ] 245 }) 246 }); 247 vi.stubGlobal('fetch', mockFetch); 248 249 const pdsHost = await resolvePDSHost(TEST_DID); 250 expect(pdsHost).toBe(TEST_PDS); 251 expect(mockFetch).toHaveBeenCalled(); 252 }); 253 254 it('resolves PDS host for did:web via well-known', async () => { 255 const webDid = 'did:web:example.com'; 256 const mockFetch = vi.fn().mockResolvedValueOnce({ 257 ok: true, 258 json: async () => ({ 259 service: [ 260 { id: '#atproto_pds', type: 'AtprotoPersonalDataServer', serviceEndpoint: 'https://pds.example.com' } 261 ] 262 }) 263 }); 264 vi.stubGlobal('fetch', mockFetch); 265 266 const pdsHost = await resolvePDSHost(webDid); 267 expect(pdsHost).toBe('https://pds.example.com'); 268 expect(mockFetch).toHaveBeenCalled(); 269 }); 270 271 it('returns null when PDS not found', async () => { 272 const mockFetch = vi.fn().mockResolvedValueOnce({ 273 ok: false 274 }); 275 vi.stubGlobal('fetch', mockFetch); 276 277 const pdsHost = await resolvePDSHost(TEST_DID); 278 expect(pdsHost).toBeNull(); 279 }); 280 281 it('returns null when service array is missing', async () => { 282 const mockFetch = vi.fn().mockResolvedValueOnce({ 283 ok: true, 284 json: async () => ({}) 285 }); 286 vi.stubGlobal('fetch', mockFetch); 287 288 const pdsHost = await resolvePDSHost(TEST_DID); 289 expect(pdsHost).toBeNull(); 290 }); 291 292 it('returns null when fetch throws exception', async () => { 293 const mockFetch = vi.fn().mockRejectedValueOnce(new Error('Network error')); 294 vi.stubGlobal('fetch', mockFetch); 295 296 const pdsHost = await resolvePDSHost(TEST_DID); 297 expect(pdsHost).toBeNull(); 298 }); 299}); 300 301describe('fetchBlobCidFromRecord', () => { 302 beforeEach(() => { 303 vi.stubGlobal('fetch', vi.fn()); 304 }); 305 306 afterEach(() => { 307 vi.unstubAllGlobals(); 308 }); 309 310 it('fetches blob CID from record', async () => { 311 const mockFetch = vi.fn() 312 // First call: resolvePDSHost 313 .mockResolvedValueOnce({ 314 ok: true, 315 json: async () => ({ 316 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 317 }) 318 }) 319 // Second call: getRecord 320 .mockResolvedValueOnce({ 321 ok: true, 322 json: async () => ({ 323 uri: `at://${TEST_DID}/blue.imgs.blup.image/${TEST_TID}`, 324 cid: 'somecid', 325 value: { 326 blob: { 327 ref: { $link: TEST_CID } 328 } 329 } 330 }) 331 }); 332 vi.stubGlobal('fetch', mockFetch); 333 334 const blobCid = await fetchBlobCidFromRecord(TEST_DID, TEST_TID); 335 expect(blobCid).toBe(TEST_CID); 336 }); 337 338 it('returns null when PDS resolution fails', async () => { 339 const mockFetch = vi.fn().mockResolvedValueOnce({ 340 ok: false 341 }); 342 vi.stubGlobal('fetch', mockFetch); 343 344 const blobCid = await fetchBlobCidFromRecord(TEST_DID, TEST_TID); 345 expect(blobCid).toBeNull(); 346 }); 347 348 it('returns null when record has no blob', async () => { 349 const mockFetch = vi.fn() 350 .mockResolvedValueOnce({ 351 ok: true, 352 json: async () => ({ 353 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 354 }) 355 }) 356 .mockResolvedValueOnce({ 357 ok: true, 358 json: async () => ({ 359 uri: `at://${TEST_DID}/blue.imgs.blup.image/${TEST_TID}`, 360 cid: 'somecid', 361 value: {} 362 }) 363 }); 364 vi.stubGlobal('fetch', mockFetch); 365 366 const blobCid = await fetchBlobCidFromRecord(TEST_DID, TEST_TID); 367 expect(blobCid).toBeNull(); 368 }); 369 370 it('returns null when getRecord returns non-OK response', async () => { 371 const mockFetch = vi.fn() 372 .mockResolvedValueOnce({ 373 ok: true, 374 json: async () => ({ 375 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 376 }) 377 }) 378 .mockResolvedValueOnce({ 379 ok: false, 380 status: 404 381 }); 382 vi.stubGlobal('fetch', mockFetch); 383 384 const blobCid = await fetchBlobCidFromRecord(TEST_DID, TEST_TID); 385 expect(blobCid).toBeNull(); 386 }); 387 388 it('returns null when getRecord fetch throws exception', async () => { 389 const mockFetch = vi.fn() 390 .mockResolvedValueOnce({ 391 ok: true, 392 json: async () => ({ 393 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 394 }) 395 }) 396 .mockRejectedValueOnce(new Error('Network error')); 397 vi.stubGlobal('fetch', mockFetch); 398 399 const blobCid = await fetchBlobCidFromRecord(TEST_DID, TEST_TID); 400 expect(blobCid).toBeNull(); 401 }); 402}); 403 404describe('downloadBlobUnauthenticated', () => { 405 beforeEach(() => { 406 vi.stubGlobal('fetch', vi.fn()); 407 }); 408 409 afterEach(() => { 410 vi.unstubAllGlobals(); 411 }); 412 413 it('downloads blob with correct headers', async () => { 414 const mockFetch = vi.fn() 415 // resolvePDSHost 416 .mockResolvedValueOnce({ 417 ok: true, 418 json: async () => ({ 419 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 420 }) 421 }) 422 // getBlob 423 .mockResolvedValueOnce({ 424 status: 200, 425 body: new ReadableStream(), 426 headers: new Headers({ 427 'Content-Type': 'image/jpeg', 428 'Content-Length': '12345' 429 }) 430 }); 431 vi.stubGlobal('fetch', mockFetch); 432 433 const ctx = createExecutionContext(); 434 const response = await downloadBlobUnauthenticated(TEST_DID, TEST_CID, ctx); 435 436 expect(response.status).toBe(200); 437 expect(response.headers.get('Content-Type')).toBe('image/jpeg'); 438 expect(response.headers.get('Cache-Control')).toBe('public, max-age=31536000, immutable'); 439 expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); 440 }); 441 442 it('returns 400 when PDS resolution fails', async () => { 443 const mockFetch = vi.fn().mockResolvedValueOnce({ 444 ok: false 445 }); 446 vi.stubGlobal('fetch', mockFetch); 447 448 const ctx = createExecutionContext(); 449 const response = await downloadBlobUnauthenticated(TEST_DID, TEST_CID, ctx); 450 451 expect(response.status).toBe(400); 452 expect(await response.text()).toBe('Failed to resolve PDS host'); 453 }); 454 455 it('returns 404 when blob not found', async () => { 456 const mockFetch = vi.fn() 457 .mockResolvedValueOnce({ 458 ok: true, 459 json: async () => ({ 460 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 461 }) 462 }) 463 .mockResolvedValueOnce({ 464 status: 404 465 }); 466 vi.stubGlobal('fetch', mockFetch); 467 468 const ctx = createExecutionContext(); 469 const response = await downloadBlobUnauthenticated(TEST_DID, TEST_CID, ctx); 470 471 expect(response.status).toBe(404); 472 }); 473 474 it('returns 500 when fetch throws exception', async () => { 475 const mockFetch = vi.fn() 476 .mockResolvedValueOnce({ 477 ok: true, 478 json: async () => ({ 479 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 480 }) 481 }) 482 .mockRejectedValueOnce(new Error('Network error')); 483 vi.stubGlobal('fetch', mockFetch); 484 485 const ctx = createExecutionContext(); 486 const response = await downloadBlobUnauthenticated(TEST_DID, TEST_CID, ctx); 487 488 expect(response.status).toBe(500); 489 expect(await response.text()).toBe('Failed to download blob'); 490 }); 491}); 492 493describe('Worker fetch handler', () => { 494 it('handles CORS preflight requests', async () => { 495 const request = new IncomingRequest('http://example.com/test/test', { 496 method: 'OPTIONS' 497 }); 498 const ctx = createExecutionContext(); 499 const response = await worker.fetch(request, env, ctx); 500 await waitOnExecutionContext(ctx); 501 502 expect(response.status).toBe(200); 503 expect(response.headers.get('Access-Control-Allow-Origin')).toBe('*'); 504 expect(response.headers.get('Access-Control-Allow-Methods')).toBe('GET, OPTIONS'); 505 }); 506 507 it('returns 400 for invalid paths', async () => { 508 const request = new IncomingRequest('http://example.com/'); 509 const ctx = createExecutionContext(); 510 const response = await worker.fetch(request, env, ctx); 511 await waitOnExecutionContext(ctx); 512 513 expect(response.status).toBe(400); 514 expect(await response.text()).toContain('Invalid path'); 515 }); 516 517 it('returns 400 for paths with only handle', async () => { 518 const request = new IncomingRequest('http://example.com/bsky.app'); 519 const ctx = createExecutionContext(); 520 const response = await worker.fetch(request, env, ctx); 521 await waitOnExecutionContext(ctx); 522 523 expect(response.status).toBe(400); 524 }); 525 526 it('strips file extensions from CID', async () => { 527 // This tests that extensions like .jpg, .png are stripped 528 // We'll test this by mocking and checking the CID used 529 const mockFetch = vi.fn() 530 // DNS resolution 531 .mockResolvedValueOnce({ 532 ok: true, 533 json: async () => ({ 534 Answer: [{ type: 16, data: `"did=${TEST_DID}"` }] 535 }) 536 }) 537 // PDS resolution 538 .mockResolvedValueOnce({ 539 ok: true, 540 json: async () => ({ 541 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 542 }) 543 }) 544 // Blob download 545 .mockResolvedValueOnce({ 546 status: 200, 547 body: new ReadableStream(), 548 headers: new Headers({ 'Content-Type': 'image/jpeg' }) 549 }); 550 vi.stubGlobal('fetch', mockFetch); 551 552 const request = new IncomingRequest(`http://example.com/${TEST_HANDLE}/${TEST_CID}.jpg`); 553 const ctx = createExecutionContext(); 554 const response = await worker.fetch(request, env, ctx); 555 await waitOnExecutionContext(ctx); 556 557 // Should have made it to blob download (extension stripped correctly) 558 expect(mockFetch).toHaveBeenCalledTimes(3); 559 560 vi.unstubAllGlobals(); 561 }); 562 563 it('handles DID directly without resolution', async () => { 564 const mockFetch = vi.fn() 565 // PDS resolution 566 .mockResolvedValueOnce({ 567 ok: true, 568 json: async () => ({ 569 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 570 }) 571 }) 572 // Blob download 573 .mockResolvedValueOnce({ 574 status: 200, 575 body: new ReadableStream(), 576 headers: new Headers({ 'Content-Type': 'image/jpeg' }) 577 }); 578 vi.stubGlobal('fetch', mockFetch); 579 580 const request = new IncomingRequest(`http://example.com/${TEST_DID}/${TEST_CID}`); 581 const ctx = createExecutionContext(); 582 const response = await worker.fetch(request, env, ctx); 583 await waitOnExecutionContext(ctx); 584 585 // Should skip DNS resolution since it's already a DID 586 expect(response.status).toBe(200); 587 expect(mockFetch).toHaveBeenCalledTimes(2); 588 589 vi.unstubAllGlobals(); 590 }); 591 592 it('returns 404 when handle cannot be resolved', async () => { 593 const mockFetch = vi.fn() 594 // DNS lookup fails 595 .mockResolvedValueOnce({ ok: false }) 596 // HTTPS well-known fails 597 .mockResolvedValueOnce({ status: 404 }); 598 vi.stubGlobal('fetch', mockFetch); 599 600 const request = new IncomingRequest('http://example.com/nonexistent.handle/somecid'); 601 const ctx = createExecutionContext(); 602 const response = await worker.fetch(request, env, ctx); 603 await waitOnExecutionContext(ctx); 604 605 expect(response.status).toBe(404); 606 expect(await response.text()).toBe('Handle not found'); 607 608 vi.unstubAllGlobals(); 609 }); 610 611 it('uses cached DID from KV (plc format)', async () => { 612 const mockFetch = vi.fn() 613 // PDS resolution 614 .mockResolvedValueOnce({ 615 ok: true, 616 json: async () => ({ 617 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 618 }) 619 }) 620 // Blob download 621 .mockResolvedValueOnce({ 622 status: 200, 623 body: new ReadableStream(), 624 headers: new Headers({ 'Content-Type': 'image/jpeg' }) 625 }); 626 vi.stubGlobal('fetch', mockFetch); 627 628 // Create a mock env with KV that returns a cached DID 629 const mockEnv = { 630 USER_CACHE: { 631 get: vi.fn().mockResolvedValue('ewvi7nxzyoun6zhxrhs64oiz'), 632 put: vi.fn() 633 } 634 }; 635 636 const request = new IncomingRequest(`http://example.com/${TEST_HANDLE}/${TEST_CID}`); 637 const ctx = createExecutionContext(); 638 const response = await worker.fetch(request, mockEnv as any, ctx); 639 await waitOnExecutionContext(ctx); 640 641 // Should use cached DID and skip DNS resolution 642 expect(response.status).toBe(200); 643 expect(mockEnv.USER_CACHE.get).toHaveBeenCalledWith(TEST_HANDLE); 644 // Only PDS + blob fetch, no DNS lookup 645 expect(mockFetch).toHaveBeenCalledTimes(2); 646 647 vi.unstubAllGlobals(); 648 }); 649 650 it('uses cached DID from KV (web format)', async () => { 651 const mockFetch = vi.fn() 652 // PDS resolution for did:web 653 .mockResolvedValueOnce({ 654 ok: true, 655 json: async () => ({ 656 service: [{ id: '#atproto_pds', serviceEndpoint: 'https://pds.example.com' }] 657 }) 658 }) 659 // Blob download 660 .mockResolvedValueOnce({ 661 status: 200, 662 body: new ReadableStream(), 663 headers: new Headers({ 'Content-Type': 'image/png' }) 664 }); 665 vi.stubGlobal('fetch', mockFetch); 666 667 // Create a mock env with KV that returns a cached web DID 668 const mockEnv = { 669 USER_CACHE: { 670 get: vi.fn().mockResolvedValue('web:example.com'), 671 put: vi.fn() 672 } 673 }; 674 675 const request = new IncomingRequest(`http://example.com/somehandle/${TEST_CID}`); 676 const ctx = createExecutionContext(); 677 const response = await worker.fetch(request, mockEnv as any, ctx); 678 await waitOnExecutionContext(ctx); 679 680 expect(response.status).toBe(200); 681 expect(mockEnv.USER_CACHE.get).toHaveBeenCalled(); 682 683 vi.unstubAllGlobals(); 684 }); 685 686 it('resolves TID to blob CID', async () => { 687 const mockFetch = vi.fn() 688 // PDS resolution (for fetchBlobCidFromRecord) 689 .mockResolvedValueOnce({ 690 ok: true, 691 json: async () => ({ 692 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 693 }) 694 }) 695 // getRecord 696 .mockResolvedValueOnce({ 697 ok: true, 698 json: async () => ({ 699 uri: `at://${TEST_DID}/blue.imgs.blup.image/${TEST_TID}`, 700 cid: 'recordcid', 701 value: { 702 blob: { ref: { $link: TEST_CID } } 703 } 704 }) 705 }) 706 // PDS resolution (for downloadBlobUnauthenticated) 707 .mockResolvedValueOnce({ 708 ok: true, 709 json: async () => ({ 710 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 711 }) 712 }) 713 // Blob download 714 .mockResolvedValueOnce({ 715 status: 200, 716 body: new ReadableStream(), 717 headers: new Headers({ 'Content-Type': 'image/jpeg' }) 718 }); 719 vi.stubGlobal('fetch', mockFetch); 720 721 const request = new IncomingRequest(`http://example.com/${TEST_DID}/${TEST_TID}`); 722 const ctx = createExecutionContext(); 723 const response = await worker.fetch(request, env, ctx); 724 await waitOnExecutionContext(ctx); 725 726 expect(response.status).toBe(200); 727 728 vi.unstubAllGlobals(); 729 }); 730 731 it('returns 404 when TID record not found', async () => { 732 const mockFetch = vi.fn() 733 // PDS resolution 734 .mockResolvedValueOnce({ 735 ok: true, 736 json: async () => ({ 737 service: [{ id: '#atproto_pds', serviceEndpoint: TEST_PDS }] 738 }) 739 }) 740 // getRecord returns no blob 741 .mockResolvedValueOnce({ 742 ok: true, 743 json: async () => ({ 744 uri: `at://${TEST_DID}/blue.imgs.blup.image/${TEST_TID}`, 745 cid: 'recordcid', 746 value: {} 747 }) 748 }); 749 vi.stubGlobal('fetch', mockFetch); 750 751 const request = new IncomingRequest(`http://example.com/${TEST_DID}/${TEST_TID}`); 752 const ctx = createExecutionContext(); 753 const response = await worker.fetch(request, env, ctx); 754 await waitOnExecutionContext(ctx); 755 756 expect(response.status).toBe(404); 757 expect(await response.text()).toBe('Record not found'); 758 759 vi.unstubAllGlobals(); 760 }); 761 762 it('returns 400 for invalid base62 CID encoding', async () => { 763 // Use a string that's detected as base62 but produces invalid CID bytes 764 // 'AAAA' is valid base62 but won't decode to a valid CID 765 const invalidBase62 = 'AAAA'; 766 767 const request = new IncomingRequest(`http://example.com/${TEST_DID}/${invalidBase62}`); 768 const ctx = createExecutionContext(); 769 const response = await worker.fetch(request, env, ctx); 770 await waitOnExecutionContext(ctx); 771 772 expect(response.status).toBe(400); 773 expect(await response.text()).toBe('Invalid CID encoding'); 774 }); 775});