qapinterface
Version:
Comprehensive API utilities for Node.js applications including authentication, security, request processing, and response handling with zero external dependencies
396 lines (329 loc) • 12.9 kB
JavaScript
/**
* Comprehensive unit tests for middleware modules
* Tests all middleware functionality with mocked Express objects
*/
const { expect } = require('chai');
const sinon = require('sinon');
// Import modules to test
const { createRequestProcessingMiddleware } = require('../middleware/request-processor');
const { securityMiddlewareStack } = require('../middleware/security-stack-creator');
const { handleValidationErrors } = require('../middleware/validation-error-handler');
const { requestLogger } = require('../middleware/request-logger');
const { errorLogger } = require('../middleware/error-logger');
describe('Middleware Module Tests', () => {
describe('Request Processing Middleware', () => {
it('should create request processing middleware', () => {
const middleware = createRequestProcessingMiddleware();
expect(typeof middleware).to.equal('function');
expect(middleware.length).to.equal(3); // req, res, next
});
it('should process requests with timing and ID generation', async () => {
const middleware = createRequestProcessingMiddleware();
const mockReq = {
method: 'GET',
url: '/api/test',
headers: { 'user-agent': 'test-agent' }
};
const mockRes = {
locals: {}
};
const mockNext = sinon.stub();
await middleware(mockReq, mockRes, mockNext);
expect(typeof mockReq.id).to.equal('string');
expect(mockReq.id.length > 0).to.equal(true);
expect(typeof mockReq.startTime).to.equal('number');
expect(mockNext.called).to.equal(true);
});
it('should handle errors gracefully', async () => {
const middleware = createRequestProcessingMiddleware();
const mockReq = null; // Simulate error condition
const mockRes = {};
const mockNext = sinon.stub();
try {
await middleware(mockReq, mockRes, mockNext);
} catch (error) {
expect(error instanceof Error).to.equal(true);
}
});
it('should extract request context', async () => {
const middleware = createRequestProcessingMiddleware();
const mockReq = {
method: 'POST',
url: '/api/users',
headers: {
'user-agent': 'Mozilla/5.0',
'x-forwarded-for': '192.168.1.100'
},
ip: '192.168.1.100',
body: { name: 'John Doe' }
};
const mockRes = { locals: {} };
const mockNext = sinon.stub();
await middleware(mockReq, mockRes, mockNext);
expect(typeof mockReq.context).to.equal('object');
expect(mockReq.context.method).to.equal('POST');
expect(mockReq.context.ip).to.equal('192.168.1.100');
});
});
describe('Security Middleware Stack', () => {
it('should create security middleware stack', () => {
const stack = securityMiddlewareStack();
expect(Array.isArray(stack)).to.equal(true);
expect(stack.length > 0).to.equal(true);
stack.forEach(middleware => {
expect(typeof middleware).to.equal('function');
});
});
it('should include helmet middleware', () => {
const stack = securityMiddlewareStack();
expect(stack.length >= 1).to.equal(true);
// First middleware should be helmet
expect(typeof stack[0]).to.equal('function');
});
it('should accept custom security options', () => {
const customOptions = {
csp: { 'script-src': ["'self'"] },
hsts: { maxAge: 31536000 }
};
const stack = securityMiddlewareStack(customOptions);
expect(Array.isArray(stack)).to.equal(true);
expect(stack.length > 0).to.equal(true);
});
it('should include rate limiting middleware', () => {
const stack = securityMiddlewareStack();
// Should include multiple security middleware
expect(stack.length >= 2).to.equal(true);
});
});
describe('Validation Error Handler', () => {
it('should handle validation errors', () => {
const mockErrors = [
{ field: 'email', message: 'Invalid email format' },
{ field: 'password', message: 'Password too short' }
];
const mockReq = { validationErrors: mockErrors };
const mockRes = {
status: sinon.stub().returnsThis(),
json: sinon.stub()
};
const mockNext = sinon.stub();
handleValidationErrors(mockReq, mockRes, mockNext);
expect(mockRes.status.calledWith(400)).to.equal(true);
expect(mockRes.json.called).to.equal(true);
expect(mockNext.notCalled).to.equal(true);
});
it('should proceed when no validation errors', () => {
const mockReq = {};
const mockRes = {
status: sinon.stub(),
json: sinon.stub()
};
const mockNext = sinon.stub();
handleValidationErrors(mockReq, mockRes, mockNext);
expect(mockRes.status.notCalled).to.equal(true);
expect(mockRes.json.notCalled).to.equal(true);
expect(mockNext.called).to.equal(true);
});
it('should handle empty validation errors array', () => {
const mockReq = { validationErrors: [] };
const mockRes = {
status: sinon.stub(),
json: sinon.stub()
};
const mockNext = sinon.stub();
handleValidationErrors(mockReq, mockRes, mockNext);
expect(mockNext.called).to.equal(true);
});
it('should format validation errors properly', () => {
const mockErrors = [
{ field: 'username', message: 'Username is required' }
];
const mockReq = { validationErrors: mockErrors };
const mockRes = {
status: sinon.stub().returnsThis(),
json: sinon.stub()
};
const mockNext = sinon.stub();
handleValidationErrors(mockReq, mockRes, mockNext);
const jsonCall = mockRes.json.getCall(0);
const response = jsonCall.args[0];
expect(response.success).to.equal(false);
expect(response.error.message).to.equal('Validation failed');
expect(Array.isArray(response.error.issues)).to.equal(true);
});
});
describe('Request Logger Middleware', () => {
it('should create request logger middleware', () => {
const middleware = requestLogger();
expect(typeof middleware).to.equal('function');
expect(middleware.length).to.equal(3); // req, res, next
});
it('should log request details', () => {
const originalLog = console.log;
let loggedData = '';
console.log = (data) => { loggedData += data; };
const middleware = requestLogger();
const mockReq = {
id: 'test-request-123',
method: 'GET',
url: '/api/test',
ip: '127.0.0.1',
headers: { 'user-agent': 'test-agent' }
};
const mockRes = {};
const mockNext = sinon.stub();
middleware(mockReq, mockRes, mockNext);
console.log = originalLog;
expect(loggedData.includes('test-request-123')).to.equal(true);
expect(loggedData.includes('GET')).to.equal(true);
expect(loggedData.includes('/api/test')).to.equal(true);
expect(mockNext.called).to.equal(true);
});
it('should handle requests without ID', () => {
const originalLog = console.log;
let logCalled = false;
console.log = () => { logCalled = true; };
const middleware = requestLogger();
const mockReq = {
method: 'POST',
url: '/api/users',
ip: '192.168.1.100'
};
const mockRes = {};
const mockNext = sinon.stub();
middleware(mockReq, mockRes, mockNext);
console.log = originalLog;
expect(logCalled).to.equal(true);
expect(mockNext.called).to.equal(true);
});
it('should log with timestamp', () => {
const originalLog = console.log;
let loggedData = '';
console.log = (data) => { loggedData += data; };
const middleware = requestLogger();
const mockReq = {
id: 'test-123',
method: 'GET',
url: '/test'
};
const mockRes = {};
const mockNext = sinon.stub();
middleware(mockReq, mockRes, mockNext);
console.log = originalLog;
// Should include timestamp format
expect(loggedData.includes(':')).to.equal(true);
expect(loggedData.includes('AM') || loggedData.includes('PM')).to.equal(true);
});
});
describe('Error Logger Middleware', () => {
it('should create error logger middleware', () => {
const middleware = errorLogger();
expect(typeof middleware).to.equal('function');
expect(middleware.length).to.equal(4); // err, req, res, next
});
it('should log error details', () => {
const originalError = console.error;
let loggedData = '';
console.error = (data) => { loggedData += data; };
const middleware = errorLogger();
const mockError = new Error('Test error message');
const mockReq = {
id: 'error-request-123',
method: 'POST',
url: '/api/error'
};
const mockRes = {};
const mockNext = sinon.stub();
middleware(mockError, mockReq, mockRes, mockNext);
console.error = originalError;
expect(loggedData.includes('Test error message')).to.equal(true);
expect(loggedData.includes('error-request-123')).to.equal(true);
expect(mockNext.calledWith(mockError)).to.equal(true);
});
it('should handle errors without request ID', () => {
const originalError = console.error;
let logCalled = false;
console.error = () => { logCalled = true; };
const middleware = errorLogger();
const mockError = new Error('No ID error');
const mockReq = { method: 'GET', url: '/test' };
const mockRes = {};
const mockNext = sinon.stub();
middleware(mockError, mockReq, mockRes, mockNext);
console.error = originalError;
expect(logCalled).to.equal(true);
expect(mockNext.called).to.equal(true);
});
it('should log stack trace for debugging', () => {
const originalError = console.error;
let loggedData = '';
console.error = (data) => { loggedData += data; };
const middleware = errorLogger();
const mockError = new Error('Stack trace test');
mockError.stack = 'Error: Stack trace test\n at test.js:1:1';
const mockReq = { id: 'stack-test' };
const mockRes = {};
const mockNext = sinon.stub();
middleware(mockError, mockReq, mockRes, mockNext);
console.error = originalError;
expect(loggedData.includes('Stack trace test')).to.equal(true);
expect(loggedData.includes('test.js:1:1')).to.equal(true);
});
it('should include request context in error logs', () => {
const originalError = console.error;
let loggedData = '';
console.error = (data) => { loggedData += data; };
const middleware = errorLogger();
const mockError = new Error('Context test');
const mockReq = {
id: 'context-test-123',
method: 'PUT',
url: '/api/update',
ip: '10.0.0.1'
};
const mockRes = {};
const mockNext = sinon.stub();
middleware(mockError, mockReq, mockRes, mockNext);
console.error = originalError;
expect(loggedData.includes('PUT')).to.equal(true);
expect(loggedData.includes('/api/update')).to.equal(true);
expect(loggedData.includes('10.0.0.1')).to.equal(true);
});
});
describe('Middleware Integration', () => {
it('should work together in middleware stack', async () => {
const requestProcessor = createRequestProcessingMiddleware();
const validationHandler = handleValidationErrors;
const reqLogger = requestLogger();
const mockReq = {
method: 'GET',
url: '/api/integration-test',
headers: {}
};
const mockRes = { locals: {} };
let nextCallCount = 0;
const mockNext = () => { nextCallCount++; };
// Process through middleware stack
await requestProcessor(mockReq, mockRes, mockNext);
validationHandler(mockReq, mockRes, mockNext);
reqLogger(mockReq, mockRes, mockNext);
expect(nextCallCount).to.equal(3);
expect(typeof mockReq.id).to.equal('string');
});
it('should handle errors through middleware chain', () => {
const originalError = console.error;
let errorLogged = false;
console.error = () => { errorLogged = true; };
const errorLoggerMiddleware = errorLogger();
const mockError = new Error('Chain error test');
const mockReq = { id: 'chain-test' };
const mockRes = {};
const mockNext = sinon.stub();
errorLoggerMiddleware(mockError, mockReq, mockRes, mockNext);
console.error = originalError;
expect(errorLogged).to.equal(true);
expect(mockNext.calledWith(mockError)).to.equal(true);
});
});
});
module.exports = { runMiddlewareTests: () => expect().to.be.undefined };