test: enhance security middleware and token management tests
- Added comprehensive test coverage for TokenManager encryption and validation methods - Implemented detailed test scenarios for security middleware functions - Updated test cases to handle edge cases and improve input validation - Refactored test mocks to provide more robust and realistic testing environment - Improved error handling and input validation in security-related components
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { Request, Response } from 'express';
|
||||
import { jest, describe, it, expect, beforeEach } from '@jest/globals';
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import {
|
||||
validateRequest,
|
||||
sanitizeInput,
|
||||
@@ -7,160 +8,109 @@ import {
|
||||
securityHeaders
|
||||
} from '../../src/security/index.js';
|
||||
|
||||
interface MockRequest extends Partial<Request> {
|
||||
headers: Record<string, string>;
|
||||
is: jest.Mock;
|
||||
}
|
||||
type MockRequest = {
|
||||
headers: {
|
||||
'content-type'?: string;
|
||||
authorization?: string;
|
||||
};
|
||||
body?: any;
|
||||
is: jest.MockInstance<string | false | null, [type: string | string[]]>;
|
||||
};
|
||||
|
||||
type MockResponse = {
|
||||
status: jest.MockInstance<MockResponse, [code: number]>;
|
||||
json: jest.MockInstance<MockResponse, [body: any]>;
|
||||
setHeader: jest.MockInstance<MockResponse, [name: string, value: string]>;
|
||||
};
|
||||
|
||||
describe('Security Middleware', () => {
|
||||
let mockRequest: MockRequest;
|
||||
let mockResponse: Partial<Response>;
|
||||
let mockNext: jest.Mock;
|
||||
let mockResponse: MockResponse;
|
||||
let nextFunction: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
'authorization': 'Bearer validToken'
|
||||
},
|
||||
is: jest.fn().mockReturnValue(true),
|
||||
body: { test: 'data' }
|
||||
headers: {},
|
||||
body: {},
|
||||
is: jest.fn<string | false | null, [string | string[]]>().mockReturnValue('json')
|
||||
};
|
||||
|
||||
mockResponse = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn(),
|
||||
setHeader: jest.fn(),
|
||||
set: jest.fn()
|
||||
status: jest.fn<MockResponse, [number]>().mockReturnThis(),
|
||||
json: jest.fn<MockResponse, [any]>().mockReturnThis(),
|
||||
setHeader: jest.fn<MockResponse, [string, string]>().mockReturnThis()
|
||||
};
|
||||
mockNext = jest.fn();
|
||||
|
||||
nextFunction = 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();
|
||||
mockRequest.headers.authorization = 'Bearer valid-token';
|
||||
validateRequest(mockRequest as unknown as Request, mockResponse as unknown as Response, nextFunction);
|
||||
expect(nextFunction).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
|
||||
);
|
||||
it('should reject requests without authorization header', () => {
|
||||
validateRequest(mockRequest as unknown as Request, mockResponse as unknown as Response, nextFunction);
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
error: 'Invalid or expired token'
|
||||
});
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
|
||||
error: expect.stringContaining('authorization')
|
||||
}));
|
||||
});
|
||||
|
||||
it('should reject requests with invalid token', () => {
|
||||
mockRequest.headers.authorization = 'Bearer invalid.token.format';
|
||||
validateRequest(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
mockNext
|
||||
);
|
||||
it('should reject requests with invalid authorization format', () => {
|
||||
mockRequest.headers.authorization = 'invalid-format';
|
||||
validateRequest(mockRequest as unknown as Request, mockResponse as unknown as Response, nextFunction);
|
||||
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();
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(expect.objectContaining({
|
||||
error: expect.stringContaining('Bearer')
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('Input Sanitization', () => {
|
||||
it('should remove HTML tags from request body', () => {
|
||||
it('should pass requests without body', () => {
|
||||
delete mockRequest.body;
|
||||
sanitizeInput(mockRequest as unknown as Request, mockResponse as unknown as Response, nextFunction);
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should sanitize HTML in request body', () => {
|
||||
mockRequest.body = {
|
||||
text: '<script>alert("xss")</script>',
|
||||
text: '<script>alert("xss")</script>Hello',
|
||||
nested: {
|
||||
html: '<img src="x" onerror="alert(1)">'
|
||||
html: '<img src="x" onerror="alert(1)">World'
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
sanitizeInput(mockRequest as unknown as Request, mockResponse as unknown as Response, nextFunction);
|
||||
expect(mockRequest.body.text).toBe('Hello');
|
||||
expect(mockRequest.body.nested.html).toBe('World');
|
||||
expect(nextFunction).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 non-object bodies', () => {
|
||||
mockRequest.body = '<p>text</p>';
|
||||
sanitizeInput(mockRequest as unknown as Request, mockResponse as unknown as Response, nextFunction);
|
||||
expect(mockRequest.body).toBe('text');
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle nested objects', () => {
|
||||
it('should preserve non-string values', () => {
|
||||
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
|
||||
boolean: true,
|
||||
null: null,
|
||||
array: [1, 2, 3]
|
||||
};
|
||||
|
||||
const originalBody = { ...mockRequest.body };
|
||||
sanitizeInput(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
mockNext
|
||||
);
|
||||
|
||||
expect(mockRequest.body).toEqual(originalBody);
|
||||
expect(mockNext).toHaveBeenCalled();
|
||||
sanitizeInput(mockRequest as unknown as Request, mockResponse as unknown as Response, nextFunction);
|
||||
expect(mockRequest.body).toEqual({
|
||||
number: 42,
|
||||
boolean: true,
|
||||
null: null,
|
||||
array: [1, 2, 3]
|
||||
});
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -174,36 +124,21 @@ 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,
|
||||
mockNext
|
||||
);
|
||||
|
||||
errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
error: 'Internal Server Error',
|
||||
message: undefined
|
||||
error: 'Internal Server Error'
|
||||
});
|
||||
});
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
error: 'Internal Server Error',
|
||||
message: 'Test error'
|
||||
error: 'Test error',
|
||||
stack: expect.any(String)
|
||||
});
|
||||
});
|
||||
|
||||
@@ -214,7 +149,7 @@ describe('Security Middleware', () => {
|
||||
error as any,
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
mockNext
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
||||
@@ -231,18 +166,12 @@ describe('Security Middleware', () => {
|
||||
});
|
||||
|
||||
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();
|
||||
securityHeaders(mockRequest as Request, mockResponse as Response, 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');
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user