import { describe, expect, it } from "vitest"; import { createRateLimiter } from "../src/rate-limit.js"; describe("rate-limit", () => { it("accepts up to burst then rejects", () => { const lim = createRateLimiter({ perMinute: 60, burst: 3 }); expect(lim.consume("u1", 0)).toEqual({ ok: true, remaining: 2 }); expect(lim.consume("u1", 0)).toEqual({ ok: true, remaining: 1 }); expect(lim.consume("u1", 0)).toEqual({ ok: true, remaining: 0 }); const denied = lim.consume("u1", 0); expect(denied.ok).toBe(false); if (!denied.ok) { expect(denied.retryAfterMs).toBeGreaterThan(0); expect(denied.retryAfterMs).toBeLessThanOrEqual(1000); } }); it("refills tokens over time", () => { const lim = createRateLimiter({ perMinute: 60, burst: 2 }); expect(lim.consume("u1", 0).ok).toBe(true); expect(lim.consume("u1", 0).ok).toBe(true); expect(lim.consume("u1", 0).ok).toBe(false); // 1 second later, 1 token refilled expect(lim.consume("u1", 1000).ok).toBe(true); expect(lim.consume("u1", 1000).ok).toBe(false); }); it("isolates buckets per id", () => { const lim = createRateLimiter({ perMinute: 60, burst: 1 }); expect(lim.consume("u1", 0).ok).toBe(true); expect(lim.consume("u1", 0).ok).toBe(false); // u2 has its own bucket expect(lim.consume("u2", 0).ok).toBe(true); expect(lim.consume("u2", 0).ok).toBe(false); }); it("caps refill at burst", () => { const lim = createRateLimiter({ perMinute: 60, burst: 2 }); // Wait a long time, tokens should still be capped at 2 const result = lim.consume("u1", 60_000); expect(result).toEqual({ ok: true, remaining: 1 }); expect(lim.consume("u1", 60_000).ok).toBe(true); expect(lim.consume("u1", 60_000).ok).toBe(false); }); it("reset clears a single bucket or all", () => { const lim = createRateLimiter({ perMinute: 60, burst: 1 }); lim.consume("u1", 0); lim.consume("u2", 0); expect(lim.size()).toBe(2); lim.reset("u1"); expect(lim.size()).toBe(1); expect(lim.consume("u1", 0).ok).toBe(true); lim.reset(); expect(lim.size()).toBe(0); }); });