Initial Version of sitemap.xml spec
This commit is contained in:
377
tests/unit/proxy-routing.test.js.old
Normal file
377
tests/unit/proxy-routing.test.js.old
Normal file
@@ -0,0 +1,377 @@
|
||||
/**
|
||||
* 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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user