Spaces:
Runtime error
Runtime error
| import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' | |
| import { createRateLimiter } from '@/lib/rate-limit' | |
| describe('createRateLimiter', () => { | |
| beforeEach(() => { | |
| vi.useFakeTimers() | |
| }) | |
| afterEach(() => { | |
| vi.useRealTimers() | |
| }) | |
| function makeRequest(ip: string = '127.0.0.1'): Request { | |
| return new Request('http://localhost/api/test', { | |
| headers: new Headers({ 'x-real-ip': ip }), | |
| }) | |
| } | |
| it('allows first request within limit (returns null)', () => { | |
| const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 5 }) | |
| const result = limiter(makeRequest()) | |
| expect(result).toBeNull() | |
| }) | |
| it('allows requests up to the max limit', () => { | |
| const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 3 }) | |
| expect(limiter(makeRequest())).toBeNull() | |
| expect(limiter(makeRequest())).toBeNull() | |
| expect(limiter(makeRequest())).toBeNull() | |
| }) | |
| it('blocks request exceeding the limit with 429', () => { | |
| const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 2 }) | |
| limiter(makeRequest()) | |
| limiter(makeRequest()) | |
| const blocked = limiter(makeRequest()) | |
| expect(blocked).not.toBeNull() | |
| expect(blocked!.status).toBe(429) | |
| }) | |
| it('uses custom message in 429 response', async () => { | |
| const limiter = createRateLimiter({ | |
| windowMs: 60_000, | |
| maxRequests: 1, | |
| message: 'Slow down!', | |
| }) | |
| limiter(makeRequest()) | |
| const blocked = limiter(makeRequest()) | |
| expect(blocked).not.toBeNull() | |
| const body = await blocked!.json() | |
| expect(body.error).toBe('Slow down!') | |
| }) | |
| it('resets after the window expires', () => { | |
| const limiter = createRateLimiter({ windowMs: 10_000, maxRequests: 1 }) | |
| expect(limiter(makeRequest())).toBeNull() | |
| expect(limiter(makeRequest())).not.toBeNull() | |
| // Advance past the window | |
| vi.advanceTimersByTime(11_000) | |
| // Should be allowed again | |
| expect(limiter(makeRequest())).toBeNull() | |
| }) | |
| it('tracks different IPs independently', () => { | |
| const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 1 }) | |
| expect(limiter(makeRequest('10.0.0.1'))).toBeNull() | |
| expect(limiter(makeRequest('10.0.0.2'))).toBeNull() | |
| // First IP now blocked | |
| expect(limiter(makeRequest('10.0.0.1'))).not.toBeNull() | |
| // Second IP now blocked | |
| expect(limiter(makeRequest('10.0.0.2'))).not.toBeNull() | |
| }) | |
| it('evicts oldest entry when maxEntries is reached', () => { | |
| const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 1, maxEntries: 3 }) | |
| // Fill to capacity and exhaust 10.0.0.1's quota | |
| limiter(makeRequest('10.0.0.1')) | |
| vi.advanceTimersByTime(1) | |
| expect(limiter(makeRequest('10.0.0.1'))).not.toBeNull() // blocked: quota consumed | |
| limiter(makeRequest('10.0.0.2')) | |
| vi.advanceTimersByTime(1) | |
| limiter(makeRequest('10.0.0.3')) | |
| vi.advanceTimersByTime(1) | |
| // Store has 3 entries (A blocked, B, C). Adding D evicts A (oldest resetAt) | |
| limiter(makeRequest('10.0.0.4')) | |
| // 10.0.0.1 was evicted — counter is gone, this is allowed (fresh entry) | |
| expect(limiter(makeRequest('10.0.0.1'))).toBeNull() | |
| // Now 10.0.0.1 is back at count=1 and blocked | |
| expect(limiter(makeRequest('10.0.0.1'))).not.toBeNull() | |
| // 10.0.0.3 should still be tracked (not evicted — it had the newest resetAt) | |
| expect(limiter(makeRequest('10.0.0.3'))).not.toBeNull() | |
| }) | |
| }) | |