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 }); }); });