backend unit tests
This commit is contained in:
723
backend/tests/unit/middleware/security.test.js
Normal file
723
backend/tests/unit/middleware/security.test.js
Normal file
@@ -0,0 +1,723 @@
|
||||
const {
|
||||
enforceHTTPS,
|
||||
securityHeaders,
|
||||
addRequestId,
|
||||
logSecurityEvent,
|
||||
sanitizeError
|
||||
} = require('../../../middleware/security');
|
||||
|
||||
// Mock crypto module
|
||||
jest.mock('crypto', () => ({
|
||||
randomBytes: jest.fn(() => ({
|
||||
toString: jest.fn(() => 'mocked-hex-string-1234567890abcdef')
|
||||
}))
|
||||
}));
|
||||
|
||||
describe('Security Middleware', () => {
|
||||
let req, res, next, consoleSpy, consoleWarnSpy, consoleErrorSpy;
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
secure: false,
|
||||
headers: {},
|
||||
protocol: 'http',
|
||||
url: '/test-path',
|
||||
ip: '127.0.0.1',
|
||||
connection: { remoteAddress: '127.0.0.1' },
|
||||
get: jest.fn(),
|
||||
user: null
|
||||
};
|
||||
res = {
|
||||
redirect: jest.fn(),
|
||||
setHeader: jest.fn(),
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn()
|
||||
};
|
||||
next = jest.fn();
|
||||
|
||||
// Mock console methods
|
||||
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
||||
consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
|
||||
consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
|
||||
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
consoleSpy.mockRestore();
|
||||
consoleWarnSpy.mockRestore();
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
|
||||
describe('enforceHTTPS', () => {
|
||||
describe('Development environment', () => {
|
||||
it('should skip HTTPS enforcement in dev environment', () => {
|
||||
process.env.NODE_ENV = 'dev';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(res.setHeader).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should skip HTTPS enforcement in development environment', () => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(res.setHeader).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Production environment', () => {
|
||||
beforeEach(() => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
process.env.FRONTEND_URL = 'example.com';
|
||||
});
|
||||
|
||||
describe('HTTPS detection', () => {
|
||||
it('should detect HTTPS from req.secure', () => {
|
||||
req.secure = true;
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(res.setHeader).toHaveBeenCalledWith(
|
||||
'Strict-Transport-Security',
|
||||
'max-age=31536000; includeSubDomains; preload'
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect HTTPS from x-forwarded-proto header', () => {
|
||||
req.headers['x-forwarded-proto'] = 'https';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(res.setHeader).toHaveBeenCalledWith(
|
||||
'Strict-Transport-Security',
|
||||
'max-age=31536000; includeSubDomains; preload'
|
||||
);
|
||||
});
|
||||
|
||||
it('should detect HTTPS from req.protocol', () => {
|
||||
req.protocol = 'https';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
expect(res.redirect).not.toHaveBeenCalled();
|
||||
expect(res.setHeader).toHaveBeenCalledWith(
|
||||
'Strict-Transport-Security',
|
||||
'max-age=31536000; includeSubDomains; preload'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('HTTP to HTTPS redirect', () => {
|
||||
it('should redirect HTTP requests to HTTPS', () => {
|
||||
req.headers.host = 'example.com';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle requests with query parameters', () => {
|
||||
req.url = '/test-path?param=value';
|
||||
req.headers.host = 'example.com';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path?param=value');
|
||||
});
|
||||
|
||||
it('should log warning for host header mismatch', () => {
|
||||
req.headers.host = 'malicious.com';
|
||||
req.ip = '192.168.1.1';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(consoleWarnSpy).toHaveBeenCalledWith(
|
||||
'[SECURITY] Host header mismatch during HTTPS redirect:',
|
||||
{
|
||||
requestHost: 'malicious.com',
|
||||
allowedHost: 'example.com',
|
||||
ip: '192.168.1.1',
|
||||
url: '/test-path'
|
||||
}
|
||||
);
|
||||
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
|
||||
});
|
||||
|
||||
it('should not log warning when host matches allowed host', () => {
|
||||
req.headers.host = 'example.com';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(consoleWarnSpy).not.toHaveBeenCalled();
|
||||
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
|
||||
});
|
||||
|
||||
it('should use FRONTEND_URL as allowed host', () => {
|
||||
process.env.FRONTEND_URL = 'secure-site.com';
|
||||
req.headers.host = 'different.com';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(res.redirect).toHaveBeenCalledWith(301, 'https://secure-site.com/test-path');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle missing host header', () => {
|
||||
delete req.headers.host;
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/test-path');
|
||||
});
|
||||
|
||||
it('should handle empty URL', () => {
|
||||
req.url = '';
|
||||
req.headers.host = 'example.com';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com');
|
||||
});
|
||||
|
||||
it('should handle root path', () => {
|
||||
req.url = '/';
|
||||
req.headers.host = 'example.com';
|
||||
|
||||
enforceHTTPS(req, res, next);
|
||||
|
||||
expect(res.redirect).toHaveBeenCalledWith(301, 'https://example.com/');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('securityHeaders', () => {
|
||||
it('should set X-Content-Type-Options header', () => {
|
||||
securityHeaders(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith('X-Content-Type-Options', 'nosniff');
|
||||
});
|
||||
|
||||
it('should set X-Frame-Options header', () => {
|
||||
securityHeaders(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith('X-Frame-Options', 'DENY');
|
||||
});
|
||||
|
||||
it('should set Referrer-Policy header', () => {
|
||||
securityHeaders(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith('Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
});
|
||||
|
||||
it('should set Permissions-Policy header', () => {
|
||||
securityHeaders(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledWith(
|
||||
'Permissions-Policy',
|
||||
'camera=(), microphone=(), geolocation=(self)'
|
||||
);
|
||||
});
|
||||
|
||||
it('should call next after setting headers', () => {
|
||||
securityHeaders(req, res, next);
|
||||
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set all security headers in one call', () => {
|
||||
securityHeaders(req, res, next);
|
||||
|
||||
expect(res.setHeader).toHaveBeenCalledTimes(4);
|
||||
expect(res.setHeader).toHaveBeenNthCalledWith(1, 'X-Content-Type-Options', 'nosniff');
|
||||
expect(res.setHeader).toHaveBeenNthCalledWith(2, 'X-Frame-Options', 'DENY');
|
||||
expect(res.setHeader).toHaveBeenNthCalledWith(3, 'Referrer-Policy', 'strict-origin-when-cross-origin');
|
||||
expect(res.setHeader).toHaveBeenNthCalledWith(4, 'Permissions-Policy', 'camera=(), microphone=(), geolocation=(self)');
|
||||
});
|
||||
});
|
||||
|
||||
describe('addRequestId', () => {
|
||||
const crypto = require('crypto');
|
||||
|
||||
it('should generate and set request ID', () => {
|
||||
addRequestId(req, res, next);
|
||||
|
||||
expect(crypto.randomBytes).toHaveBeenCalledWith(16);
|
||||
expect(req.id).toBe('mocked-hex-string-1234567890abcdef');
|
||||
expect(res.setHeader).toHaveBeenCalledWith('X-Request-ID', 'mocked-hex-string-1234567890abcdef');
|
||||
expect(next).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should generate unique IDs for different requests', () => {
|
||||
const mockRandomBytes = require('crypto').randomBytes;
|
||||
|
||||
// First call
|
||||
mockRandomBytes.mockReturnValueOnce({
|
||||
toString: jest.fn(() => 'first-request-id')
|
||||
});
|
||||
|
||||
addRequestId(req, res, next);
|
||||
expect(req.id).toBe('first-request-id');
|
||||
|
||||
// Reset for second call
|
||||
jest.clearAllMocks();
|
||||
const req2 = { ...req };
|
||||
const res2 = { ...res, setHeader: jest.fn() };
|
||||
const next2 = jest.fn();
|
||||
|
||||
// Second call
|
||||
mockRandomBytes.mockReturnValueOnce({
|
||||
toString: jest.fn(() => 'second-request-id')
|
||||
});
|
||||
|
||||
addRequestId(req2, res2, next2);
|
||||
expect(req2.id).toBe('second-request-id');
|
||||
});
|
||||
|
||||
it('should call toString with hex parameter', () => {
|
||||
const mockToString = jest.fn(() => 'hex-string');
|
||||
require('crypto').randomBytes.mockReturnValueOnce({
|
||||
toString: mockToString
|
||||
});
|
||||
|
||||
addRequestId(req, res, next);
|
||||
|
||||
expect(mockToString).toHaveBeenCalledWith('hex');
|
||||
});
|
||||
});
|
||||
|
||||
describe('logSecurityEvent', () => {
|
||||
beforeEach(() => {
|
||||
req.id = 'test-request-id';
|
||||
req.ip = '192.168.1.1';
|
||||
req.get = jest.fn((header) => {
|
||||
if (header === 'user-agent') return 'Mozilla/5.0 Test Browser';
|
||||
return null;
|
||||
});
|
||||
});
|
||||
|
||||
describe('Production environment', () => {
|
||||
beforeEach(() => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
});
|
||||
|
||||
it('should log security event with JSON format', () => {
|
||||
const eventType = 'LOGIN_ATTEMPT';
|
||||
const details = { username: 'testuser', success: false };
|
||||
|
||||
logSecurityEvent(eventType, details, req);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('[SECURITY]', expect.any(String));
|
||||
|
||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
||||
expect(loggedData).toEqual({
|
||||
timestamp: expect.any(String),
|
||||
eventType: 'LOGIN_ATTEMPT',
|
||||
requestId: 'test-request-id',
|
||||
ip: '192.168.1.1',
|
||||
userAgent: 'Mozilla/5.0 Test Browser',
|
||||
userId: 'anonymous',
|
||||
username: 'testuser',
|
||||
success: false
|
||||
});
|
||||
});
|
||||
|
||||
it('should include user ID when user is authenticated', () => {
|
||||
req.user = { id: 123 };
|
||||
const eventType = 'DATA_ACCESS';
|
||||
const details = { resource: '/api/users' };
|
||||
|
||||
logSecurityEvent(eventType, details, req);
|
||||
|
||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
||||
expect(loggedData.userId).toBe(123);
|
||||
});
|
||||
|
||||
it('should handle missing request ID', () => {
|
||||
delete req.id;
|
||||
const eventType = 'SUSPICIOUS_ACTIVITY';
|
||||
const details = { reason: 'Multiple failed attempts' };
|
||||
|
||||
logSecurityEvent(eventType, details, req);
|
||||
|
||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
||||
expect(loggedData.requestId).toBe('unknown');
|
||||
});
|
||||
|
||||
it('should handle missing IP address', () => {
|
||||
delete req.ip;
|
||||
req.connection.remoteAddress = '10.0.0.1';
|
||||
const eventType = 'IP_CHECK';
|
||||
const details = { status: 'blocked' };
|
||||
|
||||
logSecurityEvent(eventType, details, req);
|
||||
|
||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
||||
expect(loggedData.ip).toBe('10.0.0.1');
|
||||
});
|
||||
|
||||
it('should include ISO timestamp', () => {
|
||||
const eventType = 'TEST_EVENT';
|
||||
const details = {};
|
||||
|
||||
logSecurityEvent(eventType, details, req);
|
||||
|
||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
||||
expect(loggedData.timestamp).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Non-production environment', () => {
|
||||
beforeEach(() => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
});
|
||||
|
||||
it('should log security event with simple format', () => {
|
||||
const eventType = 'LOGIN_ATTEMPT';
|
||||
const details = { username: 'testuser', success: false };
|
||||
|
||||
logSecurityEvent(eventType, details, req);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'[SECURITY]',
|
||||
'LOGIN_ATTEMPT',
|
||||
{ username: 'testuser', success: false }
|
||||
);
|
||||
});
|
||||
|
||||
it('should not log JSON in development', () => {
|
||||
const eventType = 'TEST_EVENT';
|
||||
const details = { test: true };
|
||||
|
||||
logSecurityEvent(eventType, details, req);
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith('[SECURITY]', 'TEST_EVENT', { test: true });
|
||||
// Ensure it's not JSON.stringify format
|
||||
expect(consoleSpy).not.toHaveBeenCalledWith('[SECURITY]', expect.stringMatching(/^{.*}$/));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle missing user-agent header', () => {
|
||||
req.get.mockReturnValue(null);
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
logSecurityEvent('TEST', {}, req);
|
||||
|
||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
||||
expect(loggedData.userAgent).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle empty details object', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
|
||||
logSecurityEvent('EMPTY_DETAILS', {}, req);
|
||||
|
||||
const loggedData = JSON.parse(consoleSpy.mock.calls[0][1]);
|
||||
expect(loggedData.eventType).toBe('EMPTY_DETAILS');
|
||||
expect(Object.keys(loggedData)).toContain('timestamp');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeError', () => {
|
||||
beforeEach(() => {
|
||||
req.id = 'test-request-id';
|
||||
req.user = { id: 123 };
|
||||
});
|
||||
|
||||
describe('Error logging', () => {
|
||||
it('should log full error details internally', () => {
|
||||
const error = new Error('Database connection failed');
|
||||
error.stack = 'Error: Database connection failed\n at /app/db.js:10:5';
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Error:', {
|
||||
requestId: 'test-request-id',
|
||||
error: 'Database connection failed',
|
||||
stack: 'Error: Database connection failed\n at /app/db.js:10:5',
|
||||
userId: 123
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing user in logging', () => {
|
||||
req.user = null;
|
||||
const error = new Error('Test error');
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(consoleErrorSpy).toHaveBeenCalledWith('Error:', {
|
||||
requestId: 'test-request-id',
|
||||
error: 'Test error',
|
||||
stack: error.stack,
|
||||
userId: undefined
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Client error responses (4xx)', () => {
|
||||
it('should handle 400 Bad Request errors', () => {
|
||||
const error = new Error('Invalid input data');
|
||||
error.status = 400;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(400);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Invalid input data',
|
||||
requestId: 'test-request-id'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle 400 errors with default message', () => {
|
||||
const error = new Error();
|
||||
error.status = 400;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Bad Request',
|
||||
requestId: 'test-request-id'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle 401 Unauthorized errors', () => {
|
||||
const error = new Error('Token expired');
|
||||
error.status = 401;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(401);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Unauthorized',
|
||||
requestId: 'test-request-id'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle 403 Forbidden errors', () => {
|
||||
const error = new Error('Access denied');
|
||||
error.status = 403;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(403);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Forbidden',
|
||||
requestId: 'test-request-id'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle 404 Not Found errors', () => {
|
||||
const error = new Error('User not found');
|
||||
error.status = 404;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(404);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Not Found',
|
||||
requestId: 'test-request-id'
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Server error responses (5xx)', () => {
|
||||
describe('Development environment', () => {
|
||||
beforeEach(() => {
|
||||
process.env.NODE_ENV = 'development';
|
||||
});
|
||||
|
||||
it('should include detailed error message and stack trace', () => {
|
||||
const error = new Error('Database connection failed');
|
||||
error.status = 500;
|
||||
error.stack = 'Error: Database connection failed\n at /app/db.js:10:5';
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Database connection failed',
|
||||
requestId: 'test-request-id',
|
||||
stack: 'Error: Database connection failed\n at /app/db.js:10:5'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle dev environment check', () => {
|
||||
process.env.NODE_ENV = 'dev';
|
||||
const error = new Error('Test error');
|
||||
error.status = 500;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Test error',
|
||||
requestId: 'test-request-id',
|
||||
stack: error.stack
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default status 500 when not specified', () => {
|
||||
const error = new Error('Unhandled error');
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Unhandled error',
|
||||
requestId: 'test-request-id',
|
||||
stack: error.stack
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle custom error status codes', () => {
|
||||
const error = new Error('Service unavailable');
|
||||
error.status = 503;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(503);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Service unavailable',
|
||||
requestId: 'test-request-id',
|
||||
stack: error.stack
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Production environment', () => {
|
||||
beforeEach(() => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
});
|
||||
|
||||
it('should return generic error message', () => {
|
||||
const error = new Error('Database connection failed');
|
||||
error.status = 500;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Internal Server Error',
|
||||
requestId: 'test-request-id'
|
||||
});
|
||||
});
|
||||
|
||||
it('should not include stack trace in production', () => {
|
||||
const error = new Error('Database error');
|
||||
error.status = 500;
|
||||
error.stack = 'Error: Database error\n at /app/db.js:10:5';
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
const response = res.json.mock.calls[0][0];
|
||||
expect(response).not.toHaveProperty('stack');
|
||||
});
|
||||
|
||||
it('should handle custom status codes in production', () => {
|
||||
const error = new Error('Service down');
|
||||
error.status = 502;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(502);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Internal Server Error',
|
||||
requestId: 'test-request-id'
|
||||
});
|
||||
});
|
||||
|
||||
it('should use default status 500 in production', () => {
|
||||
const error = new Error('Unknown error');
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Internal Server Error',
|
||||
requestId: 'test-request-id'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases', () => {
|
||||
it('should handle error without message', () => {
|
||||
const error = new Error();
|
||||
error.status = 400;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Bad Request',
|
||||
requestId: 'test-request-id'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing request ID', () => {
|
||||
delete req.id;
|
||||
const error = new Error('Test error');
|
||||
error.status = 400;
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.json).toHaveBeenCalledWith({
|
||||
error: 'Test error',
|
||||
requestId: undefined
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle error without status property', () => {
|
||||
const error = new Error('No status error');
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(res.status).toHaveBeenCalledWith(500);
|
||||
});
|
||||
|
||||
it('should not call next() - error handling middleware', () => {
|
||||
const error = new Error('Test error');
|
||||
|
||||
sanitizeError(error, req, res, next);
|
||||
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Module exports', () => {
|
||||
it('should export all required functions', () => {
|
||||
const securityModule = require('../../../middleware/security');
|
||||
|
||||
expect(securityModule).toHaveProperty('enforceHTTPS');
|
||||
expect(securityModule).toHaveProperty('securityHeaders');
|
||||
expect(securityModule).toHaveProperty('addRequestId');
|
||||
expect(securityModule).toHaveProperty('logSecurityEvent');
|
||||
expect(securityModule).toHaveProperty('sanitizeError');
|
||||
});
|
||||
|
||||
it('should export functions with correct types', () => {
|
||||
const securityModule = require('../../../middleware/security');
|
||||
|
||||
expect(typeof securityModule.enforceHTTPS).toBe('function');
|
||||
expect(typeof securityModule.securityHeaders).toBe('function');
|
||||
expect(typeof securityModule.addRequestId).toBe('function');
|
||||
expect(typeof securityModule.logSecurityEvent).toBe('function');
|
||||
expect(typeof securityModule.sanitizeError).toBe('function');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user