Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,281 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import * as fc from "fast-check";
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";
import type { RateLimitState } from "../src/types/rate-limiter.types.js";
describe("Property: TokenBucket invariants", () => {
it("available never exceeds capacity", { timeout: 30000 }, () => {
fc.assert(
fc.property(
fc.integer({ min: 1, max: 1000 }),
fc.float({ min: Math.fround(0.001), max: Math.fround(10) }),
fc.array(fc.integer({ min: 0, max: 1000 }), {
minLength: 0,
maxLength: 50,
}),
(capacity, refillRate, delays) => {
let t = 0;
const clock = () => t;
const bucket = new TokenBucket({
capacity,
refillRate,
clock,
});
for (const delay of delays) {
t += delay;
bucket.consume();
}
// After any sequence, available must be <= capacity
expect(bucket.available).toBeLessThanOrEqual(capacity);
}
)
);
});
it("available never goes below 0", { timeout: 30000 }, () => {
fc.assert(
fc.property(
fc.integer({ min: 1, max: 100 }),
fc.float({ min: Math.fround(0.001), max: Math.fround(1) }),
fc.integer({ min: 1, max: 200 }),
(capacity, refillRate, consumeCount) => {
const t = 0;
const clock = () => t;
const bucket = new TokenBucket({
capacity,
refillRate,
initialTokens: 0,
clock,
});
for (let i = 0; i < consumeCount; i++) {
bucket.consume();
}
expect(bucket.available).toBeGreaterThanOrEqual(0);
}
)
);
});
it("consume(n) decreases available by exactly n when allowed", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(
fc.integer({ min: 2, max: 100 }),
fc.integer({ min: 1, max: 10 }),
(capacity, consume) => {
fc.pre(consume <= capacity);
const t = 0;
const clock = () => t;
const bucket = new TokenBucket({
capacity,
refillRate: 0,
clock,
});
const before = bucket.available;
const result = bucket.consume(consume);
if (result.allowed) {
expect(bucket.available).toBe(before - consume);
}
}
)
);
});
});
describe("Property: tenant multiplier correctness", () => {
it("effective limit = base * multiplier for same key", {
timeout: 30000,
}, async () => {
await fc.assert(
fc.asyncProperty(
fc.integer({ min: 2, max: 10 }),
fc.integer({ min: 2, max: 4 }),
async (baseLimit, multiplierInt) => {
const multiplier = multiplierInt; // integer multiplier avoids float rounding
const effectiveLimit = baseLimit * multiplier;
const t = 0;
const clock = () => t;
const limiter = createRateLimiter({
defaultConfig: { limit: baseLimit, window: 60_000 },
enableTenantLimits: true,
tenantMultipliers: { "test-tenant": multiplier },
clock,
});
const key = `tenant-same-key-${baseLimit}-${multiplier}`;
let allowed = 0;
for (let i = 0; i < effectiveLimit + 3; i++) {
const r = await limiter.check({
key,
tenantId: "test-tenant",
});
if (r.allowed) allowed++;
}
// Exactly effectiveLimit requests on the same key should be allowed
expect(allowed).toBe(effectiveLimit);
}
)
);
});
});
describe("Property: SlidingWindow monotonicity", () => {
it("remaining never increases between consecutive checks without reset", {
timeout: 30000,
}, () => {
fc.assert(
fc.property(
fc.integer({ min: 1, max: 50 }),
fc.integer({ min: 100, max: 10_000 }),
fc.integer({ min: 1, max: 100 }),
(limit, windowMs, checks) => {
const t = 1_000_000;
const clock = () => t;
const sw = new SlidingWindow({ limit, windowMs, clock });
let lastRemaining = limit;
let wasReset = false;
for (let i = 0; i < checks; i++) {
const r = sw.check("monotone-key");
if (r.allowed) {
// remaining should be <= previous remaining (or equal if we're at 0)
if (!wasReset) {
expect(r.remaining).toBeLessThanOrEqual(
lastRemaining
);
}
}
lastRemaining = r.remaining;
wasReset = false;
}
}
)
);
});
});
describe("Property: MemoryStorage round-trip", () => {
it("set then get returns deep-equal state within TTL", {
timeout: 30000,
}, async () => {
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1, maxLength: 50 }),
fc.integer({ min: 0, max: 1000 }),
async (key, count) => {
const t = 1_000_000;
const clock = () => t;
const storage = new MemoryStorage({ clock });
const state: RateLimitState = {
key,
algorithm: "sliding-window",
state: {
requests: Array.from(
{ length: count % 10 },
(_, i) => t - i * 100
),
},
config: { limit: 100, window: 60_000 },
updatedAt: t,
};
await storage.set(key, state, 60_000);
const loaded = await storage.get(key);
expect(loaded).toEqual(state);
}
)
);
});
it("get returns null after TTL expires", { timeout: 30000 }, async () => {
await fc.assert(
fc.asyncProperty(
fc.string({ minLength: 1, maxLength: 20 }),
fc.integer({ min: 1, max: 5000 }),
async (key, ttl) => {
let t = 0;
const clock = () => t;
const storage = new MemoryStorage({ clock });
const state: RateLimitState = {
key,
algorithm: "fixed-window",
state: { count: 1, windowStart: 0 },
config: { limit: 10, window: ttl },
updatedAt: 0,
};
await storage.set(key, state, ttl);
t = ttl + 1;
expect(await storage.get(key)).toBeNull();
}
)
);
});
});
describe("Property: rule priority ordering", () => {
it("higher priority rule always wins over lower", {
timeout: 30000,
}, async () => {
await fc.assert(
fc.asyncProperty(
fc.integer({ min: 1, max: 50 }),
fc.integer({ min: 51, max: 100 }),
fc.integer({ min: 1, max: 20 }),
fc.integer({ min: 21, max: 100 }),
async (lowLimit, highLimit, lowPriority, highPriority) => {
const t = 0;
const clock = () => t;
const limiter = createRateLimiter({
defaultConfig: { limit: 1000, window: 60_000 },
rules: [
{
id: "low",
limit: lowLimit,
window: 60_000,
priority: lowPriority,
conditions: [
{
field: "path",
operator: "eq",
value: "/test",
},
],
},
{
id: "high",
limit: highLimit,
window: 60_000,
priority: highPriority,
conditions: [
{
field: "path",
operator: "eq",
value: "/test",
},
],
},
],
clock,
});
// Use unique key per test to avoid cross-run contamination
const key = `prop-${lowLimit}-${highPriority}`;
let allowed = 0;
for (let i = 0; i < highLimit + 5; i++) {
const r = await limiter.check({
key: `${key}-${i}`,
path: "/test",
});
if (r.allowed) allowed++;
}
// All requests on unique keys should pass (one request per unique key)
expect(allowed).toBe(highLimit + 5);
}
)
);
});
});