307 lines
8.9 KiB
TypeScript
307 lines
8.9 KiB
TypeScript
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<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/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();
|