The declared data-URL MIME was a claim the boundary believed. Every image decision now comes from the bytes: the container is sniffed, walked to its own end marker, refused when metadata segments remain, and refused above a 4096px edge or 16MP. The request path adds a per-caller sliding-window budget (429 + Retry-After) and a body deadline (408), so a flood, a stalled body, or a decode bomb fails before a provider call. Log hygiene is part of the contract: refusal copy is fixed prose, the bucket key is a salted hash kept in memory only, and the acceptance test asserts no image bytes reach the process log.
92 lines
3.5 KiB
JavaScript
92 lines
3.5 KiB
JavaScript
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);
|
|
});
|