flybrain/services/bridge/tests/ratelimit.test.ts
acamilo 660c3cf00d
Some checks failed
ci / node 22 (test + typecheck) (push) Has been cancelled
ci / rust stable (cargo test --workspace --release) (push) Has been cancelled
ci / infra/tests/lint.sh (push) Has been cancelled
ci / playwright apps/stage (allowed to fail) (push) Has been cancelled
flybrain v0.4.0: public tree (history retained privately)
2026-09-21 15:09:46 +00:00

90 lines
3.6 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { FakeClock, RateLimiter } from '../src/ratelimit';
import type { RateLimitSettings } from '../src/config';
function settings(overrides: Partial<RateLimitSettings> = {}): RateLimitSettings {
return {
perUserPerCommandMs: 60_000,
globalReplyMs: 5_000,
chatMessagesPer30s: 15,
perCommandCooldownMs: { sugar: 10_000 },
...overrides,
};
}
void test('allows the first call for a user/command', () => {
const limiter = new RateLimiter(settings(), new FakeClock());
assert.deepEqual(limiter.tryConsume('u1', 'fly'), { allowed: true });
});
void test('blocks the same user repeating the same command inside the per-user window', () => {
const clock = new FakeClock();
const limiter = new RateLimiter(settings(), clock);
assert.deepEqual(limiter.tryConsume('u1', 'fly'), { allowed: true });
clock.advance(1_000);
const second = limiter.tryConsume('u1', 'fly');
assert.equal(second.allowed, false);
if (!second.allowed) {
assert.equal(second.reason, 'user');
assert.equal(second.retryAfterMs, 59_000);
}
});
void test('allows the same user again after the per-user window elapses', () => {
const clock = new FakeClock();
const limiter = new RateLimiter(settings(), clock);
assert.deepEqual(limiter.tryConsume('u1', 'fly'), { allowed: true });
clock.advance(60_000);
assert.deepEqual(limiter.tryConsume('u1', 'fly'), { allowed: true });
});
void test('a different user is not blocked by another user, but shares the global reply bucket', () => {
const clock = new FakeClock();
const limiter = new RateLimiter(settings(), clock);
assert.deepEqual(limiter.tryConsume('u1', 'fly'), { allowed: true });
clock.advance(1_000);
const second = limiter.tryConsume('u2', 'fly');
assert.equal(second.allowed, false);
if (!second.allowed) assert.equal(second.reason, 'global');
});
void test('global reply bucket clears after globalReplyMs', () => {
const clock = new FakeClock();
const limiter = new RateLimiter(settings(), clock);
assert.deepEqual(limiter.tryConsume('u1', 'fly'), { allowed: true });
clock.advance(5_000);
assert.deepEqual(limiter.tryConsume('u2', 'brain'), { allowed: true });
});
void test('per-command global cooldown blocks a different user invoking the same command', () => {
const clock = new FakeClock();
const limiter = new RateLimiter(settings(), clock);
assert.deepEqual(limiter.tryConsume('u1', 'sugar'), { allowed: true });
clock.advance(5_000); // past the 5s global reply bucket, before the 10s sugar cooldown
const second = limiter.tryConsume('u2', 'sugar');
assert.equal(second.allowed, false);
if (!second.allowed) {
assert.equal(second.reason, 'cooldown');
assert.equal(second.retryAfterMs, 5_000);
}
});
void test('commands without a configured cooldown are unaffected by the cooldown check', () => {
const clock = new FakeClock();
const limiter = new RateLimiter(settings({ perCommandCooldownMs: {} }), clock);
assert.deepEqual(limiter.tryConsume('u1', 'sugar'), { allowed: true });
clock.advance(5_000);
assert.deepEqual(limiter.tryConsume('u2', 'sugar'), { allowed: true });
});
void test('check() does not consume the bucket', () => {
const clock = new FakeClock();
const limiter = new RateLimiter(settings(), clock);
assert.deepEqual(limiter.check('u1', 'fly'), { allowed: true });
assert.deepEqual(limiter.check('u1', 'fly'), { allowed: true });
assert.deepEqual(limiter.tryConsume('u1', 'fly'), { allowed: true });
clock.advance(1_000);
const blocked = limiter.check('u1', 'fly');
assert.equal(blocked.allowed, false);
});