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:
jango-blockchained
2025-02-03 22:41:22 +01:00
parent c519d250a1
commit 481dc5b1a8
11 changed files with 403 additions and 476 deletions

View File

@@ -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)
});
});
});
});