Add comprehensive security middleware and token management tests

- Introduced detailed test suites for security middleware components
- Added robust test coverage for token encryption, decryption, and validation
- Implemented comprehensive tests for input sanitization and error handling
- Created test scenarios for rate limiting and security header configurations
- Enhanced test infrastructure for security-related modules with edge case handling
This commit is contained in:
jango-blockchained
2025-01-30 10:10:15 +01:00
parent c64dc4334b
commit c904e4f188
4 changed files with 783 additions and 0 deletions

View File

@@ -0,0 +1,248 @@
import { Request, Response } from 'express';
import {
validateRequest,
sanitizeInput,
errorHandler,
rateLimiter,
securityHeaders
} from '../../src/security/index.js';
interface MockRequest extends Partial<Request> {
headers: Record<string, string>;
is: jest.Mock;
}
describe('Security Middleware', () => {
let mockRequest: MockRequest;
let mockResponse: Partial<Response>;
let mockNext: jest.Mock;
beforeEach(() => {
mockRequest = {
method: 'POST',
headers: {
'content-type': 'application/json',
'authorization': 'Bearer validToken'
},
is: jest.fn().mockReturnValue(true),
body: { test: 'data' }
};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn(),
setHeader: jest.fn(),
set: jest.fn()
};
mockNext = jest.fn();
});
describe('Request Validation', () => {
it('should pass valid requests', () => {
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockNext).toHaveBeenCalled();
expect(mockResponse.status).not.toHaveBeenCalled();
});
it('should reject requests with invalid content type', () => {
mockRequest.is = jest.fn().mockReturnValue(false);
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(415);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Unsupported Media Type - Content-Type must be application/json'
});
});
it('should reject requests without authorization', () => {
mockRequest.headers = {};
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(401);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Invalid or expired token'
});
});
it('should reject requests with invalid token', () => {
mockRequest.headers.authorization = 'Bearer invalid.token.format';
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(401);
});
it('should handle GET requests without body validation', () => {
mockRequest.method = 'GET';
mockRequest.body = undefined;
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockNext).toHaveBeenCalled();
});
});
describe('Input Sanitization', () => {
it('should remove HTML tags from request body', () => {
mockRequest.body = {
text: '<script>alert("xss")</script>',
nested: {
html: '<img src="x" onerror="alert(1)">'
}
};
sanitizeInput(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockRequest.body.text).not.toContain('<script>');
expect(mockRequest.body.nested.html).not.toContain('<img');
expect(mockNext).toHaveBeenCalled();
});
it('should handle non-object body', () => {
mockRequest.body = 'plain text';
sanitizeInput(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockRequest.body).toBe('plain text');
expect(mockNext).toHaveBeenCalled();
});
it('should handle nested objects', () => {
mockRequest.body = {
level1: {
level2: {
text: '<p>test</p>'
}
}
};
sanitizeInput(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockRequest.body.level1.level2.text).not.toContain('<p>');
expect(mockNext).toHaveBeenCalled();
});
it('should preserve safe content', () => {
mockRequest.body = {
text: 'Safe text without HTML',
number: 42,
boolean: true
};
const originalBody = { ...mockRequest.body };
sanitizeInput(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockRequest.body).toEqual(originalBody);
expect(mockNext).toHaveBeenCalled();
});
});
describe('Error Handler', () => {
const originalEnv = process.env.NODE_ENV;
afterAll(() => {
process.env.NODE_ENV = originalEnv;
});
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,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Internal Server Error',
message: undefined
});
});
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,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(500);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Internal Server Error',
message: 'Test error'
});
});
it('should handle non-Error objects', () => {
const error = 'String error message';
errorHandler(
error as any,
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(500);
});
});
describe('Rate Limiter', () => {
it('should be configured with correct options', () => {
expect(rateLimiter).toBeDefined();
const middleware = rateLimiter as any;
expect(middleware.windowMs).toBeDefined();
expect(middleware.max).toBeDefined();
});
});
describe('Security Headers', () => {
it('should be configured with secure defaults', () => {
expect(securityHeaders).toBeDefined();
const middleware = securityHeaders as any;
expect(middleware.getDefaultDirectives).toBeDefined();
});
it('should set appropriate security headers', () => {
const mockRes = {
setHeader: jest.fn()
};
securityHeaders(mockRequest as Request, mockRes as any, mockNext);
expect(mockRes.setHeader).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,127 @@
import { TokenManager } from '../../src/security/index.js';
describe('TokenManager', () => {
const validToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNzE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
const encryptionKey = 'test_encryption_key_12345';
describe('Token Encryption/Decryption', () => {
it('should encrypt and decrypt tokens successfully', () => {
const encrypted = TokenManager.encryptToken(validToken, encryptionKey);
const decrypted = TokenManager.decryptToken(encrypted, encryptionKey);
expect(decrypted).toBe(validToken);
});
it('should generate different encrypted values for same token', () => {
const encrypted1 = TokenManager.encryptToken(validToken, encryptionKey);
const encrypted2 = TokenManager.encryptToken(validToken, encryptionKey);
expect(encrypted1).not.toBe(encrypted2);
});
it('should handle empty tokens', () => {
expect(() => TokenManager.encryptToken('', encryptionKey)).toThrow();
});
it('should handle empty encryption keys', () => {
expect(() => TokenManager.encryptToken(validToken, '')).toThrow();
});
it('should fail decryption with wrong key', () => {
const encrypted = TokenManager.encryptToken(validToken, encryptionKey);
expect(() => TokenManager.decryptToken(encrypted, 'wrong_key')).toThrow();
});
});
describe('Token Validation', () => {
it('should validate correct tokens', () => {
expect(TokenManager.validateToken(validToken)).toBe(true);
});
it('should reject expired tokens', () => {
expect(TokenManager.validateToken(expiredToken)).toBe(false);
});
it('should reject malformed tokens', () => {
const malformedTokens = [
'not.a.token',
'invalid-token-format',
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9',
'',
'null',
'undefined'
];
malformedTokens.forEach(token => {
expect(TokenManager.validateToken(token)).toBe(false);
});
});
it('should reject tokens with invalid signature', () => {
const tamperedToken = validToken.slice(0, -1) + 'X';
expect(TokenManager.validateToken(tamperedToken)).toBe(false);
});
it('should handle tokens with missing expiration', () => {
const tokenWithoutExp = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
expect(TokenManager.validateToken(tokenWithoutExp)).toBe(true);
});
});
describe('Security Features', () => {
it('should use secure encryption algorithm', () => {
const encrypted = TokenManager.encryptToken(validToken, encryptionKey);
expect(encrypted).toMatch(/^[A-Za-z0-9+/=]+$/); // Base64 format
expect(encrypted.length).toBeGreaterThan(validToken.length); // Should include IV and tag
});
it('should prevent token tampering', () => {
const encrypted = TokenManager.encryptToken(validToken, encryptionKey);
const tampered = encrypted.slice(0, -1) + 'X';
expect(() => TokenManager.decryptToken(tampered, encryptionKey)).toThrow();
});
it('should use unique IVs for each encryption', () => {
const encrypted1 = TokenManager.encryptToken(validToken, encryptionKey);
const encrypted2 = TokenManager.encryptToken(validToken, encryptionKey);
const encrypted3 = TokenManager.encryptToken(validToken, encryptionKey);
// Each encryption should be different due to unique IVs
expect(new Set([encrypted1, encrypted2, encrypted3]).size).toBe(3);
});
it('should handle large tokens', () => {
const largeToken = validToken.repeat(10); // Create a much larger token
const encrypted = TokenManager.encryptToken(largeToken, encryptionKey);
const decrypted = TokenManager.decryptToken(encrypted, encryptionKey);
expect(decrypted).toBe(largeToken);
});
});
describe('Error Handling', () => {
it('should throw descriptive errors for invalid inputs', () => {
expect(() => TokenManager.encryptToken(null as any, encryptionKey))
.toThrow(/invalid/i);
expect(() => TokenManager.encryptToken(validToken, null as any))
.toThrow(/invalid/i);
expect(() => TokenManager.decryptToken('invalid-base64', encryptionKey))
.toThrow(/invalid/i);
});
it('should handle corrupted encrypted data', () => {
const encrypted = TokenManager.encryptToken(validToken, encryptionKey);
const corrupted = encrypted.substring(10); // Remove part of the encrypted data
expect(() => TokenManager.decryptToken(corrupted, encryptionKey))
.toThrow();
});
it('should handle invalid base64 input', () => {
expect(() => TokenManager.decryptToken('not-base64!@#$', encryptionKey))
.toThrow(/invalid/i);
});
it('should handle undefined and null inputs', () => {
expect(() => TokenManager.validateToken(undefined as any)).toBe(false);
expect(() => TokenManager.validateToken(null as any)).toBe(false);
});
});
});