import test from 'node:test'; import assert from 'node:assert/strict'; import { createRateLimiter } from '../src/rate-limit.js'; // A clock the test owns, so no test sleeps and every window boundary is exact. function manualClock(start = 1_700_000_000_000) { let current = start; return { now: () => current, advance: ms => { current += ms; } }; } test('allows requests up to the limit and refuses the next one', () => { const clock = manualClock(); const limiter = createRateLimiter({ limit: 3, windowMs: 60_000, now: clock.now }); for (let attempt = 1; attempt <= 3; attempt += 1) { const verdict = limiter.take('device-a'); assert.equal(verdict.allowed, true, `attempt ${attempt} must be allowed`); assert.equal(verdict.remaining, 3 - attempt); } const refused = limiter.take('device-a'); assert.equal(refused.allowed, false); assert.equal(refused.remaining, 0); assert.equal(refused.retryAfterMs, 60_000); }); test('counts each caller on its own budget', () => { const clock = manualClock(); const limiter = createRateLimiter({ limit: 1, windowMs: 60_000, now: clock.now }); assert.equal(limiter.take('device-a').allowed, true); assert.equal(limiter.take('device-a').allowed, false); assert.equal(limiter.take('device-b').allowed, true); }); test('lets a caller back in once its window has passed', () => { const clock = manualClock(); const limiter = createRateLimiter({ limit: 1, windowMs: 60_000, now: clock.now }); assert.equal(limiter.take('device-a').allowed, true); assert.equal(limiter.take('device-a').allowed, false); clock.advance(60_001); const allowedAgain = limiter.take('device-a'); assert.equal(allowedAgain.allowed, true); assert.equal(allowedAgain.remaining, 0); }); test('a refused request does not extend the block', () => { const clock = manualClock(); const limiter = createRateLimiter({ limit: 1, windowMs: 1_000, now: clock.now }); assert.equal(limiter.take('device-a').allowed, true); clock.advance(400); assert.equal(limiter.take('device-a').allowed, false); clock.advance(601); // The original hit is now 1001ms old, so the window has moved on even though the caller hammered // the endpoint in between. A refusal must never buy a longer penalty than the caller earned. assert.equal(limiter.take('device-a').allowed, true); }); test('reports the wait from the oldest hit still inside the window', () => { const clock = manualClock(); const limiter = createRateLimiter({ limit: 2, windowMs: 10_000, now: clock.now }); limiter.take('device-a'); clock.advance(4_000); limiter.take('device-a'); clock.advance(1_000); const refused = limiter.take('device-a'); assert.equal(refused.allowed, false); assert.equal(refused.retryAfterMs, 5_000); }); test('keeps its caller map bounded when many callers appear', () => { const clock = manualClock(); const limiter = createRateLimiter({ limit: 5, windowMs: 60_000, maxKeys: 8, now: clock.now }); for (let index = 0; index < 50; index += 1) limiter.take(`device-${index}`); assert.ok(limiter.size() <= 8, `expected at most 8 tracked callers, saw ${limiter.size()}`); }); test('prunes expired callers instead of growing for the life of the process', () => { const clock = manualClock(); const limiter = createRateLimiter({ limit: 5, windowMs: 1_000, now: clock.now }); for (let index = 0; index < 20; index += 1) limiter.take(`device-${index}`); clock.advance(1_001); limiter.take('device-fresh'); assert.equal(limiter.size(), 1); });