Files
webmail/lib/__tests__/admin-rate-limit.test.ts
T
Linus Rath 291b6364c9 feat: count admin login attempts across every instance and refuse sign-in while the shared store is down #1066
The three admin login limiters (5 per address and 50 overall in one
reservation, 5 per known device) keep their numbers and still don't count
refused attempts, but live in the store: memory in this process as before,
or the shared store with several replicas. checkRateLimit and
checkDeviceRateLimit are async now; the login route awaits them and
answers 503 "Sign-in is temporarily unavailable" when the store can't be
reached. The two limiter tests gain await and reset the store's counters,
which vi.resetModules no longer clears.

The admin code tells a store outage by the error's name, since the store
may come from another bundle's copy of the module.
2026-10-31 23:18:40 +01:00

47 lines
1.6 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
// The per-IP key is X-Forwarded-For, which a client writes itself when the
// app is reachable without a reverse proxy: a fresh forged address used to
// mean a fresh bucket of five guesses, without end.
async function load() {
vi.resetModules();
// The counters live in the store on globalThis now, not in the module.
(await import('@/lib/shared-store/local')).resetLocalStoreForTests();
return import('@/lib/admin/rate-limit');
}
describe('admin login rate limit', () => {
beforeEach(() => {
vi.useFakeTimers();
});
afterEach(() => {
vi.useRealTimers();
});
it('allows five attempts per address', async () => {
const { checkRateLimit } = await load();
for (let i = 0; i < 5; i++) expect((await checkRateLimit('203.0.113.1')).allowed).toBe(true);
expect((await checkRateLimit('203.0.113.1')).allowed).toBe(false);
expect((await checkRateLimit('203.0.113.2')).allowed).toBe(true);
});
it('stops rotating forged addresses at the global ceiling', async () => {
const { checkRateLimit } = await load();
let allowed = 0;
for (let i = 0; i < 200; i++) {
if ((await checkRateLimit(`198.51.100.${i}`)).allowed) allowed++;
}
expect(allowed).toBe(50);
expect((await checkRateLimit('192.0.2.77')).allowed).toBe(false);
});
it('opens again after the window', async () => {
const { checkRateLimit } = await load();
for (let i = 0; i < 60; i++) await checkRateLimit(`198.51.100.${i}`);
vi.advanceTimersByTime(15 * 60 * 1000 + 1);
expect((await checkRateLimit('192.0.2.77')).allowed).toBe(true);
});
});