/** * Unit Tests: Request Routing Logic * * Tests request routing and error mapping in proxy.js * Tests T015, T016, T050 */ import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; describe('Unit: handleRequest() Routing (T015)', () => { // Mock routing function (will be in proxy.js) function parseRoute(method, url) { if (method !== 'GET') { return { route: null, error: 'Method not allowed', statusCode: 405 }; } const urlObj = new URL(url, 'http://localhost'); const path = urlObj.pathname; if (path === '/health') { return { route: 'health' }; } if (path === '/sitemap.xml') { return { route: 'sitemap' }; } // Document route: /:documentId const docMatch = path.match(/^\/([a-zA-Z0-9_-]+)$/); if (docMatch) { return { route: 'document', documentId: docMatch[1] }; } return { route: null, error: 'Not found', statusCode: 404 }; } it('T015: should route /health to health check handler', () => { // Given: GET request to /health const method = 'GET'; const url = '/health'; // When: Parsing route const result = parseRoute(method, url); // Then: Should route to health assert.equal(result.route, 'health', 'Should route to health handler'); }); it('T015: should route /:documentId to document export handler', () => { // Given: GET request to /:documentId const method = 'GET'; const url = '/1BxAA_testDocument123'; // When: Parsing route const result = parseRoute(method, url); // Then: Should route to document handler assert.equal(result.route, 'document', 'Should route to document handler'); assert.equal(result.documentId, '1BxAA_testDocument123', 'Should extract document ID'); }); it('T015: should route /sitemap.xml to sitemap handler', () => { // Given: GET request to /sitemap.xml const method = 'GET'; const url = '/sitemap.xml'; // When: Parsing route const result = parseRoute(method, url); // Then: Should route to sitemap assert.equal(result.route, 'sitemap', 'Should route to sitemap handler'); }); it('T015: should return 404 for unknown routes', () => { // Given: GET request to unknown path const method = 'GET'; const url = '/unknown/path'; // When: Parsing route const result = parseRoute(method, url); // Then: Should return 404 assert.equal(result.route, null, 'Should not match any route'); assert.equal(result.statusCode, 404, 'Should return 404 status'); }); it('T015: should return 405 for non-GET methods', () => { // Given: POST request const method = 'POST'; const url = '/1BxAA_test'; // When: Parsing route const result = parseRoute(method, url); // Then: Should return 405 Method Not Allowed assert.equal(result.route, null, 'Should not match any route'); assert.equal(result.statusCode, 405, 'Should return 405 status'); }); it('T015: should extract documentId with hyphens and underscores', () => { // Given: Document ID with special allowed characters const urls = [ '/1BxAA-test-123', '/1BxAA_test_123', '/1BxAA-test_123' ]; // When: Parsing each route // Then: Should extract document IDs correctly urls.forEach(url => { const result = parseRoute('GET', url); assert.equal(result.route, 'document', `Should route ${url} to document handler`); assert.ok(result.documentId, `Should extract document ID from ${url}`); }); }); }); describe('Unit: mapDriveError() (T016)', () => { // Mock error mapping function (will be in proxy.js) function mapDriveError(error) { // Handle GaxiosError from googleapis const statusCode = error.code || error.response?.status || 500; const mapping = { 404: { status: 404, message: 'Not Found' }, 403: { status: 403, message: 'Forbidden' }, 401: { status: 401, message: 'Unauthorized' }, 429: { status: 429, message: 'Too Many Requests', retryAfter: 60 }, 500: { status: 500, message: 'Internal Server Error' }, 503: { status: 503, message: 'Service Unavailable' } }; return mapping[statusCode] || { status: 500, message: 'Internal Server Error' }; } it('T016: should convert Drive API 404 to HTTP 404', () => { // Given: Drive API 404 error const driveError = { code: 404, message: 'File not found' }; // When: Mapping error const result = mapDriveError(driveError); // Then: Should map to HTTP 404 assert.equal(result.status, 404, 'Should map to 404 status'); }); it('T016: should convert Drive API 403 to HTTP 403', () => { // Given: Drive API 403 error const driveError = { code: 403, message: 'Permission denied' }; // When: Mapping error const result = mapDriveError(driveError); // Then: Should map to HTTP 403 assert.equal(result.status, 403, 'Should map to 403 status'); }); it('T016: should convert Drive API 401 to HTTP 401', () => { // Given: Drive API 401 error const driveError = { code: 401, message: 'Invalid credentials' }; // When: Mapping error const result = mapDriveError(driveError); // Then: Should map to HTTP 401 assert.equal(result.status, 401, 'Should map to 401 status'); }); it('T016: should convert Drive API 429 to HTTP 429 with Retry-After', () => { // Given: Drive API rate limit error const driveError = { code: 429, message: 'Rate limit exceeded' }; // When: Mapping error const result = mapDriveError(driveError); // Then: Should map to HTTP 429 with Retry-After assert.equal(result.status, 429, 'Should map to 429 status'); assert.equal(result.retryAfter, 60, 'Should include Retry-After of 60 seconds'); }); it('T016: should convert Drive API 500 to HTTP 500', () => { // Given: Drive API internal error const driveError = { code: 500, message: 'Internal error' }; // When: Mapping error const result = mapDriveError(driveError); // Then: Should map to HTTP 500 assert.equal(result.status, 500, 'Should map to 500 status'); }); it('T016: should convert Drive API 503 to HTTP 503', () => { // Given: Drive API service unavailable const driveError = { code: 503, message: 'Service unavailable' }; // When: Mapping error const result = mapDriveError(driveError); // Then: Should map to HTTP 503 assert.equal(result.status, 503, 'Should map to 503 status'); }); it('should handle errors without code by checking response.status', () => { // Given: Error with response.status instead of code const driveError = { response: { status: 404, statusText: 'Not Found' }, message: 'Request failed' }; // When: Mapping error const result = mapDriveError(driveError); // Then: Should map using response.status assert.equal(result.status, 404, 'Should map using response.status'); }); it('should default to 500 for unknown error codes', () => { // Given: Error with unknown status code const driveError = { code: 999, message: 'Unknown error' }; // When: Mapping error const result = mapDriveError(driveError); // Then: Should default to 500 assert.equal(result.status, 500, 'Should default to 500 for unknown codes'); }); }); describe('Unit: Rate Limiting (T050)', () => { // Mock rate limiter (will be in proxy.js) class RateLimiter { constructor(maxRequests = 100, windowMs = 60000) { this.maxRequests = maxRequests; this.windowMs = windowMs; this.requests = new Map(); // ip -> [timestamps] } checkLimit(ip) { const now = Date.now(); const windowStart = now - this.windowMs; // Get existing requests for this IP let timestamps = this.requests.get(ip) || []; // Remove old timestamps outside window timestamps = timestamps.filter(ts => ts > windowStart); // Check if limit exceeded if (timestamps.length >= this.maxRequests) { const oldestRequest = timestamps[0]; const retryAfter = Math.ceil((oldestRequest + this.windowMs - now) / 1000); return { allowed: false, statusCode: 429, retryAfter }; } // Add current request timestamps.push(now); this.requests.set(ip, timestamps); return { allowed: true }; } cleanup() { const now = Date.now(); const windowStart = now - this.windowMs; for (const [ip, timestamps] of this.requests.entries()) { const filtered = timestamps.filter(ts => ts > windowStart); if (filtered.length === 0) { this.requests.delete(ip); } else { this.requests.set(ip, filtered); } } } } it('T050: should allow 100 requests from same IP within window', () => { // Given: Rate limiter with 100 req/min limit const limiter = new RateLimiter(100, 60000); const testIp = '192.168.1.1'; // When: Making 100 requests let allowedCount = 0; for (let i = 0; i < 100; i++) { const result = limiter.checkLimit(testIp); if (result.allowed) allowedCount++; } // Then: All 100 requests should be allowed assert.equal(allowedCount, 100, 'Should allow 100 requests'); }); it('T050: should return 429 with Retry-After header on 101st request', () => { // Given: Rate limiter with 100 req/min limit const limiter = new RateLimiter(100, 60000); const testIp = '192.168.1.1'; // When: Making 101 requests for (let i = 0; i < 100; i++) { limiter.checkLimit(testIp); } const result = limiter.checkLimit(testIp); // Then: 101st request should be rate limited assert.equal(result.allowed, false, 'Should not allow 101st request'); assert.equal(result.statusCode, 429, 'Should return 429 status'); assert.ok(result.retryAfter > 0, 'Should include Retry-After in seconds'); assert.ok(result.retryAfter <= 60, 'Retry-After should be <= 60 seconds'); }); it('T050: should track requests per IP independently', () => { // Given: Rate limiter and multiple IPs const limiter = new RateLimiter(100, 60000); const ip1 = '192.168.1.1'; const ip2 = '192.168.1.2'; // When: Making 100 requests from each IP for (let i = 0; i < 100; i++) { limiter.checkLimit(ip1); limiter.checkLimit(ip2); } // Then: Both IPs should still be allowed (independent limits) const result1 = limiter.checkLimit(ip1); const result2 = limiter.checkLimit(ip2); assert.equal(result1.allowed, false, 'IP1 should be rate limited'); assert.equal(result2.allowed, false, 'IP2 should be rate limited'); }); it('T050: should cleanup old entries outside time window', () => { // Given: Rate limiter with short window const limiter = new RateLimiter(10, 1000); // 10 req/sec for testing const testIp = '192.168.1.1'; // When: Making requests then cleaning up for (let i = 0; i < 10; i++) { limiter.checkLimit(testIp); } // Wait for window to pass (simulate with manual cleanup) limiter.cleanup(); // Then: Should have entries in map assert.ok(limiter.requests.has(testIp), 'Should have IP in requests map'); }); it('T050: should reset limit after time window expires', () => { // Given: Rate limiter with very short window const limiter = new RateLimiter(5, 100); // 5 req / 100ms const testIp = '192.168.1.1'; // When: Filling up limit for (let i = 0; i < 5; i++) { limiter.checkLimit(testIp); } // Simulate time passing by manipulating timestamps const oldTimestamps = limiter.requests.get(testIp); const expiredTimestamps = oldTimestamps.map(ts => ts - 200); // Make them 200ms old limiter.requests.set(testIp, expiredTimestamps); // Then: New request should be allowed after window const result = limiter.checkLimit(testIp); assert.equal(result.allowed, true, 'Should allow request after window expires'); }); });