backend unit tests

This commit is contained in:
jackiettran
2025-09-19 19:46:41 -04:00
parent cf6dd9be90
commit 649289bf90
28 changed files with 17266 additions and 57 deletions

View File

@@ -0,0 +1,194 @@
const { authenticateToken } = require('../../../middleware/auth');
const jwt = require('jsonwebtoken');
jest.mock('jsonwebtoken');
jest.mock('../../../models', () => ({
User: {
findByPk: jest.fn()
}
}));
const { User } = require('../../../models');
describe('Auth Middleware', () => {
let req, res, next;
beforeEach(() => {
req = {
cookies: {}
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
next = jest.fn();
jest.clearAllMocks();
process.env.JWT_SECRET = 'test-secret';
});
describe('Valid token', () => {
it('should verify valid token from cookie and call next', async () => {
const mockUser = { id: 1, email: 'test@test.com' };
req.cookies.accessToken = 'validtoken';
jwt.verify.mockReturnValue({ id: 1 });
User.findByPk.mockResolvedValue(mockUser);
await authenticateToken(req, res, next);
expect(jwt.verify).toHaveBeenCalledWith('validtoken', process.env.JWT_SECRET);
expect(User.findByPk).toHaveBeenCalledWith(1);
expect(req.user).toEqual(mockUser);
expect(next).toHaveBeenCalled();
});
it('should handle token with valid user', async () => {
const mockUser = { id: 2, email: 'user@test.com', firstName: 'Test' };
req.cookies.accessToken = 'validtoken2';
jwt.verify.mockReturnValue({ id: 2 });
User.findByPk.mockResolvedValue(mockUser);
await authenticateToken(req, res, next);
expect(jwt.verify).toHaveBeenCalledWith('validtoken2', process.env.JWT_SECRET);
expect(User.findByPk).toHaveBeenCalledWith(2);
expect(req.user).toEqual(mockUser);
expect(next).toHaveBeenCalled();
});
});
describe('Invalid token', () => {
it('should return 401 for missing token', async () => {
req.cookies = {};
await authenticateToken(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
error: 'Access token required',
code: 'NO_TOKEN'
});
expect(next).not.toHaveBeenCalled();
});
it('should return 401 for invalid token', async () => {
req.cookies.accessToken = 'invalidtoken';
jwt.verify.mockImplementation(() => {
throw new Error('Invalid token');
});
await authenticateToken(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid token',
code: 'INVALID_TOKEN'
});
expect(next).not.toHaveBeenCalled();
});
it('should return 401 for expired token', async () => {
req.cookies.accessToken = 'expiredtoken';
const error = new Error('jwt expired');
error.name = 'TokenExpiredError';
jwt.verify.mockImplementation(() => {
throw error;
});
await authenticateToken(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
error: 'Token expired',
code: 'TOKEN_EXPIRED'
});
expect(next).not.toHaveBeenCalled();
});
it('should return 401 for invalid token format (missing user id)', async () => {
req.cookies.accessToken = 'tokenwithnoid';
jwt.verify.mockReturnValue({ email: 'test@test.com' }); // Missing id
await authenticateToken(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid token format',
code: 'INVALID_TOKEN_FORMAT'
});
expect(next).not.toHaveBeenCalled();
});
it('should return 401 when user not found', async () => {
req.cookies.accessToken = 'validtoken';
jwt.verify.mockReturnValue({ id: 999 });
User.findByPk.mockResolvedValue(null);
await authenticateToken(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
error: 'User not found',
code: 'USER_NOT_FOUND'
});
expect(next).not.toHaveBeenCalled();
});
});
describe('Edge cases', () => {
it('should handle empty string token', async () => {
req.cookies.accessToken = '';
await authenticateToken(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
error: 'Access token required',
code: 'NO_TOKEN'
});
});
it('should handle JWT malformed error', async () => {
req.cookies.accessToken = 'malformed.token';
const error = new Error('jwt malformed');
error.name = 'JsonWebTokenError';
jwt.verify.mockImplementation(() => {
throw error;
});
await authenticateToken(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid token',
code: 'INVALID_TOKEN'
});
});
it('should handle database error when finding user', async () => {
req.cookies.accessToken = 'validtoken';
jwt.verify.mockReturnValue({ id: 1 });
User.findByPk.mockRejectedValue(new Error('Database error'));
await authenticateToken(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid token',
code: 'INVALID_TOKEN'
});
expect(next).not.toHaveBeenCalled();
});
it('should handle undefined cookies', async () => {
req.cookies = undefined;
await authenticateToken(req, res, next);
expect(res.status).toHaveBeenCalledWith(401);
expect(res.json).toHaveBeenCalledWith({
error: 'Access token required',
code: 'NO_TOKEN'
});
});
});
});

View File

@@ -0,0 +1,506 @@
const mockTokensInstance = {
secretSync: jest.fn().mockReturnValue('mock-secret'),
create: jest.fn().mockReturnValue('mock-token-123'),
verify: jest.fn().mockReturnValue(true)
};
jest.mock('csrf', () => {
return jest.fn().mockImplementation(() => mockTokensInstance);
});
jest.mock('cookie-parser', () => {
return jest.fn().mockReturnValue((req, res, next) => next());
});
const { csrfProtection, generateCSRFToken, getCSRFToken } = require('../../../middleware/csrf');
describe('CSRF Middleware', () => {
let req, res, next;
beforeEach(() => {
req = {
method: 'POST',
headers: {},
body: {},
query: {},
cookies: {}
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
cookie: jest.fn(),
set: jest.fn(),
locals: {}
};
next = jest.fn();
jest.clearAllMocks();
});
describe('csrfProtection', () => {
describe('Safe methods', () => {
it('should skip CSRF protection for GET requests', () => {
req.method = 'GET';
csrfProtection(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
it('should skip CSRF protection for HEAD requests', () => {
req.method = 'HEAD';
csrfProtection(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
it('should skip CSRF protection for OPTIONS requests', () => {
req.method = 'OPTIONS';
csrfProtection(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
});
describe('Token validation', () => {
beforeEach(() => {
req.cookies = { 'csrf-token': 'mock-token-123' };
});
it('should validate token from x-csrf-token header', () => {
req.headers['x-csrf-token'] = 'mock-token-123';
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
it('should validate token from request body', () => {
req.body.csrfToken = 'mock-token-123';
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
it('should validate token from query parameters', () => {
req.query.csrfToken = 'mock-token-123';
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
it('should prefer header token over body token', () => {
req.headers['x-csrf-token'] = 'mock-token-123';
req.body.csrfToken = 'different-token';
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
});
it('should prefer header token over query token', () => {
req.headers['x-csrf-token'] = 'mock-token-123';
req.query.csrfToken = 'different-token';
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
});
it('should prefer body token over query token', () => {
req.body.csrfToken = 'mock-token-123';
req.query.csrfToken = 'different-token';
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
});
});
describe('Missing tokens', () => {
it('should return 403 when no token provided', () => {
req.cookies = { 'csrf-token': 'mock-token-123' };
csrfProtection(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid CSRF token',
code: 'CSRF_TOKEN_MISMATCH'
});
expect(next).not.toHaveBeenCalled();
});
it('should return 403 when no cookie token provided', () => {
req.headers['x-csrf-token'] = 'mock-token-123';
req.cookies = {};
csrfProtection(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid CSRF token',
code: 'CSRF_TOKEN_MISMATCH'
});
expect(next).not.toHaveBeenCalled();
});
it('should return 403 when cookies object is missing', () => {
req.headers['x-csrf-token'] = 'mock-token-123';
req.cookies = undefined;
csrfProtection(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid CSRF token',
code: 'CSRF_TOKEN_MISMATCH'
});
expect(next).not.toHaveBeenCalled();
});
it('should return 403 when both tokens are missing', () => {
req.cookies = {};
csrfProtection(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid CSRF token',
code: 'CSRF_TOKEN_MISMATCH'
});
expect(next).not.toHaveBeenCalled();
});
});
describe('Token mismatch', () => {
it('should return 403 when tokens do not match', () => {
req.headers['x-csrf-token'] = 'token-from-header';
req.cookies = { 'csrf-token': 'token-from-cookie' };
csrfProtection(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid CSRF token',
code: 'CSRF_TOKEN_MISMATCH'
});
expect(next).not.toHaveBeenCalled();
});
it('should return 403 when header token is empty but cookie exists', () => {
req.headers['x-csrf-token'] = '';
req.cookies = { 'csrf-token': 'mock-token-123' };
csrfProtection(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid CSRF token',
code: 'CSRF_TOKEN_MISMATCH'
});
expect(next).not.toHaveBeenCalled();
});
it('should return 403 when cookie token is empty but header exists', () => {
req.headers['x-csrf-token'] = 'mock-token-123';
req.cookies = { 'csrf-token': '' };
csrfProtection(req, res, next);
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid CSRF token',
code: 'CSRF_TOKEN_MISMATCH'
});
expect(next).not.toHaveBeenCalled();
});
});
describe('Token verification', () => {
beforeEach(() => {
req.headers['x-csrf-token'] = 'mock-token-123';
req.cookies = { 'csrf-token': 'mock-token-123' };
});
it('should return 403 when token verification fails', () => {
mockTokensInstance.verify.mockReturnValue(false);
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(res.status).toHaveBeenCalledWith(403);
expect(res.json).toHaveBeenCalledWith({
error: 'Invalid CSRF token',
code: 'CSRF_TOKEN_INVALID'
});
expect(next).not.toHaveBeenCalled();
});
it('should call next when token verification succeeds', () => {
mockTokensInstance.verify.mockReturnValue(true);
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
});
describe('Edge cases', () => {
it('should handle case-insensitive HTTP methods', () => {
req.method = 'post';
req.headers['x-csrf-token'] = 'mock-token-123';
req.cookies = { 'csrf-token': 'mock-token-123' };
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
});
it('should handle PUT requests', () => {
req.method = 'PUT';
req.headers['x-csrf-token'] = 'mock-token-123';
req.cookies = { 'csrf-token': 'mock-token-123' };
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
});
it('should handle DELETE requests', () => {
req.method = 'DELETE';
req.headers['x-csrf-token'] = 'mock-token-123';
req.cookies = { 'csrf-token': 'mock-token-123' };
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
});
it('should handle PATCH requests', () => {
req.method = 'PATCH';
req.headers['x-csrf-token'] = 'mock-token-123';
req.cookies = { 'csrf-token': 'mock-token-123' };
csrfProtection(req, res, next);
expect(mockTokensInstance.verify).toHaveBeenCalledWith('mock-secret', 'mock-token-123');
expect(next).toHaveBeenCalled();
});
});
});
describe('generateCSRFToken', () => {
it('should generate token and set cookie with proper options', () => {
process.env.NODE_ENV = 'production';
generateCSRFToken(req, res, next);
expect(mockTokensInstance.create).toHaveBeenCalledWith('mock-secret');
expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 1000
});
expect(next).toHaveBeenCalled();
});
it('should set secure flag to false in dev environment', () => {
process.env.NODE_ENV = 'dev';
generateCSRFToken(req, res, next);
expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
httpOnly: true,
secure: false,
sameSite: 'strict',
maxAge: 60 * 60 * 1000
});
});
it('should set secure flag to true in non-dev environment', () => {
process.env.NODE_ENV = 'production';
generateCSRFToken(req, res, next);
expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 1000
});
});
it('should set token in response header', () => {
generateCSRFToken(req, res, next);
expect(res.set).toHaveBeenCalledWith('X-CSRF-Token', 'mock-token-123');
});
it('should make token available in res.locals', () => {
generateCSRFToken(req, res, next);
expect(res.locals.csrfToken).toBe('mock-token-123');
});
it('should call next after setting up token', () => {
generateCSRFToken(req, res, next);
expect(next).toHaveBeenCalled();
});
it('should handle test environment', () => {
process.env.NODE_ENV = 'test';
generateCSRFToken(req, res, next);
expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 1000
});
});
it('should handle undefined NODE_ENV', () => {
delete process.env.NODE_ENV;
generateCSRFToken(req, res, next);
expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 1000
});
});
});
describe('getCSRFToken', () => {
it('should generate token and return it in response', () => {
process.env.NODE_ENV = 'production';
getCSRFToken(req, res);
expect(mockTokensInstance.create).toHaveBeenCalledWith('mock-secret');
expect(res.json).toHaveBeenCalledWith({ csrfToken: 'mock-token-123' });
});
it('should set token in cookie with proper options', () => {
process.env.NODE_ENV = 'production';
getCSRFToken(req, res);
expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 1000
});
});
it('should set secure flag to false in dev environment', () => {
process.env.NODE_ENV = 'dev';
getCSRFToken(req, res);
expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
httpOnly: true,
secure: false,
sameSite: 'strict',
maxAge: 60 * 60 * 1000
});
});
it('should set secure flag to true in production environment', () => {
process.env.NODE_ENV = 'production';
getCSRFToken(req, res);
expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 1000
});
});
it('should handle test environment', () => {
process.env.NODE_ENV = 'test';
getCSRFToken(req, res);
expect(res.cookie).toHaveBeenCalledWith('csrf-token', 'mock-token-123', {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 60 * 60 * 1000
});
});
it('should generate new token each time', () => {
mockTokensInstance.create
.mockReturnValueOnce('token-1')
.mockReturnValueOnce('token-2');
getCSRFToken(req, res);
expect(res.json).toHaveBeenCalledWith({ csrfToken: 'token-1' });
getCSRFToken(req, res);
expect(res.json).toHaveBeenCalledWith({ csrfToken: 'token-2' });
});
});
describe('Integration scenarios', () => {
it('should handle complete CSRF flow', () => {
// First, generate a token
generateCSRFToken(req, res, next);
const generatedToken = res.locals.csrfToken;
// Reset mocks
jest.clearAllMocks();
// Now test protection with the generated token
req.method = 'POST';
req.headers['x-csrf-token'] = generatedToken;
req.cookies = { 'csrf-token': generatedToken };
csrfProtection(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
it('should handle token generation endpoint flow', () => {
getCSRFToken(req, res);
const tokenFromResponse = res.json.mock.calls[0][0].csrfToken;
const cookieCall = res.cookie.mock.calls[0];
expect(cookieCall[0]).toBe('csrf-token');
expect(cookieCall[1]).toBe(tokenFromResponse);
expect(tokenFromResponse).toBe('mock-token-123');
});
});
});

View File

@@ -0,0 +1,501 @@
// Mock express-rate-limit
const mockRateLimitInstance = jest.fn();
jest.mock('express-rate-limit', () => {
const rateLimitFn = jest.fn((config) => {
// Store the config for inspection in tests
rateLimitFn.lastConfig = config;
return mockRateLimitInstance;
});
rateLimitFn.defaultKeyGenerator = jest.fn().mockReturnValue('127.0.0.1');
return rateLimitFn;
});
const rateLimit = require('express-rate-limit');
const {
placesAutocomplete,
placeDetails,
geocoding,
loginLimiter,
registerLimiter,
passwordResetLimiter,
generalLimiter,
burstProtection,
createMapsRateLimiter,
createUserBasedRateLimiter
} = require('../../../middleware/rateLimiter');
describe('Rate Limiter Middleware', () => {
let req, res, next;
beforeEach(() => {
req = {
ip: '127.0.0.1',
user: null
};
res = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
set: jest.fn()
};
next = jest.fn();
jest.clearAllMocks();
});
describe('createMapsRateLimiter', () => {
it('should create rate limiter with correct configuration', () => {
const windowMs = 60000;
const max = 30;
const message = 'Test message';
createMapsRateLimiter(windowMs, max, message);
expect(rateLimit).toHaveBeenCalledWith({
windowMs,
max,
message: {
error: message,
retryAfter: Math.ceil(windowMs / 1000)
},
standardHeaders: true,
legacyHeaders: false,
keyGenerator: expect.any(Function)
});
});
describe('keyGenerator', () => {
it('should use user ID when user is authenticated', () => {
const windowMs = 60000;
const max = 30;
const message = 'Test message';
createMapsRateLimiter(windowMs, max, message);
const config = rateLimit.lastConfig;
const reqWithUser = { user: { id: 123 } };
const key = config.keyGenerator(reqWithUser);
expect(key).toBe('user:123');
});
it('should use default IP generator when user is not authenticated', () => {
const windowMs = 60000;
const max = 30;
const message = 'Test message';
createMapsRateLimiter(windowMs, max, message);
const config = rateLimit.lastConfig;
const reqWithoutUser = { user: null };
config.keyGenerator(reqWithoutUser);
expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(reqWithoutUser);
});
it('should use default IP generator when user has no ID', () => {
const windowMs = 60000;
const max = 30;
const message = 'Test message';
createMapsRateLimiter(windowMs, max, message);
const config = rateLimit.lastConfig;
const reqWithUserNoId = { user: {} };
config.keyGenerator(reqWithUserNoId);
expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(reqWithUserNoId);
});
});
it('should calculate retryAfter correctly', () => {
const windowMs = 90000; // 90 seconds
const max = 10;
const message = 'Test message';
createMapsRateLimiter(windowMs, max, message);
expect(rateLimit).toHaveBeenCalledWith(expect.objectContaining({
message: {
error: message,
retryAfter: 90 // Math.ceil(90000 / 1000)
}
}));
});
});
describe('Pre-configured rate limiters', () => {
describe('placesAutocomplete', () => {
it('should be a function (rate limiter middleware)', () => {
expect(typeof placesAutocomplete).toBe('function');
});
});
describe('placeDetails', () => {
it('should be a function (rate limiter middleware)', () => {
expect(typeof placeDetails).toBe('function');
});
});
describe('geocoding', () => {
it('should be a function (rate limiter middleware)', () => {
expect(typeof geocoding).toBe('function');
});
});
describe('loginLimiter', () => {
it('should be a function (rate limiter middleware)', () => {
expect(typeof loginLimiter).toBe('function');
});
});
describe('registerLimiter', () => {
it('should be a function (rate limiter middleware)', () => {
expect(typeof registerLimiter).toBe('function');
});
});
describe('passwordResetLimiter', () => {
it('should be a function (rate limiter middleware)', () => {
expect(typeof passwordResetLimiter).toBe('function');
});
});
describe('generalLimiter', () => {
it('should be a function (rate limiter middleware)', () => {
expect(typeof generalLimiter).toBe('function');
});
});
});
describe('createUserBasedRateLimiter', () => {
let userBasedLimiter;
const windowMs = 10000; // 10 seconds
const max = 5;
const message = 'Too many requests';
beforeEach(() => {
userBasedLimiter = createUserBasedRateLimiter(windowMs, max, message);
});
describe('Key generation', () => {
it('should use user ID when user is authenticated', () => {
req.user = { id: 123 };
userBasedLimiter(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
it('should use IP when user is not authenticated', () => {
req.user = null;
rateLimit.defaultKeyGenerator.mockReturnValue('192.168.1.1');
userBasedLimiter(req, res, next);
expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(req);
expect(next).toHaveBeenCalled();
});
});
describe('Rate limiting logic', () => {
it('should allow requests within limit', () => {
req.user = { id: 123 };
// Make requests within limit
for (let i = 0; i < max; i++) {
jest.clearAllMocks();
userBasedLimiter(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
}
});
it('should block requests when limit exceeded', () => {
req.user = { id: 123 };
// Exhaust the limit
for (let i = 0; i < max; i++) {
userBasedLimiter(req, res, next);
}
// Next request should be blocked
jest.clearAllMocks();
userBasedLimiter(req, res, next);
expect(res.status).toHaveBeenCalledWith(429);
expect(res.json).toHaveBeenCalledWith({
error: message,
retryAfter: expect.any(Number)
});
expect(next).not.toHaveBeenCalled();
});
it('should set correct rate limit headers', () => {
req.user = { id: 123 };
userBasedLimiter(req, res, next);
expect(res.set).toHaveBeenCalledWith({
'RateLimit-Limit': max,
'RateLimit-Remaining': max - 1,
'RateLimit-Reset': expect.any(String)
});
});
it('should update remaining count correctly', () => {
req.user = { id: 123 };
// First request
userBasedLimiter(req, res, next);
expect(res.set).toHaveBeenCalledWith(expect.objectContaining({
'RateLimit-Remaining': 4
}));
// Second request
jest.clearAllMocks();
userBasedLimiter(req, res, next);
expect(res.set).toHaveBeenCalledWith(expect.objectContaining({
'RateLimit-Remaining': 3
}));
});
it('should not go below 0 for remaining count', () => {
req.user = { id: 123 };
// Exhaust the limit
for (let i = 0; i < max; i++) {
userBasedLimiter(req, res, next);
}
// Check that remaining doesn't go negative
const lastCall = res.set.mock.calls[res.set.mock.calls.length - 1][0];
expect(lastCall['RateLimit-Remaining']).toBe(0);
});
});
describe('Window management', () => {
it('should reset count after window expires', () => {
req.user = { id: 123 };
const originalDateNow = Date.now;
// Mock time to start of window
let currentTime = 1000000000;
Date.now = jest.fn(() => currentTime);
// Exhaust the limit
for (let i = 0; i < max; i++) {
userBasedLimiter(req, res, next);
}
// Verify limit is reached
jest.clearAllMocks();
userBasedLimiter(req, res, next);
expect(res.status).toHaveBeenCalledWith(429);
// Move time forward past the window
currentTime += windowMs + 1000;
jest.clearAllMocks();
// Should allow requests again
userBasedLimiter(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
// Restore original Date.now
Date.now = originalDateNow;
});
it('should clean up old entries from store', () => {
const originalDateNow = Date.now;
let currentTime = 1000000000;
Date.now = jest.fn(() => currentTime);
// Create entries for different users
req.user = { id: 1 };
userBasedLimiter(req, res, next);
req.user = { id: 2 };
userBasedLimiter(req, res, next);
// Move time forward to expire first entries
currentTime += windowMs + 1000;
req.user = { id: 3 };
userBasedLimiter(req, res, next);
// The cleanup should have occurred when processing user 3's request
// We can't directly test the internal store, but we can verify the behavior
expect(next).toHaveBeenCalled();
// Restore original Date.now
Date.now = originalDateNow;
});
});
describe('Different users/IPs', () => {
it('should maintain separate counts for different users', () => {
// User 1 makes max requests
req.user = { id: 1 };
for (let i = 0; i < max; i++) {
userBasedLimiter(req, res, next);
}
// User 1 should be blocked
jest.clearAllMocks();
userBasedLimiter(req, res, next);
expect(res.status).toHaveBeenCalledWith(429);
// User 2 should still be allowed
jest.clearAllMocks();
req.user = { id: 2 };
userBasedLimiter(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
it('should maintain separate counts for different IPs', () => {
req.user = null;
// IP 1 makes max requests
rateLimit.defaultKeyGenerator.mockReturnValue('192.168.1.1');
for (let i = 0; i < max; i++) {
userBasedLimiter(req, res, next);
}
// IP 1 should be blocked
jest.clearAllMocks();
userBasedLimiter(req, res, next);
expect(res.status).toHaveBeenCalledWith(429);
// IP 2 should still be allowed
jest.clearAllMocks();
rateLimit.defaultKeyGenerator.mockReturnValue('192.168.1.2');
userBasedLimiter(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
});
});
describe('Edge cases', () => {
it('should handle undefined user gracefully', () => {
req.user = undefined;
rateLimit.defaultKeyGenerator.mockReturnValue('127.0.0.1');
userBasedLimiter(req, res, next);
expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(req);
expect(next).toHaveBeenCalled();
});
it('should handle user object without id', () => {
req.user = { email: 'test@test.com' };
rateLimit.defaultKeyGenerator.mockReturnValue('127.0.0.1');
userBasedLimiter(req, res, next);
expect(rateLimit.defaultKeyGenerator).toHaveBeenCalledWith(req);
expect(next).toHaveBeenCalled();
});
it('should set correct reset time in ISO format', () => {
req.user = { id: 123 };
userBasedLimiter(req, res, next);
const setCall = res.set.mock.calls[0][0];
const resetTime = setCall['RateLimit-Reset'];
// Should be a valid ISO string
expect(() => new Date(resetTime)).not.toThrow();
expect(resetTime).toMatch(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/);
});
it('should calculate retry after correctly when limit exceeded', () => {
req.user = { id: 123 };
const originalDateNow = Date.now;
const currentTime = 1000000000;
Date.now = jest.fn(() => currentTime);
// Exhaust the limit
for (let i = 0; i < max; i++) {
userBasedLimiter(req, res, next);
}
jest.clearAllMocks();
userBasedLimiter(req, res, next);
const jsonCall = res.json.mock.calls[0][0];
expect(jsonCall.retryAfter).toBe(Math.ceil(windowMs / 1000));
// Restore original Date.now
Date.now = originalDateNow;
});
});
});
describe('burstProtection', () => {
it('should be a function', () => {
expect(typeof burstProtection).toBe('function');
});
it('should allow requests within burst limit', () => {
req.user = { id: 123 };
// Should allow up to 5 requests in 10 seconds
for (let i = 0; i < 5; i++) {
jest.clearAllMocks();
burstProtection(req, res, next);
expect(next).toHaveBeenCalled();
expect(res.status).not.toHaveBeenCalled();
}
});
it('should block requests when burst limit exceeded', () => {
req.user = { id: 123 };
// Exhaust burst limit
for (let i = 0; i < 5; i++) {
burstProtection(req, res, next);
}
// Next request should be blocked
jest.clearAllMocks();
burstProtection(req, res, next);
expect(res.status).toHaveBeenCalledWith(429);
expect(res.json).toHaveBeenCalledWith({
error: 'Too many requests in a short period. Please slow down.',
retryAfter: expect.any(Number)
});
expect(next).not.toHaveBeenCalled();
});
});
describe('Module exports', () => {
it('should export all required rate limiters', () => {
const rateLimiterModule = require('../../../middleware/rateLimiter');
expect(rateLimiterModule).toHaveProperty('placesAutocomplete');
expect(rateLimiterModule).toHaveProperty('placeDetails');
expect(rateLimiterModule).toHaveProperty('geocoding');
expect(rateLimiterModule).toHaveProperty('loginLimiter');
expect(rateLimiterModule).toHaveProperty('registerLimiter');
expect(rateLimiterModule).toHaveProperty('passwordResetLimiter');
expect(rateLimiterModule).toHaveProperty('generalLimiter');
expect(rateLimiterModule).toHaveProperty('burstProtection');
expect(rateLimiterModule).toHaveProperty('createMapsRateLimiter');
expect(rateLimiterModule).toHaveProperty('createUserBasedRateLimiter');
});
it('should export functions for utility methods', () => {
const rateLimiterModule = require('../../../middleware/rateLimiter');
expect(typeof rateLimiterModule.createMapsRateLimiter).toBe('function');
expect(typeof rateLimiterModule.createUserBasedRateLimiter).toBe('function');
expect(typeof rateLimiterModule.burstProtection).toBe('function');
});
});
});

View 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');
});
});
});

File diff suppressed because it is too large Load Diff