import { describe, it, expect, beforeEach, jest } from '@jest/globals'; import { RateLimiterPlugin } from '../../../src/guardrails/plugins/rate-limiter'; import { createGuardrailContext } from '../../../src/guardrails/context'; // Mock logger to avoid console noise during tests jest.mock('../../../src/utils/logger'); describe('RateLimiterPlugin', () => { let plugin: RateLimiterPlugin; beforeEach(async () => { plugin = new RateLimiterPlugin(); plugin.reset(); }); describe('initialization', () => { it('should initialize with default values', async () => { await plugin.initialize({ enabled: true }); expect(plugin.enabled).toBe(true); expect(plugin.name).toBe('rate_limiter'); expect(plugin.phases).toContain('pre_request'); }); it('should initialize with custom config', async () => { await plugin.initialize({ enabled: true, priority: 5, requests_per_minute: 30, requests_per_hour: 500, per_provider: true, burst_allowance: 10, }); expect(plugin.priority).toBe(5); }); }); describe('rate limiting - per minute', () => { it('should allow requests under the limit', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 10, requests_per_hour: 1000, burst_allowance: 0, }); const context = createGuardrailContext({ provider: 'openai' }); // Make 5 requests - all should be allowed for (let i = 0; i < 5; i++) { const result = await plugin.execute('pre_request', context); expect(result.action).toBe('allow'); } expect(plugin.getRequestCounts().lastMinute).toBe(5); }); it('should block requests over the limit', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 3, requests_per_hour: 1000, burst_allowance: 0, }); const context = createGuardrailContext({ provider: 'openai' }); // Make 3 requests - all should be allowed for (let i = 0; i < 3; i++) { const result = await plugin.execute('pre_request', context); expect(result.action).toBe('allow'); } // 4th request should be blocked const result = await plugin.execute('pre_request', context); expect(result.action).toBe('block'); expect(result.blockedBy).toBe('rate_limiter'); expect(result.blockReason).toContain('Rate limit exceeded'); }); it('should allow burst requests within burst allowance', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 3, requests_per_hour: 1000, burst_allowance: 2, }); const context = createGuardrailContext({ provider: 'openai' }); // Make 5 requests - all should be allowed (3 + 2 burst) for (let i = 0; i < 5; i++) { const result = await plugin.execute('pre_request', context); expect(result.action).toBe('allow'); } // 6th request should be blocked const result = await plugin.execute('pre_request', context); expect(result.action).toBe('block'); }); }); describe('rate limiting - per provider', () => { it('should track requests globally by default', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 5, requests_per_hour: 1000, per_provider: false, burst_allowance: 0, }); // Make requests for different providers for (let i = 0; i < 5; i++) { const provider = i % 2 === 0 ? 'openai' : 'google'; const context = createGuardrailContext({ provider }); await plugin.execute('pre_request', context); } // Global count should be 5 expect(plugin.getRequestCounts('global').lastMinute).toBe(5); // Next request from any provider should be blocked const context = createGuardrailContext({ provider: 'openai' }); const result = await plugin.execute('pre_request', context); expect(result.action).toBe('block'); }); it('should track requests per provider when configured', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 3, requests_per_hour: 1000, per_provider: true, burst_allowance: 0, }); // Make 3 requests for OpenAI for (let i = 0; i < 3; i++) { const context = createGuardrailContext({ provider: 'openai' }); const result = await plugin.execute('pre_request', context); expect(result.action).toBe('allow'); } // OpenAI should be blocked const openaiContext = createGuardrailContext({ provider: 'openai' }); const openaiResult = await plugin.execute('pre_request', openaiContext); expect(openaiResult.action).toBe('block'); // But Google should still be allowed const googleContext = createGuardrailContext({ provider: 'google' }); const googleResult = await plugin.execute('pre_request', googleContext); expect(googleResult.action).toBe('allow'); expect(plugin.getRequestCounts('openai').lastMinute).toBe(3); expect(plugin.getRequestCounts('google').lastMinute).toBe(1); }); }); describe('rate limiting - per hour', () => { it('should block requests when hourly limit exceeded', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 1000, // High minute limit requests_per_hour: 5, // Low hour limit burst_allowance: 0, }); const context = createGuardrailContext({ provider: 'openai' }); // Make 5 requests - all should be allowed for (let i = 0; i < 5; i++) { const result = await plugin.execute('pre_request', context); expect(result.action).toBe('allow'); } // 6th request should be blocked due to hourly limit const result = await plugin.execute('pre_request', context); expect(result.action).toBe('block'); expect(result.blockReason).toContain('per hour'); }); it('should add violation for hourly limit exceeded', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 1000, requests_per_hour: 3, burst_allowance: 0, }); const context = createGuardrailContext({ provider: 'openai' }); // Make 3 requests for (let i = 0; i < 3; i++) { await plugin.execute('pre_request', context); } // 4th request - should be blocked with hourly violation const result = await plugin.execute('pre_request', context); expect(result.action).toBe('block'); const hourlyViolation = context.violations.find((v) => v.rule === 'requests_per_hour'); expect(hourlyViolation).toBeDefined(); expect(hourlyViolation?.severity).toBe('error'); }); }); describe('violations', () => { it('should add violation when rate limit exceeded', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 1, requests_per_hour: 1000, burst_allowance: 0, }); const context = createGuardrailContext({ provider: 'openai' }); // First request await plugin.execute('pre_request', context); // Second request - should be blocked with violation const result = await plugin.execute('pre_request', context); expect(result.action).toBe('block'); expect(context.violations.length).toBeGreaterThan(0); expect(context.violations[0].rule).toBe('requests_per_minute'); expect(context.violations[0].severity).toBe('error'); }); it('should add warning when approaching limit', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 10, requests_per_hour: 1000, burst_allowance: 0, }); const context = createGuardrailContext({ provider: 'openai' }); // Make 9 requests - the 9th sees 8 previous requests (80% of limit) // Warning is added when request count before current >= 80% for (let i = 0; i < 9; i++) { await plugin.execute('pre_request', context); } // Check for warning violation const warnings = context.violations.filter( (v) => v.severity === 'warning' && v.rule === 'requests_per_minute_warning' ); expect(warnings.length).toBeGreaterThan(0); }); }); describe('phase handling', () => { it('should only process pre_request phase', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 1 }); const context = createGuardrailContext({ provider: 'openai' }); // pre_request should be processed const preResult = await plugin.execute('pre_request', context); expect(preResult.action).toBe('allow'); // post_response should be skipped (not in phases) const postResult = await plugin.execute('post_response', context); expect(postResult.action).toBe('allow'); // Only 1 request counted (pre_request) expect(plugin.getRequestCounts().lastMinute).toBe(1); }); }); describe('shutdown', () => { it('should disable plugin on shutdown', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 10 }); expect(plugin.enabled).toBe(true); await plugin.shutdown(); expect(plugin.enabled).toBe(false); }); }); describe('reset', () => { it('should clear request history on reset', async () => { await plugin.initialize({ enabled: true, requests_per_minute: 10, burst_allowance: 0, }); const context = createGuardrailContext({ provider: 'openai' }); // Make some requests for (let i = 0; i < 5; i++) { await plugin.execute('pre_request', context); } expect(plugin.getRequestCounts().lastMinute).toBe(5); // Reset plugin.reset(); expect(plugin.getRequestCounts().lastMinute).toBe(0); }); }); });