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
JavaScript
/**
* 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 };