69 lines
2.3 KiB
TypeScript
69 lines
2.3 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
import {
|
|
AccessCodeRateLimiter,
|
|
resolveAccessCodeRateLimitConfig,
|
|
resolveAccessCodeRateLimitKey,
|
|
} from '@/lib/server/access-code-rate-limit';
|
|
|
|
describe('ACCESS_CODE login rate limiting', () => {
|
|
test('blocks after the configured attempt count and reopens after the window', () => {
|
|
const limiter = new AccessCodeRateLimiter();
|
|
const config = { maxAttempts: 2, windowMs: 10_000, maxClients: 100 };
|
|
|
|
expect(limiter.consume('client-a', config, 1_000)).toMatchObject({
|
|
allowed: true,
|
|
remaining: 1,
|
|
});
|
|
expect(limiter.consume('client-a', config, 2_000)).toMatchObject({
|
|
allowed: true,
|
|
remaining: 0,
|
|
});
|
|
expect(limiter.consume('client-a', config, 3_000)).toMatchObject({
|
|
allowed: false,
|
|
retryAfterSeconds: 8,
|
|
});
|
|
expect(limiter.consume('client-a', config, 11_000)).toMatchObject({
|
|
allowed: true,
|
|
remaining: 1,
|
|
});
|
|
});
|
|
|
|
test('keeps the in-process client map bounded', () => {
|
|
const limiter = new AccessCodeRateLimiter();
|
|
const config = { maxAttempts: 3, windowMs: 10_000, maxClients: 2 };
|
|
|
|
limiter.consume('client-a', config, 1_000);
|
|
limiter.consume('client-b', config, 1_001);
|
|
limiter.consume('client-c', config, 1_002);
|
|
|
|
expect(limiter.size).toBe(2);
|
|
expect(limiter.consume('client-a', config, 1_003)).toMatchObject({
|
|
allowed: true,
|
|
remaining: 2,
|
|
});
|
|
expect(limiter.size).toBe(2);
|
|
});
|
|
|
|
test('trusts proxy IP headers only through explicit opt-in', () => {
|
|
const request = new Request('https://ops.example/api/access-code/verify', {
|
|
headers: {
|
|
'x-forwarded-for': '203.0.113.8, 10.0.0.2',
|
|
'x-real-ip': '203.0.113.9',
|
|
},
|
|
});
|
|
|
|
expect(resolveAccessCodeRateLimitKey(request, false)).toBe('shared:unknown-client');
|
|
expect(resolveAccessCodeRateLimitKey(request, true)).toBe('ip:203.0.113.8');
|
|
});
|
|
|
|
test('falls back safely for invalid configuration', () => {
|
|
expect(
|
|
resolveAccessCodeRateLimitConfig({
|
|
ACCESS_CODE_RATE_LIMIT_MAX_ATTEMPTS: '-1',
|
|
ACCESS_CODE_RATE_LIMIT_WINDOW_SECONDS: 'not-a-number',
|
|
ACCESS_CODE_RATE_LIMIT_MAX_CLIENTS: '0',
|
|
}),
|
|
).toEqual({ maxAttempts: 10, windowMs: 900_000, maxClients: 10_000 });
|
|
});
|
|
});
|