317 lines
8.8 KiB
TypeScript
317 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 always fails with 500 (for fallback testing)
|
|
testServer.addRoute(
|
|
'/always-fails',
|
|
new typedserver.servertools.Handler('GET', (req, res) => {
|
|
res.status(500);
|
|
res.end();
|
|
}),
|
|
);
|
|
|
|
// Route that takes a long time to respond (for timeout testing)
|
|
testServer.addRoute(
|
|
'/slow',
|
|
new typedserver.servertools.Handler('GET', async (req, res) => {
|
|
await new Promise((resolve) => setTimeout(resolve, 5000));
|
|
res.status(200);
|
|
res.send({ data: 'slow response' });
|
|
}),
|
|
);
|
|
|
|
// 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/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 () => {
|
|
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/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();
|