140 lines
4.7 KiB
TypeScript
140 lines
4.7 KiB
TypeScript
import { beforeEach, describe, expect, it } from "@suaveplan/testing/runner";
|
|
import { FixedWindow } from "../src/core/fixed-window/fixed-window.js";
|
|
import { createRateLimiter } from "../src/core/rate-limiter/rate-limiter.js";
|
|
import { SlidingWindow } from "../src/core/sliding-window/sliding-window.js";
|
|
import { TokenBucket } from "../src/core/token-bucket/token-bucket.js";
|
|
import { MemoryStorage } from "../src/storage/memory-storage/memory-storage.js";
|
|
|
|
describe("Integration: multi-algorithm coordination", () => {
|
|
let t: number;
|
|
let clock: () => number;
|
|
|
|
beforeEach(() => {
|
|
t = 1_000_000;
|
|
clock = () => t;
|
|
});
|
|
|
|
it("token bucket + storage round-trip", { timeout: 5000 }, () => {
|
|
const bucket = new TokenBucket({
|
|
capacity: 10,
|
|
refillRate: 0.01,
|
|
clock,
|
|
});
|
|
// Drain bucket
|
|
for (let i = 0; i < 10; i++) {
|
|
expect(bucket.consume().allowed).toBe(true);
|
|
}
|
|
expect(bucket.consume().allowed).toBe(false);
|
|
// Refill via time
|
|
t += 1000; // +10 tokens
|
|
expect(bucket.consume().allowed).toBe(true);
|
|
});
|
|
|
|
it("sliding window per-key isolation", { timeout: 5000 }, async () => {
|
|
const storage = new MemoryStorage({ clock });
|
|
const limiter = createRateLimiter({
|
|
defaultConfig: { limit: 2, window: 60_000 },
|
|
storage,
|
|
clock,
|
|
keyPrefix: "rl",
|
|
});
|
|
|
|
// Exhaust key "a:user:1"
|
|
await limiter.check({ key: "a:user:1", tenantId: "a" });
|
|
await limiter.check({ key: "a:user:1", tenantId: "a" });
|
|
const r1 = await limiter.check({ key: "a:user:1", tenantId: "a" });
|
|
expect(r1.allowed).toBe(false);
|
|
|
|
// "b:user:1" is a separate storage key and is unaffected
|
|
const r2 = await limiter.check({ key: "b:user:1", tenantId: "b" });
|
|
expect(r2.allowed).toBe(true);
|
|
});
|
|
|
|
it("fixed window and sliding window agreement on boundary", {
|
|
timeout: 5000,
|
|
}, () => {
|
|
t = 0;
|
|
const fw = new FixedWindow({ limit: 3, windowMs: 1000, clock });
|
|
const sw = new SlidingWindow({ limit: 3, windowMs: 1000, clock });
|
|
|
|
for (let i = 0; i < 3; i++) {
|
|
expect(fw.check("k").allowed).toBe(true);
|
|
expect(sw.check("k").allowed).toBe(true);
|
|
}
|
|
// Both reject
|
|
expect(fw.check("k").allowed).toBe(false);
|
|
expect(sw.check("k").allowed).toBe(false);
|
|
|
|
// Advance past window
|
|
t = 1001;
|
|
expect(fw.check("k").allowed).toBe(true);
|
|
expect(sw.check("k").allowed).toBe(true);
|
|
});
|
|
|
|
it("rate limiter with all rule conditions", { timeout: 5000 }, async () => {
|
|
const limiter = createRateLimiter({
|
|
defaultConfig: { limit: 100, window: 1000 },
|
|
rules: [
|
|
{
|
|
id: "ip-block",
|
|
limit: 0,
|
|
window: 1000,
|
|
priority: 90,
|
|
bypass: false,
|
|
conditions: [
|
|
{ field: "ip", operator: "eq", value: "10.0.0.1" },
|
|
],
|
|
},
|
|
{
|
|
id: "admin-pass",
|
|
limit: 0,
|
|
window: 1000,
|
|
priority: 100,
|
|
bypass: true,
|
|
conditions: [
|
|
{ field: "userId", operator: "in", value: ["admin"] },
|
|
],
|
|
},
|
|
],
|
|
clock,
|
|
});
|
|
|
|
// Admin bypasses all limits
|
|
const adminResult = await limiter.check({
|
|
key: "admin-key",
|
|
userId: "admin",
|
|
});
|
|
expect(adminResult.allowed).toBe(true);
|
|
|
|
// Blocked IP hits limit immediately
|
|
const blockedResult = await limiter.check({
|
|
key: "blocked-ip",
|
|
ip: "10.0.0.1",
|
|
});
|
|
expect(blockedResult.allowed).toBe(false);
|
|
});
|
|
|
|
it("getState after multiple checks is consistent", {
|
|
timeout: 5000,
|
|
}, async () => {
|
|
const storage = new MemoryStorage({ clock });
|
|
const limiter = createRateLimiter({
|
|
defaultConfig: { limit: 5, window: 1000 },
|
|
storage,
|
|
clock,
|
|
});
|
|
|
|
await limiter.check({ key: "u1" });
|
|
await limiter.check({ key: "u1" });
|
|
await limiter.check({ key: "u1" });
|
|
|
|
const state = await limiter.getState("u1");
|
|
expect(state).not.toBeNull();
|
|
expect(state?.algorithm).toBe("sliding-window");
|
|
expect("requests" in state!.state).toBe(true);
|
|
if ("requests" in state!.state) {
|
|
expect(state!.state.requests.length).toBe(3);
|
|
}
|
|
});
|
|
});
|