import { describe, expect, it, vi } from 'vitest'; import { createRateLimiter, createRedisRateLimiter } from '../src/rate-limit.js'; describe('rate limiter', () => { it('enforces request and spend windows', () => { let time = 0; const limiter = createRateLimiter({ now: () => time }); expect(limiter.check('ip').allowed).toBe(true); expect(limiter.check('ip').allowed).toBe(true); expect(limiter.check('ip').allowed).toBe(false); time = 1_001; expect(limiter.check('ip').allowed).toBe(true); limiter.recordCost('ip', 15); expect(limiter.check('ip').allowed).toBe(false); time = 5 * 60 * 60 * 1_000 + 1_002; expect(limiter.check('ip').allowed).toBe(true); }); it('keeps clients independent', () => { const limiter = createRateLimiter(); expect(limiter.check('a').allowed).toBe(true); expect(limiter.check('a').allowed).toBe(true); expect(limiter.check('a').allowed).toBe(false); expect(limiter.check('b').allowed).toBe(true); }); it('uses Redis scripts and namespaces keys by client IP', async () => { const client = { eval: vi.fn().mockResolvedValueOnce([1]).mockResolvedValueOnce(1) }; const limiter = createRedisRateLimiter({ client, now: () => 10_000, keyPrefix: 'test:limits' }); await expect(limiter.check('2001:db8::1')).resolves.toEqual({ allowed: true }); await limiter.recordCost('2001:db8::1', 1.25); expect(client.eval).toHaveBeenCalledTimes(2); expect(client.eval.mock.calls[0][1].keys).toEqual([ 'test:limits:{2001%3Adb8%3A%3A1}:requests', 'test:limits:{2001%3Adb8%3A%3A1}:spend', 'test:limits:{2001%3Adb8%3A%3A1}:sequence', ]); expect(client.eval.mock.calls[1][1].arguments).toEqual(['10000', '-17990000', '1.25', '18000000']); }); it('converts Redis denials into rate-limit responses', async () => { const client = { eval: vi.fn().mockResolvedValue([0, 'spend', 1_001]) }; const limiter = createRedisRateLimiter({ client, now: () => 10_000 }); await expect(limiter.check('ip')).resolves.toEqual({ allowed: false, reason: 'spend', retryAfter: 2 }); }); });