Image CDN for atproto built on cloudflare
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});