assign042-idem-1788856435
a8c741a9d7
- 实现 Express.js 幂等键中间件,支持 Idempotency-Key 请求头 - 缓存成功响应(2xx)24小时,重复请求直接返回缓存结果 - 处理并发重复请求(in-flight 去重) - 错误响应不缓存,允许客户端重试 - 实现 POST /api/orders 订单创建端点演示幂等性 - 添加 7 组共 21 个测试用例,全部通过
163 行
6.0 KiB
JavaScript
163 行
6.0 KiB
JavaScript
/**
|
|
* Tests for Idempotency Key Middleware
|
|
*
|
|
* Verifies:
|
|
* 1. First request with a new idempotency key creates a resource
|
|
* 2. Duplicate request with same key returns cached response (no duplicate creation)
|
|
* 3. Different keys create different resources
|
|
* 4. GET requests are not affected by idempotency middleware
|
|
* 5. Missing key allows normal processing
|
|
*/
|
|
|
|
const http = require('http');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const app = require('../src/server');
|
|
|
|
const PORT = 0; // Let the OS assign a port
|
|
let server;
|
|
let baseUrl;
|
|
|
|
function makeRequest(method, path, headers = {}, body = null) {
|
|
return new Promise((resolve, reject) => {
|
|
const url = new URL(path, baseUrl);
|
|
const options = {
|
|
method,
|
|
hostname: url.hostname,
|
|
port: url.port,
|
|
path: url.pathname + url.search,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
...headers
|
|
}
|
|
};
|
|
|
|
if (body) {
|
|
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
|
options.headers['Content-Length'] = Buffer.byteLength(bodyStr);
|
|
}
|
|
|
|
const req = http.request(options, (res) => {
|
|
let data = '';
|
|
res.on('data', chunk => data += chunk);
|
|
res.on('end', () => {
|
|
try {
|
|
resolve({ statusCode: res.statusCode, headers: res.headers, body: JSON.parse(data) });
|
|
} catch {
|
|
resolve({ statusCode: res.statusCode, headers: res.headers, body: data });
|
|
}
|
|
});
|
|
});
|
|
|
|
req.on('error', reject);
|
|
|
|
if (body) {
|
|
req.write(typeof body === 'string' ? body : JSON.stringify(body));
|
|
}
|
|
req.end();
|
|
});
|
|
}
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
function assert(condition, message) {
|
|
if (condition) {
|
|
console.log(` ✓ ${message}`);
|
|
passed++;
|
|
} else {
|
|
console.log(` ✗ ${message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
async function runTests() {
|
|
await new Promise(resolve => {
|
|
server = app.listen(PORT, () => {
|
|
const addr = server.address();
|
|
baseUrl = `http://localhost:${addr.port}`;
|
|
resolve();
|
|
});
|
|
});
|
|
|
|
console.log('Running idempotency tests...\n');
|
|
|
|
// Test 1: First request creates an order
|
|
console.log('Test 1: First request with idempotency key creates order');
|
|
const key1 = uuidv4();
|
|
const orderData = {
|
|
customerId: 'cust-test-1',
|
|
items: [{ productId: 'prod-A', quantity: 2, price: 50 }]
|
|
};
|
|
|
|
const res1 = await makeRequest('POST', '/api/orders', { 'Idempotency-Key': key1 }, orderData);
|
|
assert(res1.statusCode === 201, 'First request returns 201 Created');
|
|
assert(res1.body.order !== undefined, 'Response contains order object');
|
|
assert(res1.body.replayed === false, 'First request is not a replay');
|
|
assert(res1.body.order.customerId === 'cust-test-1', 'Order has correct customer ID');
|
|
assert(res1.body.order.total === 100, 'Order total is calculated correctly');
|
|
const orderId1 = res1.body.order.id;
|
|
|
|
// Test 2: Duplicate request with same key returns cached response
|
|
console.log('\nTest 2: Duplicate request returns cached response (no duplicate creation)');
|
|
const res2 = await makeRequest('POST', '/api/orders', { 'Idempotency-Key': key1 }, orderData);
|
|
assert(res2.statusCode === 201, 'Duplicate request returns same status code (201)');
|
|
assert(res2.body.order.id === orderId1, 'Duplicate returns same order ID');
|
|
assert(res2.headers['x-idempotency-replay'] === 'true', 'Replay header is set');
|
|
assert(res2.body.replayed === true, 'Response indicates replay');
|
|
|
|
// Test 3: Verify only one order was created
|
|
console.log('\nTest 3: Only one order exists after duplicate requests');
|
|
const res3 = await makeRequest('GET', '/api/orders');
|
|
assert(res3.statusCode === 200, 'GET orders returns 200');
|
|
assert(res3.body.count === 1, 'Only one order in the system');
|
|
|
|
// Test 4: Different key creates a new order
|
|
console.log('\nTest 4: Different idempotency key creates new order');
|
|
const key2 = uuidv4();
|
|
const res4 = await makeRequest('POST', '/api/orders', { 'Idempotency-Key': key2 }, orderData);
|
|
assert(res4.statusCode === 201, 'New key returns 201');
|
|
assert(res4.body.order.id !== orderId1, 'New key creates different order ID');
|
|
assert(res4.body.replayed === false, 'New key is not a replay');
|
|
|
|
// Verify we now have 2 orders
|
|
const res4b = await makeRequest('GET', '/api/orders');
|
|
assert(res4b.body.count === 2, 'Two orders exist after second unique key');
|
|
|
|
// Test 5: GET requests are not affected by idempotency
|
|
console.log('\nTest 5: GET requests are not idempotency-protected');
|
|
const res5a = await makeRequest('GET', `/api/orders/${orderId1}`);
|
|
const res5b = await makeRequest('GET', `/api/orders/${orderId1}`, { 'Idempotency-Key': key1 });
|
|
assert(res5a.statusCode === 200, 'GET returns 200');
|
|
assert(!res5b.headers['x-idempotency-replay'], 'GET does not set replay header even with key');
|
|
|
|
// Test 6: Request without key processes normally
|
|
console.log('\nTest 6: Request without idempotency key proceeds normally');
|
|
const res6 = await makeRequest('POST', '/api/orders', {}, orderData);
|
|
assert(res6.statusCode === 201, 'Request without key returns 201');
|
|
assert(!res6.headers['x-idempotency-replay'], 'No replay header on keyless request');
|
|
|
|
// Test 7: Validation errors are not cached
|
|
console.log('\nTest 7: Validation errors are not cached');
|
|
const key3 = uuidv4();
|
|
const invalidData = { customerId: 'cust-bad' }; // Missing items
|
|
const res7a = await makeRequest('POST', '/api/orders', { 'Idempotency-Key': key3 }, invalidData);
|
|
assert(res7a.statusCode === 400, 'Invalid request returns 400');
|
|
|
|
// Same key with valid data should succeed (error wasn't cached)
|
|
const res7b = await makeRequest('POST', '/api/orders', { 'Idempotency-Key': key3 }, orderData);
|
|
assert(res7b.statusCode === 201, 'Same key with valid data succeeds (error not cached)');
|
|
|
|
console.log(`\n${'='.repeat(50)}`);
|
|
console.log(`Results: ${passed} passed, ${failed} failed`);
|
|
console.log(`${'='.repeat(50)}`);
|
|
|
|
server.close();
|
|
process.exit(failed > 0 ? 1 : 0);
|
|
}
|
|
|
|
runTests().catch(err => {
|
|
console.error('Test error:', err);
|
|
if (server) server.close();
|
|
process.exit(1);
|
|
});
|