Files
webrequest/test/test.v4.node.ts

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();