Files
pn-new-crm/tests/integration/rate-limit.test.ts

89 lines
3.1 KiB
TypeScript
Raw Normal View History

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);
});
});