72 lines
2.9 KiB
TypeScript
72 lines
2.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { clientKeyFrom, createRateLimiter } from "@/lib/rate-limit";
|
|
|
|
describe("createRateLimiter", () => {
|
|
it("allows up to the limit within a window, then refuses", () => {
|
|
const limiter = createRateLimiter({ limit: 3, windowMs: 1000 });
|
|
expect(limiter.allow("a", 0)).toBe(true);
|
|
expect(limiter.allow("a", 1)).toBe(true);
|
|
expect(limiter.allow("a", 2)).toBe(true);
|
|
expect(limiter.allow("a", 3)).toBe(false);
|
|
expect(limiter.allow("a", 999)).toBe(false);
|
|
});
|
|
|
|
it("starts a fresh window once the previous one expires", () => {
|
|
const limiter = createRateLimiter({ limit: 1, windowMs: 1000 });
|
|
expect(limiter.allow("a", 0)).toBe(true);
|
|
expect(limiter.allow("a", 500)).toBe(false);
|
|
expect(limiter.allow("a", 1000)).toBe(true);
|
|
});
|
|
|
|
it("tracks keys independently", () => {
|
|
const limiter = createRateLimiter({ limit: 1, windowMs: 1000 });
|
|
expect(limiter.allow("a", 0)).toBe(true);
|
|
expect(limiter.allow("a", 1)).toBe(false);
|
|
expect(limiter.allow("b", 1)).toBe(true);
|
|
});
|
|
|
|
it("reset forgets a key's attempts", () => {
|
|
const limiter = createRateLimiter({ limit: 1, windowMs: 1000 });
|
|
expect(limiter.allow("a", 0)).toBe(true);
|
|
expect(limiter.allow("a", 1)).toBe(false);
|
|
limiter.reset("a");
|
|
expect(limiter.allow("a", 2)).toBe(true);
|
|
});
|
|
|
|
it("sweeps expired entries instead of growing past maxKeys", () => {
|
|
const limiter = createRateLimiter({ limit: 1, windowMs: 1000, maxKeys: 3 });
|
|
expect(limiter.allow("a", 0)).toBe(true);
|
|
expect(limiter.allow("b", 0)).toBe(true);
|
|
expect(limiter.allow("c", 0)).toBe(true);
|
|
// All three are expired at t=1000; the sweep makes room, and the new
|
|
// key is not throttled by leftover state.
|
|
expect(limiter.allow("d", 1000)).toBe(true);
|
|
expect(limiter.allow("d", 1001)).toBe(false);
|
|
});
|
|
|
|
it("keeps live windows intact across a sweep", () => {
|
|
const limiter = createRateLimiter({ limit: 1, windowMs: 1000, maxKeys: 2 });
|
|
expect(limiter.allow("a", 0)).toBe(true);
|
|
expect(limiter.allow("b", 500)).toBe(true);
|
|
expect(limiter.allow("c", 600)).toBe(true); // sweep drops nothing live
|
|
expect(limiter.allow("b", 700)).toBe(false); // b's window survived
|
|
});
|
|
});
|
|
|
|
describe("clientKeyFrom", () => {
|
|
it("uses the first hop of x-forwarded-for", () => {
|
|
const headers = new Headers({ "x-forwarded-for": "203.0.113.7, 10.0.0.1" });
|
|
expect(clientKeyFrom(headers)).toBe("203.0.113.7");
|
|
});
|
|
|
|
it("falls back to x-real-ip, then to a shared bucket", () => {
|
|
expect(clientKeyFrom(new Headers({ "x-real-ip": "203.0.113.9" }))).toBe("203.0.113.9");
|
|
expect(clientKeyFrom(new Headers())).toBe("unknown");
|
|
});
|
|
|
|
it("caps the key length so junk headers cannot bloat the store", () => {
|
|
const headers = new Headers({ "x-forwarded-for": "x".repeat(500) });
|
|
expect(clientKeyFrom(headers)).toHaveLength(100);
|
|
});
|
|
});
|