import { describe, it, expect, beforeEach, afterAll } from 'vitest'; import { redis } from '@/lib/redis'; import { checkRateLimit, rateLimiters } from '@/lib/rate-limit'; const TEST_USER = `rl-test-${crypto.randomUUID()}`; async function clearTestKeys() { // Wipe any keys created during this test run. for (const cfg of Object.values(rateLimiters)) { await redis.del(`rl:${cfg.keyPrefix}:${TEST_USER}`); } } beforeEach(async () => { await clearTestKeys(); }); afterAll(async () => { await clearTestKeys(); await redis.quit(); }); describe('rate-limit catalog', () => { it('exposes the canonical names with the documented limits', () => { expect(rateLimiters.ocr).toMatchObject({ max: 10, windowMs: 60_000 }); expect(rateLimiters.ai).toMatchObject({ max: 60, windowMs: 60_000 }); expect(rateLimiters.exports).toMatchObject({ max: 30, windowMs: 60 * 60_000 }); }); }); describe('checkRateLimit (sliding window)', () => { it('allows up to `max` calls in a window then refuses', async () => { const cfg = rateLimiters.ocr; let lastResult; for (let i = 0; i < cfg.max; i++) { lastResult = await checkRateLimit(TEST_USER, cfg); expect(lastResult.allowed).toBe(true); } expect(lastResult!.remaining).toBe(0); const overflow = await checkRateLimit(TEST_USER, cfg); expect(overflow.allowed).toBe(false); expect(overflow.remaining).toBe(0); expect(overflow.limit).toBe(cfg.max); }); it('isolates limits across keyPrefixes (ocr vs ai vs exports)', async () => { // Burn through ocr limit. for (let i = 0; i < rateLimiters.ocr.max; i++) { await checkRateLimit(TEST_USER, rateLimiters.ocr); } const ocrOverflow = await checkRateLimit(TEST_USER, rateLimiters.ocr); expect(ocrOverflow.allowed).toBe(false); // ai limit is independent. const ai = await checkRateLimit(TEST_USER, rateLimiters.ai); expect(ai.allowed).toBe(true); // exports limit is independent. const exp = await checkRateLimit(TEST_USER, rateLimiters.exports); expect(exp.allowed).toBe(true); }); it('isolates limits across users (one user hitting cap does not affect another)', async () => { const otherUser = `rl-test-${crypto.randomUUID()}`; try { for (let i = 0; i < rateLimiters.ocr.max; i++) { await checkRateLimit(TEST_USER, rateLimiters.ocr); } const overflowSelf = await checkRateLimit(TEST_USER, rateLimiters.ocr); expect(overflowSelf.allowed).toBe(false); const otherFirst = await checkRateLimit(otherUser, rateLimiters.ocr); expect(otherFirst.allowed).toBe(true); expect(otherFirst.remaining).toBe(rateLimiters.ocr.max - 1); } finally { await redis.del(`rl:${rateLimiters.ocr.keyPrefix}:${otherUser}`); } }); it('reports a sensible resetAt in the future', async () => { const before = Date.now(); const r = await checkRateLimit(TEST_USER, rateLimiters.ocr); expect(r.resetAt).toBeGreaterThanOrEqual(before + rateLimiters.ocr.windowMs - 1000); expect(r.resetAt).toBeLessThanOrEqual(Date.now() + rateLimiters.ocr.windowMs + 1000); }); });