mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-14 03:20:49 +00:00
154 lines
4.0 KiB
TypeScript
154 lines
4.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
createBoundedCounter,
|
|
createFixedWindowRateLimiter,
|
|
createWebhookAnomalyTracker,
|
|
WEBHOOK_ANOMALY_COUNTER_DEFAULTS,
|
|
WEBHOOK_RATE_LIMIT_DEFAULTS,
|
|
} from "./webhook-memory-guards.js";
|
|
|
|
describe("createFixedWindowRateLimiter", () => {
|
|
it("enforces a fixed-window request limit", () => {
|
|
const limiter = createFixedWindowRateLimiter({
|
|
windowMs: 60_000,
|
|
maxRequests: 3,
|
|
maxTrackedKeys: 100,
|
|
});
|
|
|
|
expect(limiter.isRateLimited("k", 1_000)).toBe(false);
|
|
expect(limiter.isRateLimited("k", 1_001)).toBe(false);
|
|
expect(limiter.isRateLimited("k", 1_002)).toBe(false);
|
|
expect(limiter.isRateLimited("k", 1_003)).toBe(true);
|
|
});
|
|
|
|
it("resets counters after the window elapses", () => {
|
|
const limiter = createFixedWindowRateLimiter({
|
|
windowMs: 10,
|
|
maxRequests: 1,
|
|
maxTrackedKeys: 100,
|
|
});
|
|
|
|
expect(limiter.isRateLimited("k", 100)).toBe(false);
|
|
expect(limiter.isRateLimited("k", 101)).toBe(true);
|
|
expect(limiter.isRateLimited("k", 111)).toBe(false);
|
|
});
|
|
|
|
it("caps tracked keys", () => {
|
|
const limiter = createFixedWindowRateLimiter({
|
|
windowMs: 60_000,
|
|
maxRequests: 10,
|
|
maxTrackedKeys: 5,
|
|
});
|
|
|
|
for (let i = 0; i < 20; i += 1) {
|
|
limiter.isRateLimited(`key-${i}`, 1_000 + i);
|
|
}
|
|
|
|
expect(limiter.size()).toBeLessThanOrEqual(5);
|
|
});
|
|
|
|
it("prunes stale keys", () => {
|
|
const limiter = createFixedWindowRateLimiter({
|
|
windowMs: 10,
|
|
maxRequests: 10,
|
|
maxTrackedKeys: 100,
|
|
pruneIntervalMs: 10,
|
|
});
|
|
|
|
for (let i = 0; i < 20; i += 1) {
|
|
limiter.isRateLimited(`key-${i}`, 100);
|
|
}
|
|
expect(limiter.size()).toBe(20);
|
|
|
|
limiter.isRateLimited("fresh", 120);
|
|
expect(limiter.size()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("createBoundedCounter", () => {
|
|
it("increments and returns per-key counts", () => {
|
|
const counter = createBoundedCounter({ maxTrackedKeys: 100 });
|
|
|
|
expect(counter.increment("k", 1_000)).toBe(1);
|
|
expect(counter.increment("k", 1_001)).toBe(2);
|
|
expect(counter.increment("k", 1_002)).toBe(3);
|
|
});
|
|
|
|
it("caps tracked keys", () => {
|
|
const counter = createBoundedCounter({ maxTrackedKeys: 3 });
|
|
|
|
for (let i = 0; i < 10; i += 1) {
|
|
counter.increment(`k-${i}`, 1_000 + i);
|
|
}
|
|
|
|
expect(counter.size()).toBeLessThanOrEqual(3);
|
|
});
|
|
|
|
it("expires stale keys when ttl is set", () => {
|
|
const counter = createBoundedCounter({
|
|
maxTrackedKeys: 100,
|
|
ttlMs: 10,
|
|
pruneIntervalMs: 10,
|
|
});
|
|
|
|
counter.increment("old-1", 100);
|
|
counter.increment("old-2", 100);
|
|
expect(counter.size()).toBe(2);
|
|
|
|
counter.increment("fresh", 120);
|
|
expect(counter.size()).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("defaults", () => {
|
|
it("exports shared webhook limit profiles", () => {
|
|
expect(WEBHOOK_RATE_LIMIT_DEFAULTS).toEqual({
|
|
windowMs: 60_000,
|
|
maxRequests: 120,
|
|
maxTrackedKeys: 4_096,
|
|
});
|
|
expect(WEBHOOK_ANOMALY_COUNTER_DEFAULTS.maxTrackedKeys).toBe(4_096);
|
|
expect(WEBHOOK_ANOMALY_COUNTER_DEFAULTS.ttlMs).toBe(21_600_000);
|
|
expect(WEBHOOK_ANOMALY_COUNTER_DEFAULTS.logEvery).toBe(25);
|
|
});
|
|
});
|
|
|
|
describe("createWebhookAnomalyTracker", () => {
|
|
it("increments only tracked status codes and logs at configured cadence", () => {
|
|
const logs: string[] = [];
|
|
const tracker = createWebhookAnomalyTracker({
|
|
trackedStatusCodes: [401],
|
|
logEvery: 2,
|
|
});
|
|
|
|
expect(
|
|
tracker.record({
|
|
key: "k",
|
|
statusCode: 415,
|
|
message: (count) => `ignored:${count}`,
|
|
log: (msg) => logs.push(msg),
|
|
}),
|
|
).toBe(0);
|
|
|
|
expect(
|
|
tracker.record({
|
|
key: "k",
|
|
statusCode: 401,
|
|
message: (count) => `hit:${count}`,
|
|
log: (msg) => logs.push(msg),
|
|
}),
|
|
).toBe(1);
|
|
|
|
expect(
|
|
tracker.record({
|
|
key: "k",
|
|
statusCode: 401,
|
|
message: (count) => `hit:${count}`,
|
|
log: (msg) => logs.push(msg),
|
|
}),
|
|
).toBe(2);
|
|
|
|
expect(logs).toEqual(["hit:1", "hit:2"]);
|
|
});
|
|
});
|