import { expect, tap } from '@git.zone/tstest/tapbundle'; import { webrequest, WebrequestClient } from '../ts/index.js'; import { TypedServer } from '@api.global/typedserver'; let testServer: TypedServer; // Setup test server tap.test('setup test server for v4 tests', async () => { testServer = new TypedServer({ cors: false, forceSsl: false, port: 2346, }); // Route that returns JSON with cache headers testServer.addRoute('/cached', 'GET', async (ctx) => { return new Response(JSON.stringify({ data: 'cached response', timestamp: Date.now() }), { status: 200, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'max-age=60', 'ETag': '"12345"', }, }); }); // Route that returns different data each time testServer.addRoute('/dynamic', 'GET', async (ctx) => { return new Response(JSON.stringify({ data: 'dynamic response', timestamp: Date.now() }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); }); // Route that sometimes fails (for retry testing) let requestCount = 0; testServer.addRoute('/flaky', 'GET', async (ctx) => { requestCount++; if (requestCount < 3) { return new Response(null, { status: 500 }); } else { return new Response(JSON.stringify({ success: true, attempts: requestCount }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); } }); // Route that always fails with 500 (for fallback testing) testServer.addRoute('/always-fails', 'GET', async (ctx) => { return new Response(null, { status: 500 }); }); // Route that takes a long time to respond (for timeout testing) testServer.addRoute('/slow', 'GET', async (ctx) => { await new Promise((resolve) => setTimeout(resolve, 5000)); return new Response(JSON.stringify({ data: 'slow response' }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); }); // Route that returns 304 when ETag matches testServer.addRoute('/conditional', 'GET', async (ctx) => { const ifNoneMatch = ctx.request.headers.get('if-none-match'); if (ifNoneMatch === '"67890"') { return new Response(null, { status: 304 }); } else { return new Response(JSON.stringify({ data: 'conditional response' }), { status: 200, headers: { 'Content-Type': 'application/json', 'ETag': '"67890"', }, }); } }); // POST route for testing testServer.addRoute('/post', 'POST', async (ctx) => { return new Response(JSON.stringify({ received: true }), { status: 200, headers: { 'Content-Type': 'application/json' }, }); }); await testServer.start(); }); // Test 1: Basic fetch-compatible API tap.test('should work as fetch replacement', async () => { const response = await webrequest('http://localhost:2346/dynamic'); expect(response).toBeInstanceOf(Response); expect(response.ok).toEqual(true); expect(response.status).toEqual(200); const data = await response.json(); expect(data).toHaveProperty('data'); expect(data.data).toEqual('dynamic response'); }); // Test 2: getJson convenience method with generics tap.test('should support getJson with type safety', async () => { interface TestData { data: string; timestamp: number; } const data = await webrequest.getJson('http://localhost:2346/dynamic'); expect(data).toHaveProperty('data'); expect(data).toHaveProperty('timestamp'); expect(typeof data.timestamp).toEqual('number'); }); // Test 3: POST with JSON tap.test('should support postJson', async () => { const data = await webrequest.postJson('http://localhost:2346/post', { test: 'data' }); expect(data).toHaveProperty('received'); expect(data.received).toEqual(true); }); // Test 4: Cache strategy - network-first tap.test('should support network-first cache strategy', async () => { const response1 = await webrequest('http://localhost:2346/cached', { cacheStrategy: 'network-first' }); const data1 = await response1.json(); expect(data1).toHaveProperty('timestamp'); // Second request should hit network but may use cache on error const response2 = await webrequest('http://localhost:2346/cached', { cacheStrategy: 'network-first' }); const data2 = await response2.json(); expect(data2).toHaveProperty('timestamp'); }); // Test 5: Cache strategy - cache-first tap.test('should support cache-first strategy', async () => { // First request goes to network const response1 = await webrequest('http://localhost:2346/cached', { cacheStrategy: 'cache-first' }); const data1 = await response1.json(); const timestamp1 = data1.timestamp; // Second request should use cache const response2 = await webrequest('http://localhost:2346/cached', { cacheStrategy: 'cache-first' }); const data2 = await response2.json(); // Timestamps should be identical if cached expect(data2.timestamp).toEqual(timestamp1); }); // Test 6: Retry system tap.test('should retry failed requests', async () => { const response = await webrequest('http://localhost:2346/flaky', { retry: { maxAttempts: 3, backoff: 'constant', initialDelay: 100 } }); const data = await response.json(); expect(data.success).toEqual(true); expect(data.attempts).toBeGreaterThanOrEqual(3); }); // Test 7: Fallback URLs tap.test('should support fallback URLs', async () => { const response = await webrequest('http://localhost:2346/always-fails', { fallbackUrls: ['http://localhost:2346/dynamic'], retry: { maxAttempts: 2 } }); expect(response.ok).toEqual(true); const data = await response.json(); expect(data).toHaveProperty('data'); }); // Test 8: Request interceptors tap.test('should support request interceptors', async () => { let interceptorCalled = false; const response = await webrequest('http://localhost:2346/dynamic', { interceptors: { request: [(req) => { interceptorCalled = true; const headers = new Headers(req.headers); headers.set('X-Custom-Header', 'test'); return new Request(req, { headers }); }] } }); expect(interceptorCalled).toEqual(true); expect(response.ok).toEqual(true); }); // Test 9: Response interceptors tap.test('should support response interceptors', async () => { let interceptorCalled = false; let capturedStatus: number; const response = await webrequest('http://localhost:2346/dynamic', { interceptors: { response: [(res) => { interceptorCalled = true; capturedStatus = res.status; return res; }] } }); expect(interceptorCalled).toEqual(true); expect(capturedStatus!).toEqual(200); }); // Test 10: Global interceptors with WebrequestClient tap.test('should support global interceptors', async () => { const client = new WebrequestClient(); let globalInterceptorCalled = false; client.addRequestInterceptor((req) => { globalInterceptorCalled = true; return req; }); await client.request('http://localhost:2346/dynamic'); expect(globalInterceptorCalled).toEqual(true); }); // Test 11: Request deduplication tap.test('should deduplicate simultaneous requests', async () => { // Make 3 identical requests simultaneously const [res1, res2, res3] = await Promise.all([ webrequest('http://localhost:2346/dynamic', { deduplicate: true }), webrequest('http://localhost:2346/dynamic', { deduplicate: true }), webrequest('http://localhost:2346/dynamic', { deduplicate: true }), ]); const [data1, data2, data3] = await Promise.all([ res1.json(), res2.json(), res3.json(), ]); // All should have the same timestamp (same response) expect(data1.timestamp).toEqual(data2.timestamp); expect(data2.timestamp).toEqual(data3.timestamp); }); // Test 12: Timeout tap.test('should support timeout', async () => { try { await webrequest('http://localhost:2346/slow', { timeout: 100 // 100ms timeout should fail (route takes 5000ms) }); throw new Error('Should have timed out'); } catch (error) { expect(error.message).toContain('timeout'); } }); // Test 13: WebrequestClient with default options tap.test('should support WebrequestClient with defaults', async () => { const client = new WebrequestClient({ logging: false, cacheStrategy: 'network-first', timeout: 30000 }); const data = await client.getJson('http://localhost:2346/dynamic'); expect(data).toHaveProperty('data'); }); // Test 14: Clear cache tap.test('should clear cache', async () => { // Cache a request await webrequest('http://localhost:2346/cached', { cacheStrategy: 'cache-first' }); // Clear cache await webrequest.clearCache(); // This should work even though cache is cleared const response = await webrequest('http://localhost:2346/cached', { cacheStrategy: 'cache-first' }); expect(response.ok).toEqual(true); }); // Cleanup tap.test('stop test server', async () => { await testServer.stop(); }); export default tap.start();