723 lines
22 KiB
JavaScript
723 lines
22 KiB
JavaScript
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');
|
|
});
|
|
});
|
|
}); |