Enhance security middleware and package dependencies

- Updated security headers configuration with stricter settings
- Modified rate limiting and helmet middleware setup
- Added TypeScript type definitions for Express, rate-limit, and Helmet
- Refined referrer policy and HSTS configuration
- Improved security middleware chain for better protection
This commit is contained in:
jango-blockchained
2025-01-30 09:27:22 +01:00
parent 110f2a308c
commit 585b8d1f91
8 changed files with 1277 additions and 150 deletions

View File

@@ -0,0 +1,272 @@
import { jest } from '@jest/globals';
import { ContextManager, ResourceType, RelationType, ResourceState, ResourceRelationship } from '../../src/context/index.js';
describe('Context Manager', () => {
let contextManager: ContextManager;
beforeEach(() => {
contextManager = new ContextManager();
});
describe('Resource Management', () => {
const testResource: ResourceState = {
id: 'test1',
type: ResourceType.DEVICE,
state: 'on',
attributes: { name: 'Test Device' },
lastUpdated: Date.now()
};
it('should add resources', () => {
const addHandler = jest.fn();
contextManager.on('resource_added', addHandler);
contextManager.addResource(testResource);
const resource = contextManager.getResource('test1');
expect(resource).toEqual(testResource);
expect(addHandler).toHaveBeenCalledWith(testResource);
});
it('should update resources', () => {
const updateHandler = jest.fn();
contextManager.on('resource_updated', updateHandler);
contextManager.addResource(testResource);
contextManager.updateResource('test1', {
state: 'off',
attributes: { ...testResource.attributes, brightness: 50 }
});
const resource = contextManager.getResource('test1');
expect(resource?.state).toBe('off');
expect(resource?.attributes.brightness).toBe(50);
expect(updateHandler).toHaveBeenCalled();
});
it('should remove resources', () => {
const removeHandler = jest.fn();
contextManager.on('resource_removed', removeHandler);
contextManager.addResource(testResource);
contextManager.removeResource('test1');
expect(contextManager.getResource('test1')).toBeUndefined();
expect(removeHandler).toHaveBeenCalledWith(testResource);
});
it('should get resources by type', () => {
const resources = [
testResource,
{
id: 'test2',
type: ResourceType.DEVICE,
state: 'off',
attributes: {},
lastUpdated: Date.now()
},
{
id: 'area1',
type: ResourceType.AREA,
state: 'active',
attributes: {},
lastUpdated: Date.now()
}
];
resources.forEach(r => contextManager.addResource(r));
const devices = contextManager.getResourcesByType(ResourceType.DEVICE);
expect(devices).toHaveLength(2);
expect(devices.every((d: ResourceState) => d.type === ResourceType.DEVICE)).toBe(true);
});
});
describe('Relationship Management', () => {
const testRelationship: ResourceRelationship = {
sourceId: 'device1',
targetId: 'area1',
type: RelationType.CONTAINS
};
beforeEach(() => {
// Add test resources
contextManager.addResource({
id: 'device1',
type: ResourceType.DEVICE,
state: 'on',
attributes: {},
lastUpdated: Date.now()
});
contextManager.addResource({
id: 'area1',
type: ResourceType.AREA,
state: 'active',
attributes: {},
lastUpdated: Date.now()
});
});
it('should add relationships', () => {
const addHandler = jest.fn();
contextManager.on('relationship_added', addHandler);
contextManager.addRelationship(testRelationship);
const related = contextManager.getRelatedResources('device1');
expect(related).toHaveLength(2);
expect(related.some(r => r.id === 'area1')).toBe(true);
expect(addHandler).toHaveBeenCalledWith(testRelationship);
});
it('should remove relationships', () => {
const removeHandler = jest.fn();
contextManager.on('relationship_removed', removeHandler);
contextManager.addRelationship(testRelationship);
contextManager.removeRelationship(
'device1',
'area1',
RelationType.CONTAINS
);
const related = contextManager.getRelatedResources('device1');
expect(related).toHaveLength(0);
expect(removeHandler).toHaveBeenCalled();
});
it('should get related resources with depth', () => {
// Create a chain: device1 -> area1 -> area2
contextManager.addResource({
id: 'area2',
type: ResourceType.AREA,
state: 'active',
attributes: {},
lastUpdated: Date.now()
});
contextManager.addRelationship({
sourceId: 'device1',
targetId: 'area1',
type: RelationType.CONTAINS
});
contextManager.addRelationship({
sourceId: 'area1',
targetId: 'area2',
type: RelationType.CONTAINS
});
const relatedDepth1 = contextManager.getRelatedResources('device1', undefined, 1);
expect(relatedDepth1).toHaveLength(3);
const relatedDepth2 = contextManager.getRelatedResources('device1', undefined, 2);
expect(relatedDepth2).toHaveLength(3);
});
});
describe('Resource Analysis', () => {
beforeEach(() => {
// Setup test resources and relationships
const resources = [
{
id: 'device1',
type: ResourceType.DEVICE,
state: 'on',
attributes: {},
lastUpdated: Date.now()
},
{
id: 'automation1',
type: ResourceType.AUTOMATION,
state: 'on',
attributes: {},
lastUpdated: Date.now()
},
{
id: 'group1',
type: ResourceType.GROUP,
state: 'on',
attributes: {},
lastUpdated: Date.now()
}
];
resources.forEach(r => contextManager.addResource(r));
const relationships = [
{
sourceId: 'automation1',
targetId: 'device1',
type: RelationType.CONTROLS
},
{
sourceId: 'device1',
targetId: 'group1',
type: RelationType.DEPENDS_ON
},
{
sourceId: 'group1',
targetId: 'device1',
type: RelationType.GROUPS
}
];
relationships.forEach(r => contextManager.addRelationship(r));
});
it('should analyze resource usage', () => {
const analysis = contextManager.analyzeResourceUsage('device1');
expect(analysis.dependencies).toHaveLength(1);
expect(analysis.dependencies[0]).toBe('group1');
expect(analysis.dependents).toHaveLength(0);
expect(analysis.groups).toHaveLength(1);
expect(analysis.usage.controlCount).toBe(0);
expect(analysis.usage.triggerCount).toBe(0);
expect(analysis.usage.groupCount).toBe(1);
});
});
describe('Event Subscriptions', () => {
it('should handle resource subscriptions', () => {
const callback = jest.fn();
const unsubscribe = contextManager.subscribeToResource('test1', callback);
contextManager.addResource({
id: 'test1',
type: ResourceType.DEVICE,
state: 'on',
attributes: {},
lastUpdated: Date.now()
});
contextManager.updateResource('test1', { state: 'off' });
expect(callback).toHaveBeenCalled();
unsubscribe();
contextManager.updateResource('test1', { state: 'on' });
expect(callback).toHaveBeenCalledTimes(1);
});
it('should handle type subscriptions', () => {
const callback = jest.fn();
const unsubscribe = contextManager.subscribeToType(ResourceType.DEVICE, callback);
const resource = {
id: 'test1',
type: ResourceType.DEVICE,
state: 'on',
attributes: {},
lastUpdated: Date.now()
};
contextManager.addResource(resource);
contextManager.updateResource('test1', { state: 'off' });
expect(callback).toHaveBeenCalled();
unsubscribe();
contextManager.updateResource('test1', { state: 'on' });
expect(callback).toHaveBeenCalledTimes(1);
});
});
});

View File

@@ -0,0 +1,189 @@
import { PerformanceMonitor, PerformanceOptimizer, Metric } from '../../src/performance/index.js';
import type { MemoryUsage } from 'node:process';
describe('Performance Module', () => {
describe('PerformanceMonitor', () => {
let monitor: PerformanceMonitor;
beforeEach(() => {
monitor = new PerformanceMonitor({
responseTime: 500,
memoryUsage: 1024 * 1024 * 512, // 512MB
cpuUsage: 70
});
});
afterEach(() => {
monitor.stop();
});
it('should collect metrics', () => {
const metricHandler = jest.fn();
monitor.on('metric', metricHandler);
monitor.start();
// Wait for first collection
return new Promise(resolve => setTimeout(() => {
expect(metricHandler).toHaveBeenCalled();
const calls = metricHandler.mock.calls;
// Verify memory metrics
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'memory.heapUsed'
)).toBe(true);
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'memory.heapTotal'
)).toBe(true);
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'memory.rss'
)).toBe(true);
// Verify CPU metrics
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'cpu.user'
)).toBe(true);
expect(calls.some(([metric]: [Metric]) =>
metric.name === 'cpu.system'
)).toBe(true);
resolve(true);
}, 100));
});
it('should emit threshold exceeded events', () => {
const thresholdHandler = jest.fn();
monitor = new PerformanceMonitor({
memoryUsage: 1, // Ensure threshold is exceeded
cpuUsage: 1
});
monitor.on('threshold_exceeded', thresholdHandler);
monitor.start();
return new Promise(resolve => setTimeout(() => {
expect(thresholdHandler).toHaveBeenCalled();
resolve(true);
}, 100));
});
it('should clean old metrics', () => {
const now = Date.now();
const oldMetric: Metric = {
name: 'test',
value: 1,
timestamp: now - 25 * 60 * 60 * 1000 // 25 hours old
};
const newMetric: Metric = {
name: 'test',
value: 2,
timestamp: now - 1000 // 1 second old
};
monitor.addMetric(oldMetric);
monitor.addMetric(newMetric);
const metrics = monitor.getMetrics(now - 24 * 60 * 60 * 1000);
expect(metrics).toHaveLength(1);
expect(metrics[0]).toEqual(newMetric);
});
it('should calculate metric averages', () => {
const now = Date.now();
const metrics: Metric[] = [
{ name: 'test', value: 1, timestamp: now - 3000 },
{ name: 'test', value: 2, timestamp: now - 2000 },
{ name: 'test', value: 3, timestamp: now - 1000 }
];
metrics.forEach(metric => monitor.addMetric(metric));
const average = monitor.calculateAverage(
'test',
now - 5000,
now
);
expect(average).toBe(2);
});
});
describe('PerformanceOptimizer', () => {
it('should process batches correctly', async () => {
const items = [1, 2, 3, 4, 5];
const batchSize = 2;
const processor = jest.fn(async (batch: number[]) =>
batch.map(n => n * 2)
);
const results = await PerformanceOptimizer.processBatch(
items,
batchSize,
processor
);
expect(results).toEqual([2, 4, 6, 8, 10]);
expect(processor).toHaveBeenCalledTimes(3); // 2 + 2 + 1 items
});
it('should debounce function calls', (done) => {
const fn = jest.fn();
const debounced = PerformanceOptimizer.debounce(fn, 100);
debounced();
debounced();
debounced();
setTimeout(() => {
expect(fn).not.toHaveBeenCalled();
}, 50);
setTimeout(() => {
expect(fn).toHaveBeenCalledTimes(1);
done();
}, 150);
});
it('should throttle function calls', (done) => {
const fn = jest.fn();
const throttled = PerformanceOptimizer.throttle(fn, 100);
throttled();
throttled();
throttled();
expect(fn).toHaveBeenCalledTimes(1);
setTimeout(() => {
throttled();
expect(fn).toHaveBeenCalledTimes(2);
done();
}, 150);
});
it('should optimize memory when threshold is exceeded', async () => {
const originalGc = global.gc;
global.gc = jest.fn();
const memoryUsage = process.memoryUsage;
process.memoryUsage = jest.fn().mockImplementation((): MemoryUsage => ({
heapUsed: 900,
heapTotal: 1000,
rss: 2000,
external: 0,
arrayBuffers: 0
}));
await PerformanceOptimizer.optimizeMemory();
expect(global.gc).toHaveBeenCalled();
// Cleanup
process.memoryUsage = memoryUsage;
if (originalGc) {
global.gc = originalGc;
} else {
delete global.gc;
}
});
});
});

View File

@@ -0,0 +1,212 @@
import { TokenManager, validateRequest, sanitizeInput, errorHandler } from '../../src/security';
import { Request, Response } from 'express';
describe('Security Module', () => {
describe('TokenManager', () => {
const testToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNzE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
const encryptionKey = 'test_encryption_key';
it('should encrypt and decrypt tokens', () => {
const encrypted = TokenManager.encryptToken(testToken, encryptionKey);
const decrypted = TokenManager.decryptToken(encrypted, encryptionKey);
expect(decrypted).toBe(testToken);
});
it('should validate tokens correctly', () => {
expect(TokenManager.validateToken(testToken)).toBe(true);
expect(TokenManager.validateToken('invalid_token')).toBe(false);
expect(TokenManager.validateToken('')).toBe(false);
});
it('should handle expired tokens', () => {
const expiredToken = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiZXhwIjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c';
expect(TokenManager.validateToken(expiredToken)).toBe(false);
});
});
describe('Request Validation', () => {
let mockRequest: Partial<Request>;
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()
};
mockNext = jest.fn();
});
it('should pass valid requests', () => {
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockNext).toHaveBeenCalled();
expect(mockResponse.status).not.toHaveBeenCalled();
});
it('should reject 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 missing token', () => {
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 invalid request body', () => {
mockRequest.body = null;
validateRequest(
mockRequest as Request,
mockResponse as Response,
mockNext
);
expect(mockResponse.status).toHaveBeenCalledWith(400);
expect(mockResponse.json).toHaveBeenCalledWith({
error: 'Invalid request body'
});
});
});
describe('Input Sanitization', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockNext: jest.Mock;
beforeEach(() => {
mockRequest = {
body: {}
};
mockResponse = {};
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,
mockNext
);
expect(mockRequest.body).toEqual({
text: 'Test alert("xss")',
nested: {
html: 'img src="x" onerror="alert(1)"'
}
});
expect(mockNext).toHaveBeenCalled();
});
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();
});
});
describe('Error Handler', () => {
let mockRequest: Partial<Request>;
let mockResponse: Partial<Response>;
let mockNext: jest.Mock;
const originalEnv = process.env.NODE_ENV;
beforeEach(() => {
mockRequest = {};
mockResponse = {
status: jest.fn().mockReturnThis(),
json: jest.fn()
};
mockNext = jest.fn();
});
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 message 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'
});
});
});
});

View File

@@ -0,0 +1 @@