313 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
		
		
			
		
	
	
			313 lines
		
	
	
		
			8.8 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
|  | import { expect, tap } from '@git.zone/tstest/tapbundle'; | ||
|  | import { webrequest, WebrequestClient } from '../ts/index.js'; | ||
|  | import * as typedserver from '@api.global/typedserver'; | ||
|  | 
 | ||
|  | let testServer: typedserver.servertools.Server; | ||
|  | 
 | ||
|  | // Setup test server
 | ||
|  | tap.test('setup test server for v4 tests', async () => { | ||
|  |   testServer = new typedserver.servertools.Server({ | ||
|  |     cors: false, | ||
|  |     forceSsl: false, | ||
|  |     port: 2346, | ||
|  |   }); | ||
|  | 
 | ||
|  |   // Route that returns JSON with cache headers
 | ||
|  |   testServer.addRoute( | ||
|  |     '/cached', | ||
|  |     new typedserver.servertools.Handler('GET', (req, res) => { | ||
|  |       res.setHeader('Cache-Control', 'max-age=60'); | ||
|  |       res.setHeader('ETag', '"12345"'); | ||
|  |       res.status(200); | ||
|  |       res.send({ data: 'cached response', timestamp: Date.now() }); | ||
|  |     }), | ||
|  |   ); | ||
|  | 
 | ||
|  |   // Route that returns different data each time
 | ||
|  |   testServer.addRoute( | ||
|  |     '/dynamic', | ||
|  |     new typedserver.servertools.Handler('GET', (req, res) => { | ||
|  |       res.status(200); | ||
|  |       res.send({ data: 'dynamic response', timestamp: Date.now() }); | ||
|  |     }), | ||
|  |   ); | ||
|  | 
 | ||
|  |   // Route that sometimes fails (for retry testing)
 | ||
|  |   let requestCount = 0; | ||
|  |   testServer.addRoute( | ||
|  |     '/flaky', | ||
|  |     new typedserver.servertools.Handler('GET', (req, res) => { | ||
|  |       requestCount++; | ||
|  |       if (requestCount < 3) { | ||
|  |         res.status(500); | ||
|  |         res.end(); | ||
|  |       } else { | ||
|  |         res.status(200); | ||
|  |         res.send({ success: true, attempts: requestCount }); | ||
|  |       } | ||
|  |     }), | ||
|  |   ); | ||
|  | 
 | ||
|  |   // Route that returns 304 when ETag matches
 | ||
|  |   testServer.addRoute( | ||
|  |     '/conditional', | ||
|  |     new typedserver.servertools.Handler('GET', (req, res) => { | ||
|  |       const ifNoneMatch = req.headers['if-none-match']; | ||
|  |       if (ifNoneMatch === '"67890"') { | ||
|  |         res.status(304); | ||
|  |         res.end(); | ||
|  |       } else { | ||
|  |         res.setHeader('ETag', '"67890"'); | ||
|  |         res.status(200); | ||
|  |         res.send({ data: 'conditional response' }); | ||
|  |       } | ||
|  |     }), | ||
|  |   ); | ||
|  | 
 | ||
|  |   // POST route for testing
 | ||
|  |   testServer.addRoute( | ||
|  |     '/post', | ||
|  |     new typedserver.servertools.Handler('POST', (req, res) => { | ||
|  |       res.status(200); | ||
|  |       res.send({ received: true }); | ||
|  |     }), | ||
|  |   ); | ||
|  | 
 | ||
|  |   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<TestData>('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/nonexistent', { | ||
|  |     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 () => { | ||
|  |   const start = Date.now(); | ||
|  | 
 | ||
|  |   // 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/dynamic', { | ||
|  |       timeout: 1 // 1ms timeout should fail
 | ||
|  |     }); | ||
|  |     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); | ||
|  | }); | ||
|  | 
 | ||
|  | // Test 15: Backward compatibility with WebRequest class
 | ||
|  | tap.test('should maintain backward compatibility with v3 API', async () => { | ||
|  |   const { WebRequest } = await import('../ts/index.js'); | ||
|  |   const client = new WebRequest({ logging: false }); | ||
|  | 
 | ||
|  |   const data = await client.getJson('http://localhost:2346/dynamic'); | ||
|  |   expect(data).toHaveProperty('data'); | ||
|  | 
 | ||
|  |   // Test POST
 | ||
|  |   const postData = await client.postJson('http://localhost:2346/post', { | ||
|  |     test: 'data' | ||
|  |   }); | ||
|  |   expect(postData).toHaveProperty('received'); | ||
|  | }); | ||
|  | 
 | ||
|  | // Cleanup
 | ||
|  | tap.test('stop test server', async () => { | ||
|  |   await testServer.stop(); | ||
|  | }); | ||
|  | 
 | ||
|  | export default tap.start(); |