test: enhance security middleware and token validation tests

- Refactored security middleware tests with improved type safety and mock configurations
- Updated token validation tests with more precise token generation and expiration scenarios
- Improved input sanitization and request validation test coverage
- Added comprehensive test cases for error handling and security header configurations
- Enhanced test setup with better environment and secret management
This commit is contained in:
jango-blockchained
2025-02-03 22:52:18 +01:00
parent e688c94718
commit 04123a5740
7 changed files with 296 additions and 258 deletions

View File

@@ -2,7 +2,17 @@ import { TokenManager, validateRequest, sanitizeInput, errorHandler } from '../.
import { Request, Response } from 'express';
import jwt from 'jsonwebtoken';
const TEST_SECRET = 'test-secret-that-is-long-enough-for-testing-purposes';
describe('Security Module', () => {
beforeEach(() => {
process.env.JWT_SECRET = TEST_SECRET;
});
afterEach(() => {
delete process.env.JWT_SECRET;
});
describe('TokenManager', () => {
const testToken = 'test-token';
const encryptionKey = 'test-encryption-key-that-is-long-enough';
@@ -16,7 +26,7 @@ describe('Security Module', () => {
});
it('should validate tokens correctly', () => {
const validToken = jwt.sign({ data: 'test' }, process.env.JWT_SECRET || 'test-secret', { expiresIn: '1h' });
const validToken = jwt.sign({ data: 'test' }, TEST_SECRET, { expiresIn: '1h' });
const result = TokenManager.validateToken(validToken);
expect(result.valid).toBe(true);
expect(result.error).toBeUndefined();
@@ -29,8 +39,14 @@ describe('Security Module', () => {
});
it('should handle expired tokens', () => {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
const result = TokenManager.validateToken(expiredToken);
const now = Math.floor(Date.now() / 1000);
const payload = {
data: 'test',
iat: now - 7200, // 2 hours ago
exp: now - 3600 // expired 1 hour ago
};
const token = jwt.sign(payload, TEST_SECRET);
const result = TokenManager.validateToken(token);
expect(result.valid).toBe(false);
expect(result.error).toBe('Token has expired');
});
@@ -45,20 +61,28 @@ describe('Security Module', () => {
mockRequest = {
method: 'POST',
headers: {
'content-type': 'application/json',
authorization: 'Bearer validToken'
},
is: jest.fn().mockReturnValue(true),
body: { test: 'data' }
'content-type': 'application/json'
} as Record<string, string>,
body: {},
ip: '127.0.0.1'
};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
json: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
removeHeader: jest.fn().mockReturnThis()
};
mockNext = jest.fn();
});
it('should pass valid requests', () => {
if (mockRequest.headers) {
mockRequest.headers.authorization = 'Bearer valid-token';
}
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({ valid: true });
validateRequest(
mockRequest as Request,
mockResponse as Response,
@@ -66,11 +90,12 @@ describe('Security Module', () => {
);
expect(mockNext).toHaveBeenCalled();
expect(mockResponse.status).not.toHaveBeenCalled();
});
it('should reject invalid content type', () => {
mockRequest.is = jest.fn().mockReturnValue(false);
if (mockRequest.headers) {
mockRequest.headers['content-type'] = 'text/plain';
}
validateRequest(
mockRequest as Request,
@@ -80,12 +105,17 @@ describe('Security Module', () => {
expect(mockResponse.status).toHaveBeenCalledWith(415);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Unsupported Media Type - Content-Type must be application/json'
success: false,
message: 'Unsupported Media Type',
error: 'Content-Type must be application/json',
timestamp: expect.any(String)
});
});
it('should reject missing token', () => {
mockRequest.headers = {};
if (mockRequest.headers) {
delete mockRequest.headers.authorization;
}
validateRequest(
mockRequest as Request,
@@ -95,7 +125,10 @@ describe('Security Module', () => {
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Invalid or expired token'
success: false,
message: 'Unauthorized',
error: 'Missing or invalid authorization header',
timestamp: expect.any(String)
});
});
@@ -110,7 +143,10 @@ describe('Security Module', () => {
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Invalid request body'
success: false,
message: 'Bad Request',
error: 'Invalid request body structure',
timestamp: expect.any(String)
});
});
});
@@ -122,20 +158,27 @@ describe('Security Module', () => {
beforeEach(() => {
mockRequest = {
body: {}
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: {
text: 'Test alert("xss")',
nested: {
html: 'img src="x" onerror="alert(1)"'
}
}
};
mockResponse = {};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis()
};
mockNext = jest.fn();
});
it('should sanitize HTML tags from request body', () => {
mockRequest.body = {
text: 'Test <script>alert("xss")</script>',
nested: {
html: '<img src="x" onerror="alert(1)">'
}
};
sanitizeInput(
mockRequest as Request,
mockResponse as Response,
@@ -143,9 +186,9 @@ describe('Security Module', () => {
);
expect(mockRequest.body).toEqual({
text: 'Test alert("xss")',
text: 'Test',
nested: {
html: 'img src="x" onerror="alert(1)"'
html: ''
}
});
expect(mockNext).toHaveBeenCalled();
@@ -153,14 +196,11 @@ describe('Security Module', () => {
it('should handle non-object body', () => {
mockRequest.body = 'string body';
sanitizeInput(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockRequest.body).toBe('string body');
expect(mockNext).toHaveBeenCalled();
});
});
@@ -169,25 +209,24 @@ describe('Security Module', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockNext: jest.Mock;
const originalEnv = process.env.NODE_ENV;
beforeEach(() => {
mockRequest = {};
mockRequest = {
method: 'POST',
ip: '127.0.0.1'
};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
json: jest.fn().mockReturnThis()
};
mockNext = jest.fn();
});
afterAll(() => {
process.env.NODE_ENV = originalEnv;
mockNext = jest.fn();
});
it('should handle errors in production mode', () => {
process.env.NODE_ENV = 'production';
const error = new Error('Test error');
errorHandler(
error,
mockRequest as Request,
@@ -197,15 +236,15 @@ describe('Security Module', () => {
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Internal Server Error',
message: undefined
success: false,
message: 'Internal Server Error',
timestamp: expect.any(String)
});
});
it('should include error message in development mode', () => {
process.env.NODE_ENV = 'development';
const error = new Error('Test error');
errorHandler(
error,
mockRequest as Request,
@@ -215,8 +254,11 @@ describe('Security Module', () => {
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Internal Server Error',
message: 'Test error'
success: false,
message: 'Internal Server Error',
error: 'Test error',
stack: expect.any(String),
timestamp: expect.any(String)
});
});
});

View File

@@ -1,5 +1,6 @@
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
import { Request, Response, NextFunction } from 'express';
import { Request, Response } from 'express';
import { Mock } from 'bun:test';
import {
validateRequest,
sanitizeInput,
@@ -7,28 +8,29 @@ import {
rateLimiter,
securityHeaders
} from '../../src/security/index.js';
import { Mock } from 'bun:test';
type MockRequest = {
interface MockRequest extends Partial<Request> {
headers: {
'content-type'?: string;
authorization?: string;
};
body?: any;
is: jest.MockInstance<string | false | null, [type: string | string[]]>;
};
method: string;
body: any;
ip: string;
path: string;
}
type MockResponse = {
status: jest.MockInstance<MockResponse, [code: number]>;
json: jest.MockInstance<MockResponse, [body: any]>;
setHeader: jest.MockInstance<MockResponse, [name: string, value: string]>;
removeHeader: jest.MockInstance<MockResponse, [name: string]>;
};
interface MockResponse extends Partial<Response> {
status: Mock<(code: number) => MockResponse>;
json: Mock<(body: any) => MockResponse>;
setHeader: Mock<(name: string, value: string) => MockResponse>;
removeHeader: Mock<(name: string) => MockResponse>;
}
describe('Security Middleware', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let nextFunction: Mock<() => void>;
let mockRequest: any;
let mockResponse: any;
let nextFunction: any;
beforeEach(() => {
mockRequest = {
@@ -36,7 +38,9 @@ describe('Security Middleware', () => {
'content-type': 'application/json'
},
method: 'POST',
body: {}
body: {},
ip: '127.0.0.1',
path: '/api/test'
};
mockResponse = {
@@ -44,7 +48,7 @@ describe('Security Middleware', () => {
json: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
removeHeader: jest.fn().mockReturnThis()
};
} as MockResponse;
nextFunction = jest.fn();
});
@@ -52,24 +56,30 @@ describe('Security Middleware', () => {
describe('Request Validation', () => {
it('should pass valid requests', () => {
mockRequest.headers.authorization = 'Bearer valid-token';
validateRequest(mockRequest as Request, mockResponse as Response, nextFunction);
validateRequest(mockRequest, mockResponse, nextFunction);
expect(nextFunction).toHaveBeenCalled();
});
it('should reject requests without authorization header', () => {
validateRequest(mockRequest as Request, mockResponse as Response, nextFunction);
validateRequest(mockRequest, mockResponse, nextFunction);
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Authorization header missing'
success: false,
message: 'Unauthorized',
error: 'Missing or invalid authorization header',
timestamp: expect.any(String)
});
});
it('should reject requests with invalid authorization format', () => {
mockRequest.headers.authorization = 'invalid-format';
validateRequest(mockRequest as Request, mockResponse as Response, nextFunction);
validateRequest(mockRequest, mockResponse, nextFunction);
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Invalid authorization format'
success: false,
message: 'Unauthorized',
error: 'Missing or invalid authorization header',
timestamp: expect.any(String)
});
});
});
@@ -82,7 +92,7 @@ describe('Security Middleware', () => {
html: '<img src="x" onerror="alert(1)">World'
}
};
sanitizeInput(mockRequest as Request, mockResponse as Response, nextFunction);
sanitizeInput(mockRequest, mockResponse, nextFunction);
expect(mockRequest.body.text).toBe('Hello');
expect(mockRequest.body.nested.html).toBe('World');
expect(nextFunction).toHaveBeenCalled();
@@ -90,7 +100,7 @@ describe('Security Middleware', () => {
it('should handle non-object bodies', () => {
mockRequest.body = '<p>text</p>';
sanitizeInput(mockRequest as Request, mockResponse as Response, nextFunction);
sanitizeInput(mockRequest, mockResponse, nextFunction);
expect(mockRequest.body).toBe('text');
expect(nextFunction).toHaveBeenCalled();
});
@@ -101,7 +111,7 @@ describe('Security Middleware', () => {
boolean: true,
array: [1, 2, 3]
};
sanitizeInput(mockRequest as Request, mockResponse as Response, nextFunction);
sanitizeInput(mockRequest, mockResponse, nextFunction);
expect(mockRequest.body).toEqual({
number: 123,
boolean: true,
@@ -121,34 +131,31 @@ describe('Security Middleware', () => {
it('should handle errors in production mode', () => {
process.env.NODE_ENV = 'production';
const error = new Error('Test error');
errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction);
errorHandler(error, mockRequest, mockResponse, nextFunction);
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Internal server error'
error: 'Internal Server Error',
message: undefined,
timestamp: expect.any(String)
});
});
it('should include error details in development mode', () => {
process.env.NODE_ENV = 'development';
const error = new Error('Test error');
errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction);
errorHandler(error, mockRequest, mockResponse, nextFunction);
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Test error',
stack: expect.any(String)
error: 'Internal Server Error',
message: 'Test error',
stack: expect.any(String),
timestamp: expect.any(String)
});
});
it('should handle non-Error objects', () => {
const error = 'String error message';
errorHandler(
error as any,
mockRequest as Request,
mockResponse as Response,
nextFunction
);
errorHandler(error as any, mockRequest, mockResponse, nextFunction);
expect(mockResponse.status).toHaveBeenCalledWith(500);
});
});
@@ -164,7 +171,7 @@ describe('Security Middleware', () => {
describe('Security Headers', () => {
it('should set appropriate security headers', () => {
securityHeaders(mockRequest as Request, mockResponse as Response, nextFunction);
securityHeaders(mockRequest, mockResponse, nextFunction);
expect(mockResponse.setHeader).toHaveBeenCalledWith('X-Content-Type-Options', 'nosniff');
expect(mockResponse.setHeader).toHaveBeenCalledWith('X-Frame-Options', 'DENY');
expect(mockResponse.setHeader).toHaveBeenCalledWith('X-XSS-Protection', '1; mode=block');

View File

@@ -46,16 +46,16 @@ describe('TokenManager', () => {
describe('Token Validation', () => {
it('should validate correct tokens', () => {
const payload = { sub: '123', name: 'Test User' };
const token = jwt.sign(payload, TEST_SECRET, { expiresIn: '1h' });
const payload = { sub: '123', name: 'Test User', iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600 };
const token = jwt.sign(payload, TEST_SECRET);
const result = TokenManager.validateToken(token);
expect(result.valid).toBe(true);
expect(result.error).toBeUndefined();
});
it('should reject expired tokens', () => {
const payload = { sub: '123', name: 'Test User' };
const token = jwt.sign(payload, TEST_SECRET, { expiresIn: -1 });
const payload = { sub: '123', name: 'Test User', iat: Math.floor(Date.now() / 1000) - 7200, exp: Math.floor(Date.now() / 1000) - 3600 };
const token = jwt.sign(payload, TEST_SECRET);
const result = TokenManager.validateToken(token);
expect(result.valid).toBe(false);
expect(result.error).toBe('Token has expired');
@@ -68,8 +68,8 @@ describe('TokenManager', () => {
});
it('should reject tokens with invalid signature', () => {
const payload = { sub: '123', name: 'Test User' };
const token = jwt.sign(payload, 'different-secret', { expiresIn: '1h' });
const payload = { sub: '123', name: 'Test User', iat: Math.floor(Date.now() / 1000), exp: Math.floor(Date.now() / 1000) + 3600 };
const token = jwt.sign(payload, 'different-secret');
const result = TokenManager.validateToken(token);
expect(result.valid).toBe(false);
expect(result.error).toBe('Invalid token signature');
@@ -82,6 +82,16 @@ describe('TokenManager', () => {
expect(result.valid).toBe(false);
expect(result.error).toBe('Token missing required claims');
});
it('should handle undefined and null inputs', () => {
const undefinedResult = TokenManager.validateToken(undefined);
expect(undefinedResult.valid).toBe(false);
expect(undefinedResult.error).toBe('Invalid token format');
const nullResult = TokenManager.validateToken(null);
expect(nullResult.valid).toBe(false);
expect(nullResult.error).toBe('Invalid token format');
});
});
describe('Security Features', () => {
@@ -128,10 +138,5 @@ describe('TokenManager', () => {
it('should handle invalid base64 input', () => {
expect(() => TokenManager.decryptToken('not-base64!@#$%^', encryptionKey)).toThrow();
});
it('should handle undefined and null inputs', () => {
expect(TokenManager.validateToken(undefined as any)).toBe(false);
expect(TokenManager.validateToken(null as any)).toBe(false);
});
});
});