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); } ) ); }); });