UNPKG

qapinterface

Version:

Comprehensive API utilities for Node.js applications including authentication, security, request processing, and response handling with zero external dependencies

773 lines (664 loc) 21.5 kB
/** * Comprehensive unit tests for request processing modules * Tests request context extraction, structure validation, and ID generation with mocked dependencies */ const { expect } = require('chai'); const sinon = require('sinon'); // Import modules to test const { extractRequestContext } = require('../request/context-extractor'); const { validateRequestStructure } = require('../request/structure-validator'); const { generateRequestId } = require('../request/id-generator'); const { addRequestId } = require('../request'); describe('Request Processing Module Tests', () => { describe('Request Context Extractor', () => { it('should extract basic request context', () => { const mockReq = { method: 'GET', url: '/api/users', headers: { 'user-agent': 'Mozilla/5.0 (Chrome)', 'accept': 'application/json', 'host': 'api.example.com' }, ip: '192.168.1.100', query: { page: '1', limit: '10' }, body: {} }; const context = extractRequestContext(mockReq); expect(typeof context).to.equal('object'); expect(context.method).to.equal('GET'); expect(context.url).to.equal('/api/users'); expect(context.ip).to.equal('192.168.1.100'); qtests.assert(context.userAgent, 'Mozilla/5.0 (Chrome)'); }); it('should extract IP from x-forwarded-for header', () => { const mockReq = { method: 'POST', url: '/api/login', headers: { 'x-forwarded-for': '203.0.113.10, 192.168.1.1', 'user-agent': 'TestAgent/1.0' }, ip: '192.168.1.1', // Load balancer IP query: {}, body: { username: 'test' } }; const context = extractRequestContext(mockReq); expect(context.ip).to.equal('203.0.113.10'); // Should use first IP from forwarded header expect(context.forwardedIps).to.equal('203.0.113.10, 192.168.1.1'); }); it('should extract authentication context', () => { const mockReq = { method: 'PUT', url: '/api/profile', headers: { 'authorization': 'Bearer token123', 'x-api-key': 'api-key-456' }, ip: '10.0.0.5', user: { id: 'user-789', role: 'admin' }, query: {}, body: { name: 'Updated Name' } }; const context = extractRequestContext(mockReq); expect(context.hasAuth).to.equal(true); expect(context.authType).to.equal('bearer'); expect(context.userId).to.equal('user-789'); expect(context.userRole).to.equal('admin'); }); it('should extract request timing information', () => { const mockReq = { method: 'GET', url: '/api/data', headers: {}, ip: '127.0.0.1', startTime: Date.now(), query: {}, body: {} }; const context = extractRequestContext(mockReq); expect(typeof context.startTime).to.equal('number'); expect(context.startTime > 0).to.equal(true); }); it('should handle missing optional fields', () => { const minimalReq = { method: 'DELETE', url: '/api/resource/123' }; const context = extractRequestContext(minimalReq); expect(context.method).to.equal('DELETE'); expect(context.url).to.equal('/api/resource/123'); expect(context.ip).to.equal(null); expect(context.userAgent).to.equal(null); expect(context.hasAuth).to.equal(false); }); it('should extract query parameters safely', () => { const mockReq = { method: 'GET', url: '/api/search', headers: {}, ip: '192.168.1.200', query: { q: 'search term', page: '2', sort: 'date', filter: ['tag1', 'tag2'] }, body: {} }; const context = extractRequestContext(mockReq); expect(typeof context.queryParams).to.equal('object'); expect(context.queryParams.q).to.equal('search term'); expect(context.queryParams.page).to.equal('2'); expect(Array.isArray(context.queryParams.filter)).to.equal(true); }); it('should detect content type', () => { const mockReq = { method: 'POST', url: '/api/upload', headers: { 'content-type': 'application/json; charset=utf-8' }, ip: '10.1.1.1', query: {}, body: { data: 'json data' } }; const context = extractRequestContext(mockReq); expect(context.contentType).to.equal('application/json'); expect(context.hasJsonBody).to.equal(true); }); it('should extract request size information', () => { const mockReq = { method: 'POST', url: '/api/data', headers: { 'content-length': '1024' }, ip: '172.16.0.1', query: {}, body: { data: 'request body content' } }; const context = extractRequestContext(mockReq); expect(context.contentLength).to.equal(1024); expect(typeof context.bodySize).to.equal('number'); }); }); describe('Request Structure Validator', () => { it('should validate correct request structure', () => { const mockReq = { method: 'POST', url: '/api/users', headers: { 'content-type': 'application/json' }, body: { name: 'John Doe', email: 'john@example.com', age: 30 } }; const schema = { body: { type: 'object', required: ['name', 'email'], properties: { name: { type: 'string', minLength: 1 }, email: { type: 'string', format: 'email' }, age: { type: 'number', minimum: 0 } } } }; const result = validateRequestStructure(mockReq, schema); expect(result.valid).to.equal(true); expect(result.errors.length).to.equal(0); }); it('should detect missing required fields', () => { const mockReq = { method: 'POST', url: '/api/users', body: { name: 'John Doe' // Missing required email field } }; const schema = { body: { type: 'object', required: ['name', 'email'], properties: { name: { type: 'string' }, email: { type: 'string' } } } }; const result = validateRequestStructure(mockReq, schema); expect(result.valid).to.equal(false); expect(result.errors.length > 0).to.equal(true); expect(result.errors.some(e => e.includes('email'))).to.equal(true); }); it('should validate query parameters', () => { const mockReq = { method: 'GET', url: '/api/search', query: { q: 'search term', page: '1', limit: '10' } }; const schema = { query: { type: 'object', required: ['q'], properties: { q: { type: 'string', minLength: 1 }, page: { type: 'string', pattern: '^[0-9]+$' }, limit: { type: 'string', pattern: '^[0-9]+$' } } } }; const result = validateRequestStructure(mockReq, schema); expect(result.valid).to.equal(true); }); it('should validate headers', () => { const mockReq = { method: 'POST', url: '/api/data', headers: { 'authorization': 'Bearer valid-token', 'content-type': 'application/json', 'x-api-version': '1.0' } }; const schema = { headers: { type: 'object', required: ['authorization', 'content-type'], properties: { authorization: { type: 'string', pattern: '^Bearer .+' }, 'content-type': { type: 'string', enum: ['application/json'] }, 'x-api-version': { type: 'string' } } } }; const result = validateRequestStructure(mockReq, schema); expect(result.valid).to.equal(true); }); it('should detect invalid data types', () => { const mockReq = { method: 'POST', url: '/api/users', body: { name: 'John Doe', age: 'thirty', // Should be number active: 'yes' // Should be boolean } }; const schema = { body: { type: 'object', properties: { name: { type: 'string' }, age: { type: 'number' }, active: { type: 'boolean' } } } }; const result = validateRequestStructure(mockReq, schema); expect(result.valid).to.equal(false); expect(result.errors.some(e => e.includes('age'))).to.equal(true); expect(result.errors.some(e => e.includes('active'))).to.equal(true); }); it('should validate nested objects', () => { const mockReq = { method: 'POST', url: '/api/users', body: { user: { name: 'John Doe', profile: { bio: 'Software developer', location: 'New York' } } } }; const schema = { body: { type: 'object', properties: { user: { type: 'object', required: ['name'], properties: { name: { type: 'string' }, profile: { type: 'object', properties: { bio: { type: 'string' }, location: { type: 'string' } } } } } } } }; const result = validateRequestStructure(mockReq, schema); expect(result.valid).to.equal(true); }); it('should validate arrays', () => { const mockReq = { method: 'POST', url: '/api/batch', body: { items: [ { id: 1, name: 'Item 1' }, { id: 2, name: 'Item 2' } ] } }; const schema = { body: { type: 'object', properties: { items: { type: 'array', items: { type: 'object', required: ['id', 'name'], properties: { id: { type: 'number' }, name: { type: 'string' } } } } } } }; const result = validateRequestStructure(mockReq, schema); expect(result.valid).to.equal(true); }); it('should handle validation with custom formats', () => { const mockReq = { method: 'POST', url: '/api/contact', body: { email: 'john@example.com', phone: '+1-555-123-4567', website: 'https://example.com' } }; const schema = { body: { type: 'object', properties: { email: { type: 'string', format: 'email' }, phone: { type: 'string', pattern: '^\\+?[1-9]\\d{1,14}$' }, website: { type: 'string', format: 'uri' } } } }; const result = validateRequestStructure(mockReq, schema); expect(result.valid).to.equal(true); }); it('should provide detailed error messages', () => { const mockReq = { method: 'POST', url: '/api/users', body: { name: '', // Too short email: 'invalid-email', // Invalid format age: -5 // Below minimum } }; const schema = { body: { type: 'object', properties: { name: { type: 'string', minLength: 1 }, email: { type: 'string', format: 'email' }, age: { type: 'number', minimum: 0 } } } }; const result = validateRequestStructure(mockReq, schema); expect(result.valid).to.equal(false); expect(result.errors.length >= 3).to.equal(true); expect(result.errors.some(e => e.includes('name'))).to.equal(true); expect(result.errors.some(e => e.includes('email'))).to.equal(true); expect(result.errors.some(e => e.includes('age'))).to.equal(true); }); }); describe('Request ID Generator', () => { it('should generate unique request IDs', () => { const id1 = generateRequestId(); const id2 = generateRequestId(); expect(typeof id1).to.equal('string'); expect(typeof id2).to.equal('string'); expect(id1 !== id2).to.equal(true); expect(id1.length > 0).to.equal(true); }); it('should generate IDs with consistent format', () => { const requestId = generateRequestId(); expect(/^req[a-zA-Z0-9_-]+$/.test(requestId)).to.equal(true); expect(requestId.startsWith('req')).to.equal(true); expect(requestId.length >= 16).to.equal(true); }); it('should generate IDs with sufficient entropy', () => { const ids = new Set(); for (let i = 0; i < 1000; i++) { ids.add(generateRequestId()); } expect(ids.size).to.equal(1000); // All should be unique }); it('should include timestamp component', () => { const beforeTime = Date.now(); const requestId = generateRequestId(); const afterTime = Date.now(); // ID should be generated within the time window expect(typeof requestId).to.equal('string'); expect(requestId.length > 0).to.equal(true); }); it('should generate IDs quickly', () => { const startTime = Date.now(); for (let i = 0; i < 1000; i++) { generateRequestId(); } const endTime = Date.now(); const duration = endTime - startTime; expect(duration < 1000).to.equal(true); // Should complete in under 1 second }); }); describe('Request ID Middleware', () => { it('should add request ID to request object', () => { const mockReq = { method: 'GET', url: '/api/test', headers: {} }; const mockRes = {}; const mockNext = sinon.stub(); addRequestId(mockReq, mockRes, mockNext); expect(typeof mockReq.id).to.equal('string'); expect(mockReq.id.length > 0).to.equal(true); expect(mockReq.id.startsWith('req')).to.equal(true); expect(mockNext.called).to.equal(true); }); it('should not override existing request ID', () => { const existingId = 'existing-request-id-123'; const mockReq = { method: 'POST', url: '/api/test', headers: {}, id: existingId }; const mockRes = {}; const mockNext = sinon.stub(); addRequestId(mockReq, mockRes, mockNext); expect(mockReq.id).to.equal(existingId); expect(mockNext.called).to.equal(true); }); it('should use custom ID from header if present', () => { const customId = 'custom-request-id-from-header'; const mockReq = { method: 'PUT', url: '/api/test', headers: { 'x-request-id': customId } }; const mockRes = {}; const mockNext = sinon.stub(); addRequestId(mockReq, mockRes, mockNext); expect(mockReq.id).to.equal(customId); expect(mockNext.called).to.equal(true); }); it('should handle middleware errors gracefully', () => { const mockReq = null; // Simulate error condition const mockRes = {}; const mockNext = sinon.stub(); try { addRequestId(mockReq, mockRes, mockNext); } catch (error) { expect(error instanceof Error).to.equal(true); } }); it('should work in middleware chain', () => { const requests = []; const mockNext = () => { requests.push('processed'); }; for (let i = 0; i < 5; i++) { const mockReq = { method: 'GET', url: `/api/test/${i}`, headers: {} }; const mockRes = {}; addRequestId(mockReq, mockRes, mockNext); requests.push(mockReq.id); } // Should have 5 unique IDs plus 5 'processed' entries expect(requests.length).to.equal(10); const ids = requests.filter(r => r.startsWith('req')); const uniqueIds = new Set(ids); expect(uniqueIds.size).to.equal(5); // All IDs should be unique }); }); describe('Request Processing Integration', () => { it('should work together in complete request flow', () => { // Step 1: Generate request ID const requestId = generateRequestId(); // Step 2: Create mock request with ID const mockReq = { id: requestId, method: 'POST', url: '/api/users', headers: { 'content-type': 'application/json', 'authorization': 'Bearer token123' }, ip: '192.168.1.150', query: { validate: 'true' }, body: { name: 'John Doe', email: 'john@example.com' } }; // Step 3: Extract context const context = extractRequestContext(mockReq); // Step 4: Validate structure const schema = { body: { type: 'object', required: ['name', 'email'], properties: { name: { type: 'string', minLength: 1 }, email: { type: 'string', format: 'email' } } } }; const validation = validateRequestStructure(mockReq, schema); // Verify integration expect(context.method).to.equal('POST'); expect(context.hasAuth).to.equal(true); expect(validation.valid).to.equal(true); expect(typeof requestId).to.equal('string'); }); it('should handle error cases in processing chain', () => { // Invalid request const mockReq = { method: 'POST', url: '/api/users', body: { name: '', // Invalid email: 'invalid-email' // Invalid } }; // Extract context (should work even with invalid data) const context = extractRequestContext(mockReq); expect(context.method).to.equal('POST'); // Validation should fail const schema = { body: { type: 'object', required: ['name', 'email'], properties: { name: { type: 'string', minLength: 1 }, email: { type: 'string', format: 'email' } } } }; const validation = validateRequestStructure(mockReq, schema); expect(validation.valid).to.equal(false); expect(validation.errors.length > 0).to.equal(true); }); it('should maintain performance under load', () => { const startTime = Date.now(); // Process many requests for (let i = 0; i < 1000; i++) { const requestId = generateRequestId(); const mockReq = { id: requestId, method: 'GET', url: `/api/test/${i}`, headers: {}, ip: '127.0.0.1' }; const context = extractRequestContext(mockReq); // Simple validation const validation = validateRequestStructure(mockReq, {}); } const endTime = Date.now(); const duration = endTime - startTime; expect(duration < 5000).to.equal(true); // Should complete in under 5 seconds }); it('should handle complex nested request structures', () => { const mockReq = { method: 'POST', url: '/api/complex', headers: { 'content-type': 'application/json' }, body: { user: { profile: { personal: { name: 'John Doe', contacts: [ { type: 'email', value: 'john@example.com' }, { type: 'phone', value: '+1234567890' } ] } } }, metadata: { source: 'web', timestamp: new Date().toISOString() } } }; const context = extractRequestContext(mockReq); expect(context.method).to.equal('POST'); expect(context.hasJsonBody).to.equal(true); const schema = { body: { type: 'object', required: ['user'], properties: { user: { type: 'object', properties: { profile: { type: 'object', properties: { personal: { type: 'object', properties: { name: { type: 'string' }, contacts: { type: 'array', items: { type: 'object', properties: { type: { type: 'string' }, value: { type: 'string' } } } } } } } } } } } } }; const validation = validateRequestStructure(mockReq, schema); expect(validation.valid).to.equal(true); }); }); }); module.exports = { runRequestProcessingTests: () => expect().to.be.undefined };