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