import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; // Mock the config/env module vi.mock('./config/env', () => ({ assertKvConfiguredInProduction: vi.fn(), isKvConfigured: vi.fn(() => false), isProductionEnvironment: vi.fn(() => false), })); describe('rate-limit', () => { const originalEnv = process.env; const mockNow = 1234567890000; beforeEach(() => { vi.resetModules(); vi.clearAllMocks(); process.env = { ...originalEnv }; vi.spyOn(Date, 'now').mockReturnValue(mockNow); }); afterEach(() => { process.env = originalEnv; vi.restoreAllMocks(); }); describe('checkRateLimit - Development Mode', () => { it('should allow first request from a new identifier', async () => { const { checkRateLimit } = await import('./rate-limit'); const result = await checkRateLimit('test-ip-1'); expect(result.allowed).toBe(true); expect(result.remaining).toBe(4); // 5 total - 1 used expect(result.resetAt).toBe(mockNow + 60 * 60 * 1000); // 1 hour }); it('should increment count for subsequent requests within window', async () => { const { checkRateLimit } = await import('./rate-limit'); await checkRateLimit('test-ip-2'); const result = await checkRateLimit('test-ip-2'); expect(result.allowed).toBe(true); expect(result.remaining).toBe(3); // 5 total - 2 used }); it('should block after 5 requests', async () => { const { checkRateLimit } = await import('./rate-limit'); // Make 5 requests for (let i = 0; i < 5; i++) { await checkRateLimit('test-ip-3'); } const result = await checkRateLimit('test-ip-3'); expect(result.allowed).toBe(false); expect(result.remaining).toBe(0); }); it('should reset after window expires', async () => { const { checkRateLimit } = await import('./rate-limit'); // First request await checkRateLimit('test-ip-4'); // Simulate time passing beyond window const futureTime = mockNow + 60 * 60 * 1000 + 1; vi.spyOn(Date, 'now').mockReturnValue(futureTime); const result = await checkRateLimit('test-ip-4'); expect(result.allowed).toBe(true); expect(result.remaining).toBe(4); // Reset to 5 - 1 }); it('should track different identifiers separately', async () => { const { checkRateLimit } = await import('./rate-limit'); await checkRateLimit('ip-1'); await checkRateLimit('ip-1'); const result = await checkRateLimit('ip-2'); expect(result.allowed).toBe(true); expect(result.remaining).toBe(4); // ip-2 is at 1, ip-1 is at 2 }); }); describe('getClientIp', () => { it('should extract IP from x-forwarded-for header', async () => { const { getClientIp } = await import('./rate-limit'); const mockRequest = { headers: new Headers({ 'x-forwarded-for': '192.168.1.1, 10.0.0.1', }), } as Request; const ip = getClientIp(mockRequest); expect(ip).toBe('192.168.1.1'); }); it('should extract IP from x-real-ip header', async () => { const { getClientIp } = await import('./rate-limit'); const mockRequest = { headers: new Headers({ 'x-real-ip': '10.0.0.2', }), } as Request; const ip = getClientIp(mockRequest); expect(ip).toBe('10.0.0.2'); }); it('should extract IP from cf-connecting-ip header', async () => { const { getClientIp } = await import('./rate-limit'); const mockRequest = { headers: new Headers({ 'cf-connecting-ip': '1.2.3.4', }), } as Request; const ip = getClientIp(mockRequest); expect(ip).toBe('1.2.3.4'); }); it('should prioritize x-forwarded-for over other headers', async () => { const { getClientIp } = await import('./rate-limit'); const mockRequest = { headers: new Headers({ 'x-forwarded-for': '192.168.1.1', 'x-real-ip': '10.0.0.2', 'cf-connecting-ip': '1.2.3.4', }), } as Request; const ip = getClientIp(mockRequest); expect(ip).toBe('192.168.1.1'); }); it('should return "unknown" when no IP headers present', async () => { const { getClientIp } = await import('./rate-limit'); const mockRequest = { headers: new Headers({}), } as Request; const ip = getClientIp(mockRequest); expect(ip).toBe('unknown'); }); it('should handle x-forwarded-for with single IP', async () => { const { getClientIp } = await import('./rate-limit'); const mockRequest = { headers: new Headers({ 'x-forwarded-for': '203.0.113.1', }), } as Request; const ip = getClientIp(mockRequest); expect(ip).toBe('203.0.113.1'); }); it('should trim whitespace from IP', async () => { const { getClientIp } = await import('./rate-limit'); const mockRequest = { headers: new Headers({ 'x-forwarded-for': ' 192.168.1.1 ', }), } as Request; const ip = getClientIp(mockRequest); expect(ip).toBe('192.168.1.1'); }); }); });