mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 16:15:39 +00:00
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.
47 lines
1.6 KiB
TypeScript
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);
|
|
});
|
|
});
|