Checkpoint/.tests/behavioral-middleware.test.js
2025-08-02 15:34:04 -05:00

411 lines
No EOL
14 KiB
JavaScript

import { jest } from '@jest/globals';
// Mock dependencies
jest.unstable_mockModule('../dist/utils/behavioral-detection.js', () => ({
behavioralDetection: {
config: { enabled: true, Responses: {} },
isBlocked: jest.fn(),
getRateLimit: jest.fn(),
analyzeRequest: jest.fn()
}
}));
jest.unstable_mockModule('../dist/utils/network.js', () => ({
getRealIP: jest.fn()
}));
jest.unstable_mockModule('../dist/utils/logs.js', () => ({
plugin: jest.fn(),
error: jest.fn()
}));
// Import the modules after mocking
const BehavioralDetectionMiddleware = (await import('../dist/utils/behavioral-middleware.js')).default;
const { behavioralDetection } = await import('../dist/utils/behavioral-detection.js');
const { getRealIP } = await import('../dist/utils/network.js');
const logs = await import('../dist/utils/logs.js');
describe('Behavioral Middleware', () => {
let mockReq, mockRes, mockNext;
let activeTimeouts = [];
let activeImmediates = [];
// Track async operations for cleanup
const originalSetTimeout = global.setTimeout;
const originalSetImmediate = global.setImmediate;
global.setTimeout = (fn, delay, ...args) => {
const id = originalSetTimeout(fn, delay, ...args);
activeTimeouts.push(id);
return id;
};
global.setImmediate = (fn, ...args) => {
const id = originalSetImmediate(fn, ...args);
activeImmediates.push(id);
return id;
};
beforeEach(() => {
jest.clearAllMocks();
activeTimeouts = [];
activeImmediates = [];
// Mock Express request object
mockReq = {
url: '/api/test',
method: 'GET',
headers: {
'user-agent': 'test-agent',
'x-forwarded-for': '192.168.1.1'
},
ip: '192.168.1.1'
};
// Mock Express response object
mockRes = {
statusCode: 200,
status: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
end: jest.fn(),
json: jest.fn(),
send: jest.fn(),
locals: {}
};
// Mock next function
mockNext = jest.fn();
// Default mock returns
getRealIP.mockReturnValue('192.168.1.1');
behavioralDetection.isBlocked.mockResolvedValue({ blocked: false });
behavioralDetection.getRateLimit.mockResolvedValue(null);
behavioralDetection.analyzeRequest.mockResolvedValue({ totalScore: 0, patterns: [] });
});
afterEach(() => {
// Clear any pending timeouts and immediates
activeTimeouts.forEach(id => clearTimeout(id));
activeImmediates.forEach(id => clearImmediate(id));
activeTimeouts = [];
activeImmediates = [];
});
afterAll(() => {
// Restore original functions
global.setTimeout = originalSetTimeout;
global.setImmediate = originalSetImmediate;
});
describe('plugin creation', () => {
test('should create a behavioral detection middleware plugin', () => {
const plugin = BehavioralDetectionMiddleware();
expect(plugin.name).toBe('behavioral-detection');
expect(plugin.priority).toBe(90);
expect(typeof plugin.middleware).toBe('function');
});
});
describe('middleware execution', () => {
test('should skip processing when behavioral detection is disabled', async () => {
behavioralDetection.config.enabled = false;
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
expect(behavioralDetection.isBlocked).not.toHaveBeenCalled();
// Restore enabled state
behavioralDetection.config.enabled = true;
});
test('should process request when behavioral detection is enabled', async () => {
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
expect(getRealIP).toHaveBeenCalledWith(mockReq);
});
test('should capture client IP correctly', async () => {
const plugin = BehavioralDetectionMiddleware();
getRealIP.mockReturnValue('10.0.0.1');
await plugin.middleware(mockReq, mockRes, mockNext);
expect(getRealIP).toHaveBeenCalledWith(mockReq);
});
});
describe('blocking functionality', () => {
test('should block requests from blocked IPs', async () => {
behavioralDetection.isBlocked.mockResolvedValue({
blocked: true,
reason: 'Malicious activity detected'
});
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
// Trigger response capture by calling end and wait for async processing
mockRes.end();
// Wait for setImmediate and async processing to complete
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
expect(mockRes.status).toHaveBeenCalledWith(403);
expect(mockRes.setHeader).toHaveBeenCalledWith('X-Behavioral-Block', 'true');
expect(mockRes.setHeader).toHaveBeenCalledWith('X-Block-Reason', 'Malicious activity detected');
expect(logs.plugin).toHaveBeenCalledWith('behavioral', expect.stringContaining('Blocked IP'));
});
test('should use default block message when none configured', async () => {
behavioralDetection.isBlocked.mockResolvedValue({
blocked: true,
reason: 'Suspicious activity'
});
behavioralDetection.config.Responses.BlockMessage = undefined;
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end();
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
expect(mockRes.status).toHaveBeenCalledWith(403);
});
test('should handle blocked IP without reason', async () => {
behavioralDetection.isBlocked.mockResolvedValue({
blocked: true
});
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end();
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
expect(mockRes.setHeader).toHaveBeenCalledWith('X-Block-Reason', 'suspicious activity');
});
});
describe('rate limiting functionality', () => {
test('should apply rate limiting when limit exceeded', async () => {
// Make sure isBlocked returns false so rate limiting is checked
behavioralDetection.isBlocked.mockResolvedValue({ blocked: false });
behavioralDetection.getRateLimit.mockResolvedValue({
exceeded: true,
requests: 150,
limit: 100,
window: 60000,
resetTime: Date.now() + 60000
});
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end();
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
expect(mockRes.status).toHaveBeenCalledWith(429);
expect(mockRes.setHeader).toHaveBeenCalledWith('X-RateLimit-Limit', '100');
expect(mockRes.setHeader).toHaveBeenCalledWith('X-RateLimit-Remaining', '0');
expect(mockRes.setHeader).toHaveBeenCalledWith('Retry-After', '60');
expect(logs.plugin).toHaveBeenCalledWith('behavioral', expect.stringContaining('Rate limit exceeded'));
});
test('should set rate limit headers for non-exceeded limits', async () => {
behavioralDetection.isBlocked.mockResolvedValue({ blocked: false });
behavioralDetection.getRateLimit.mockResolvedValue({
exceeded: false,
requests: 50,
limit: 100,
resetTime: Date.now() + 30000
});
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end();
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
expect(mockRes.setHeader).toHaveBeenCalledWith('X-RateLimit-Limit', '100');
expect(mockRes.setHeader).toHaveBeenCalledWith('X-RateLimit-Remaining', '50');
expect(mockRes.status).not.toHaveBeenCalledWith(429);
});
});
describe('behavioral analysis', () => {
test('should analyze request and set behavioral headers', async () => {
behavioralDetection.isBlocked.mockResolvedValue({ blocked: false });
behavioralDetection.getRateLimit.mockResolvedValue(null);
behavioralDetection.analyzeRequest.mockResolvedValue({
totalScore: 25,
patterns: [
{ name: 'rapid_requests', score: 15 },
{ name: 'suspicious_user_agent', score: 10 }
]
});
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end();
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
expect(behavioralDetection.analyzeRequest).toHaveBeenCalledWith(
'192.168.1.1',
mockReq,
expect.objectContaining({
status: 200,
responseTime: expect.any(Number)
})
);
expect(mockRes.setHeader).toHaveBeenCalledWith('X-Behavioral-Score', '25');
expect(mockRes.setHeader).toHaveBeenCalledWith('X-Behavioral-Patterns', 'rapid_requests, suspicious_user_agent');
});
test('should store behavioral signals in response locals', async () => {
behavioralDetection.isBlocked.mockResolvedValue({ blocked: false });
behavioralDetection.getRateLimit.mockResolvedValue(null);
const analysis = {
totalScore: 10,
patterns: [{ name: 'test_pattern', score: 10 }]
};
behavioralDetection.analyzeRequest.mockResolvedValue(analysis);
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end();
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
expect(mockRes.locals.behavioralSignals).toEqual(analysis);
});
test('should not set headers when no patterns detected', async () => {
behavioralDetection.isBlocked.mockResolvedValue({ blocked: false });
behavioralDetection.getRateLimit.mockResolvedValue(null);
behavioralDetection.analyzeRequest.mockResolvedValue({
totalScore: 0,
patterns: []
});
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end();
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
expect(mockRes.setHeader).not.toHaveBeenCalledWith('X-Behavioral-Score', expect.anything());
expect(mockRes.setHeader).not.toHaveBeenCalledWith('X-Behavioral-Patterns', expect.anything());
});
});
describe('response method interception', () => {
test('should intercept res.end() calls', async () => {
const originalEnd = mockRes.end;
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end('test data');
// Should have called the original method
expect(originalEnd).toHaveBeenCalledWith('test data');
});
test('should intercept res.json() calls', async () => {
const originalJson = mockRes.json;
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
const testData = { test: 'data' };
mockRes.json(testData);
expect(originalJson).toHaveBeenCalledWith(testData);
});
test('should intercept res.send() calls', async () => {
const originalSend = mockRes.send;
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.send('test response');
expect(originalSend).toHaveBeenCalledWith('test response');
});
});
describe('error handling', () => {
test('should handle errors in behavioral analysis gracefully', async () => {
behavioralDetection.analyzeRequest.mockRejectedValue(new Error('Analysis failed'));
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end();
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setTimeout(resolve, 10)); // Give error handling time
expect(logs.error).toHaveBeenCalledWith('behavioral', expect.stringContaining('Error in behavioral analysis'));
expect(mockNext).toHaveBeenCalled(); // Should not block request flow
});
test('should handle errors in isBlocked check', async () => {
behavioralDetection.isBlocked.mockRejectedValue(new Error('Block check failed'));
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end();
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setTimeout(resolve, 10));
expect(logs.error).toHaveBeenCalledWith('behavioral', expect.stringContaining('Error in behavioral analysis'));
});
test('should fail open for availability on errors', async () => {
behavioralDetection.isBlocked.mockRejectedValue(new Error('Service unavailable'));
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
expect(mockNext).toHaveBeenCalled();
// Should not block the request even if behavioral detection fails
});
});
describe('response locals handling', () => {
test('should handle missing response locals gracefully', async () => {
delete mockRes.locals;
const plugin = BehavioralDetectionMiddleware();
await plugin.middleware(mockReq, mockRes, mockNext);
mockRes.end();
await new Promise(resolve => setImmediate(resolve));
await new Promise(resolve => setImmediate(resolve));
// Should not throw error even without locals
expect(mockNext).toHaveBeenCalled();
});
});
});