chore: add Bun types and update TypeScript configuration for Bun runtime
- Added `bun-types` to package.json dev dependencies - Updated tsconfig.json to include Bun types and test directory - Updated README.md with correct author attribution - Enhanced test configurations to support Bun testing environment
This commit is contained in:
@@ -1,302 +1,150 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { middleware } from '../index';
|
||||
import { Request, Response } from 'express';
|
||||
import { validateRequest, sanitizeInput, errorHandler } from '../index';
|
||||
import { TokenManager } from '../../security/index';
|
||||
|
||||
describe('Security Middleware', () => {
|
||||
let mockRequest: Partial<Request>;
|
||||
let mockResponse: Partial<Response>;
|
||||
let nextFunction: NextFunction;
|
||||
let nextFunction: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRequest = {
|
||||
headers: {},
|
||||
headers: {
|
||||
'content-type': 'application/json'
|
||||
},
|
||||
body: {},
|
||||
ip: '127.0.0.1',
|
||||
method: 'POST',
|
||||
is: jest.fn(),
|
||||
is: jest.fn((type: string | string[]) => type === 'application/json' ? 'application/json' : false)
|
||||
};
|
||||
|
||||
mockResponse = {
|
||||
status: jest.fn().mockReturnThis(),
|
||||
json: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn().mockReturnThis(),
|
||||
setHeader: jest.fn()
|
||||
};
|
||||
|
||||
nextFunction = jest.fn();
|
||||
});
|
||||
|
||||
describe('authenticate', () => {
|
||||
it('should pass valid authentication', () => {
|
||||
const token = 'valid_token';
|
||||
mockRequest.headers = { authorization: `Bearer ${token}` };
|
||||
describe('Request Validation', () => {
|
||||
it('should pass valid requests', () => {
|
||||
mockRequest.headers = {
|
||||
'authorization': 'Bearer valid-token',
|
||||
'content-type': 'application/json'
|
||||
};
|
||||
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({ valid: true });
|
||||
|
||||
middleware.authenticate(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
validateRequest(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject invalid token', () => {
|
||||
const token = 'invalid_token';
|
||||
mockRequest.headers = { authorization: `Bearer ${token}` };
|
||||
jest.spyOn(TokenManager, 'validateToken').mockReturnValue({
|
||||
valid: false,
|
||||
error: 'Invalid token'
|
||||
});
|
||||
|
||||
middleware.authenticate(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
it('should reject requests without authorization header', () => {
|
||||
validateRequest(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Unauthorized'
|
||||
})
|
||||
);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
message: 'Unauthorized',
|
||||
error: 'Missing or invalid authorization header',
|
||||
timestamp: expect.any(String)
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle missing authorization header', () => {
|
||||
middleware.authenticate(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
it('should reject requests with invalid authorization format', () => {
|
||||
mockRequest.headers = {
|
||||
'authorization': 'invalid-format',
|
||||
'content-type': 'application/json'
|
||||
};
|
||||
validateRequest(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe('securityHeaders', () => {
|
||||
it('should set all required security headers', () => {
|
||||
middleware.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(
|
||||
'Strict-Transport-Security',
|
||||
expect.stringContaining('max-age=31536000')
|
||||
);
|
||||
expect(mockResponse.setHeader).toHaveBeenCalledWith(
|
||||
'Content-Security-Policy',
|
||||
expect.any(String)
|
||||
);
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateRequest', () => {
|
||||
it('should pass valid requests', () => {
|
||||
mockRequest.is = jest.fn().mockReturnValue('application/json');
|
||||
mockRequest.body = { test: 'data' };
|
||||
Object.defineProperty(mockRequest, 'path', {
|
||||
get: () => '/api/test',
|
||||
configurable: true
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
message: 'Unauthorized',
|
||||
error: 'Missing or invalid authorization header',
|
||||
timestamp: expect.any(String)
|
||||
});
|
||||
|
||||
middleware.validateRequest(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should reject invalid content type', () => {
|
||||
mockRequest.is = jest.fn().mockReturnValue(false);
|
||||
Object.defineProperty(mockRequest, 'path', {
|
||||
get: () => '/api/test',
|
||||
configurable: true
|
||||
});
|
||||
|
||||
middleware.validateRequest(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(415);
|
||||
});
|
||||
|
||||
it('should reject oversized requests', () => {
|
||||
mockRequest.headers = { 'content-length': '2097152' }; // 2MB
|
||||
mockRequest.is = jest.fn().mockReturnValue('application/json');
|
||||
Object.defineProperty(mockRequest, 'path', {
|
||||
get: () => '/api/test',
|
||||
configurable: true
|
||||
});
|
||||
|
||||
middleware.validateRequest(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
mockRequest.headers = {
|
||||
'authorization': 'Bearer valid-token',
|
||||
'content-type': 'application/json',
|
||||
'content-length': '1048577' // 1MB + 1 byte
|
||||
};
|
||||
validateRequest(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(413);
|
||||
});
|
||||
|
||||
it('should skip validation for health check endpoints', () => {
|
||||
Object.defineProperty(mockRequest, 'path', {
|
||||
get: () => '/health',
|
||||
configurable: true
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
message: 'Payload Too Large',
|
||||
error: 'Request body must not exceed 1048576 bytes',
|
||||
timestamp: expect.any(String)
|
||||
});
|
||||
|
||||
middleware.validateRequest(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
expect(mockResponse.status).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('sanitizeInput', () => {
|
||||
describe('Input Sanitization', () => {
|
||||
it('should sanitize HTML in request body', () => {
|
||||
mockRequest.body = {
|
||||
text: '<script>alert("xss")</script>Hello',
|
||||
text: 'Test <script>alert("xss")</script>',
|
||||
nested: {
|
||||
html: '<img src="x" onerror="alert(1)">World'
|
||||
}
|
||||
};
|
||||
|
||||
middleware.sanitizeInput(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(mockRequest.body.text).not.toContain('<script>');
|
||||
expect(mockRequest.body.nested.html).not.toContain('<img');
|
||||
sanitizeInput(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
expect(mockRequest.body.text).toBe('Test ');
|
||||
expect(mockRequest.body.nested.html).toBe('World');
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle non-object bodies', () => {
|
||||
mockRequest.body = '<p>text</p>';
|
||||
|
||||
middleware.sanitizeInput(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(mockRequest.body).not.toContain('<p>');
|
||||
sanitizeInput(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
expect(mockRequest.body).toBe('text');
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should preserve non-string values', () => {
|
||||
const body = {
|
||||
number: 42,
|
||||
mockRequest.body = {
|
||||
number: 123,
|
||||
boolean: true,
|
||||
null: null,
|
||||
array: [1, 2, 3]
|
||||
array: [1, 2, 3],
|
||||
nested: { value: 456 }
|
||||
};
|
||||
mockRequest.body = { ...body };
|
||||
|
||||
middleware.sanitizeInput(
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(mockRequest.body).toEqual(body);
|
||||
sanitizeInput(mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
expect(mockRequest.body).toEqual({
|
||||
number: 123,
|
||||
boolean: true,
|
||||
array: [1, 2, 3],
|
||||
nested: { value: 456 }
|
||||
});
|
||||
expect(nextFunction).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('errorHandler', () => {
|
||||
it('should handle ValidationError', () => {
|
||||
const error = new Error('Validation failed');
|
||||
error.name = 'ValidationError';
|
||||
|
||||
middleware.errorHandler(
|
||||
error,
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(400);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Validation Error'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle UnauthorizedError', () => {
|
||||
const error = new Error('Unauthorized access');
|
||||
error.name = 'UnauthorizedError';
|
||||
|
||||
middleware.errorHandler(
|
||||
error,
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(401);
|
||||
});
|
||||
|
||||
it('should handle generic errors', () => {
|
||||
const error = new Error('Something went wrong');
|
||||
|
||||
middleware.errorHandler(
|
||||
error,
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
success: false,
|
||||
message: 'Internal Server Error'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should hide error details in production', () => {
|
||||
const originalEnv = process.env.NODE_ENV;
|
||||
describe('Error Handler', () => {
|
||||
it('should handle errors in production mode', () => {
|
||||
process.env.NODE_ENV = 'production';
|
||||
const error = new Error('Sensitive error details');
|
||||
const error = new Error('Test error');
|
||||
errorHandler(error, mockRequest as Request, mockResponse as Response, nextFunction);
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
message: 'Internal Server Error',
|
||||
error: 'An unexpected error occurred',
|
||||
timestamp: expect.any(String)
|
||||
});
|
||||
});
|
||||
|
||||
middleware.errorHandler(
|
||||
error,
|
||||
mockRequest as Request,
|
||||
mockResponse as Response,
|
||||
nextFunction
|
||||
);
|
||||
|
||||
expect(mockResponse.json).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
error: 'An unexpected error occurred'
|
||||
})
|
||||
);
|
||||
|
||||
process.env.NODE_ENV = originalEnv;
|
||||
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);
|
||||
expect(mockResponse.status).toHaveBeenCalledWith(500);
|
||||
expect(mockResponse.json).toHaveBeenCalledWith({
|
||||
success: false,
|
||||
message: 'Internal Server Error',
|
||||
error: 'Test error',
|
||||
stack: expect.any(String),
|
||||
timestamp: expect.any(String)
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user