193 lines
7.0 KiB
JavaScript
193 lines
7.0 KiB
JavaScript
/**
|
||
* Integration Tests: FIFO Queue Concurrency
|
||
*
|
||
* Test T029: Verify concurrent requests are processed in FIFO order (one at a time)
|
||
* Tests the request queue implementation for /sitemap.xml endpoint
|
||
*
|
||
* @module tests/integration/queue-concurrency
|
||
*/
|
||
|
||
import { describe, it, before, after } from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import http from 'node:http';
|
||
|
||
const TEST_PORT = 3001;
|
||
|
||
// =============================================================================
|
||
// T029: Integration test for concurrent requests (FIFO processing)
|
||
// =============================================================================
|
||
|
||
describe('T029: Concurrent Requests FIFO Processing', () => {
|
||
it('should process multiple concurrent requests in FIFO order (sequential)', async () => {
|
||
// Send multiple requests simultaneously
|
||
const requestCount = 5;
|
||
const startTime = Date.now();
|
||
const requests = [];
|
||
|
||
// Launch all requests at once
|
||
for (let i = 0; i < requestCount; i++) {
|
||
requests.push(makeTimedRequest(`http://localhost:${TEST_PORT}/sitemap.xml`, i));
|
||
}
|
||
|
||
// Wait for all requests to complete
|
||
const responses = await Promise.all(requests);
|
||
|
||
// Verify all requests succeeded
|
||
responses.forEach((response, index) => {
|
||
assert.equal(
|
||
response.statusCode,
|
||
200,
|
||
`Request ${index} should succeed with 200 OK`
|
||
);
|
||
});
|
||
|
||
// Verify sequential processing (FIFO)
|
||
// Each request should complete before the next starts
|
||
// If processed in parallel, total time ≈ single request time
|
||
// If processed sequentially, total time ≈ single request time × count
|
||
|
||
const totalElapsed = Date.now() - startTime;
|
||
const averageRequestTime = responses.reduce((sum, r) => sum + r.elapsed, 0) / responses.length;
|
||
|
||
// Sequential processing means total time should be close to sum of individual times
|
||
// Allow some overhead for queue management
|
||
const expectedMinTime = averageRequestTime * (requestCount - 1); // Allow first request to be instant
|
||
|
||
assert.ok(
|
||
totalElapsed >= expectedMinTime * 0.8, // 80% threshold for timing variability
|
||
`Total time (${totalElapsed}ms) should be close to sequential sum (${expectedMinTime}ms), indicating FIFO processing`
|
||
);
|
||
});
|
||
|
||
it('should maintain FIFO order: first request finishes before second starts processing', async () => {
|
||
// Track request processing order
|
||
const processingLog = [];
|
||
|
||
// Mock Drive API to log when each request is processed
|
||
// TODO: Add timing hooks in implementation to verify order
|
||
|
||
// Send two requests with small delay
|
||
const request1 = makeTimedRequest(`http://localhost:${TEST_PORT}/sitemap.xml`, 1);
|
||
|
||
// Small delay to ensure request 1 is queued first
|
||
await new Promise(resolve => setTimeout(resolve, 10));
|
||
|
||
const request2 = makeTimedRequest(`http://localhost:${TEST_PORT}/sitemap.xml`, 2);
|
||
|
||
const [response1, response2] = await Promise.all([request1, request2]);
|
||
|
||
// Both should succeed
|
||
assert.equal(response1.statusCode, 200, 'Request 1 should succeed');
|
||
assert.equal(response2.statusCode, 200, 'Request 2 should succeed');
|
||
|
||
// Request 1 should complete before request 2 starts processing
|
||
// Verify by checking that request 2 completion time > request 1 completion time
|
||
assert.ok(
|
||
response2.completedAt > response1.completedAt,
|
||
'Request 2 should complete after Request 1 (FIFO order)'
|
||
);
|
||
});
|
||
|
||
it('should only process one request at a time (no concurrent Drive API calls)', async () => {
|
||
// Send 3 requests simultaneously
|
||
const requests = [
|
||
makeTimedRequest(`http://localhost:${TEST_PORT}/sitemap.xml`, 1),
|
||
makeTimedRequest(`http://localhost:${TEST_PORT}/sitemap.xml`, 2),
|
||
makeTimedRequest(`http://localhost:${TEST_PORT}/sitemap.xml`, 3)
|
||
];
|
||
|
||
const responses = await Promise.all(requests);
|
||
|
||
// Verify all succeeded
|
||
responses.forEach((response, index) => {
|
||
assert.equal(response.statusCode, 200, `Request ${index + 1} should succeed`);
|
||
});
|
||
|
||
// Check that completion times don't overlap
|
||
// Sort responses by completion time
|
||
const sortedResponses = responses.sort((a, b) => a.completedAt - b.completedAt);
|
||
|
||
// Each request should complete before the next one starts
|
||
for (let i = 0; i < sortedResponses.length - 1; i++) {
|
||
const current = sortedResponses[i];
|
||
const next = sortedResponses[i + 1];
|
||
|
||
// Next request should start after current completes
|
||
// (Allow small timing variance)
|
||
assert.ok(
|
||
next.startedAt >= current.completedAt - 50, // 50ms tolerance for timing
|
||
`Request ${i + 2} should start after Request ${i + 1} completes (FIFO guarantee)`
|
||
);
|
||
}
|
||
});
|
||
|
||
it('should handle queue correctly when requests fail', async () => {
|
||
// Test scenario: Request 1 succeeds, Request 2 fails (e.g., Drive API error), Request 3 succeeds
|
||
// Queue should continue processing despite failures
|
||
|
||
// TODO: Mock Drive API to fail for specific request
|
||
|
||
const requests = [
|
||
makeTimedRequest(`http://localhost:${TEST_PORT}/sitemap.xml`, 1), // Should succeed
|
||
makeTimedRequest(`http://localhost:${TEST_PORT}/sitemap.xml`, 2), // Will fail (mock)
|
||
makeTimedRequest(`http://localhost:${TEST_PORT}/sitemap.xml`, 3) // Should succeed
|
||
];
|
||
|
||
const responses = await Promise.all(requests);
|
||
|
||
// Request 1 should succeed
|
||
assert.equal(responses[0].statusCode, 200, 'Request 1 should succeed');
|
||
|
||
// Request 2 should fail (mocked error)
|
||
// assert.notEqual(responses[1].statusCode, 200, 'Request 2 should fail');
|
||
|
||
// Request 3 should still succeed (queue continues)
|
||
assert.equal(responses[2].statusCode, 200, 'Request 3 should succeed despite Request 2 failure');
|
||
|
||
// All requests should still be processed in FIFO order
|
||
assert.ok(
|
||
responses[0].completedAt < responses[1].completedAt,
|
||
'Request 1 should complete before Request 2'
|
||
);
|
||
assert.ok(
|
||
responses[1].completedAt < responses[2].completedAt,
|
||
'Request 2 should complete before Request 3'
|
||
);
|
||
});
|
||
});
|
||
|
||
// =============================================================================
|
||
// Helper Functions
|
||
// =============================================================================
|
||
|
||
/**
|
||
* Make HTTP request and track timing
|
||
* @param {string} url - Full URL to request
|
||
* @param {number} requestId - Request identifier for logging
|
||
* @returns {Promise<Object>} Response with timing data
|
||
*/
|
||
function makeTimedRequest(url, requestId) {
|
||
const startedAt = Date.now();
|
||
|
||
return new Promise((resolve, reject) => {
|
||
http.get(url, (res) => {
|
||
let body = '';
|
||
res.on('data', chunk => body += chunk);
|
||
res.on('end', () => {
|
||
const completedAt = Date.now();
|
||
const elapsed = completedAt - startedAt;
|
||
|
||
resolve({
|
||
requestId,
|
||
statusCode: res.statusCode,
|
||
headers: res.headers,
|
||
body,
|
||
startedAt,
|
||
completedAt,
|
||
elapsed
|
||
});
|
||
});
|
||
}).on('error', reject);
|
||
});
|
||
}
|