auth-rate-limit.test.ts57 lines · main
| 1 | import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; |
| 2 | |
| 3 | /** |
| 4 | * S6.2 — rate-limit reliability (memory backend). |
| 5 | * Forces the in-memory path by clearing BRIVEN_REDIS_URL so we can assert |
| 6 | * deny-after-N and per-project isolation without a live Redis. |
| 7 | */ |
| 8 | |
| 9 | const ORIGINAL_REDIS = process.env.BRIVEN_REDIS_URL; |
| 10 | |
| 11 | beforeEach(() => { |
| 12 | delete process.env.BRIVEN_REDIS_URL; |
| 13 | }); |
| 14 | |
| 15 | afterEach(() => { |
| 16 | if (ORIGINAL_REDIS === undefined) delete process.env.BRIVEN_REDIS_URL; |
| 17 | else process.env.BRIVEN_REDIS_URL = ORIGINAL_REDIS; |
| 18 | }); |
| 19 | |
| 20 | describe('auth rate limit — memory backend (S6.2)', () => { |
| 21 | test('allows under maxAttempts then denies', async () => { |
| 22 | // Dynamic import after env tweak so getRedis() sees no URL. |
| 23 | const { checkIpRateLimit } = await import('./auth-rate-limit.js'); |
| 24 | const { resetAuthReliabilityCountersForTests } = await import('./auth-reliability.js'); |
| 25 | resetAuthReliabilityCountersForTests(); |
| 26 | |
| 27 | const projectId = `p_rl_${Date.now()}`; |
| 28 | const ip = `203.0.113.${Math.floor(Math.random() * 200) + 1}`; |
| 29 | const opts = { maxAttempts: 3, windowMinutes: 5 }; |
| 30 | |
| 31 | const a1 = await checkIpRateLimit(projectId, ip, opts); |
| 32 | const a2 = await checkIpRateLimit(projectId, ip, opts); |
| 33 | const a3 = await checkIpRateLimit(projectId, ip, opts); |
| 34 | const a4 = await checkIpRateLimit(projectId, ip, opts); |
| 35 | |
| 36 | expect(a1.allowed).toBe(true); |
| 37 | expect(a2.allowed).toBe(true); |
| 38 | expect(a3.allowed).toBe(true); |
| 39 | expect(a4.allowed).toBe(false); |
| 40 | expect(a4.retryAfterSeconds).toBeGreaterThan(0); |
| 41 | }); |
| 42 | |
| 43 | test('buckets are isolated per projectId (cross-project cannot share limit)', async () => { |
| 44 | const { checkIpRateLimit } = await import('./auth-rate-limit.js'); |
| 45 | const opts = { maxAttempts: 2, windowMinutes: 5 }; |
| 46 | const ip = '198.51.100.50'; |
| 47 | const a = `p_iso_a_${Date.now()}`; |
| 48 | const b = `p_iso_b_${Date.now()}`; |
| 49 | |
| 50 | expect((await checkIpRateLimit(a, ip, opts)).allowed).toBe(true); |
| 51 | expect((await checkIpRateLimit(a, ip, opts)).allowed).toBe(true); |
| 52 | expect((await checkIpRateLimit(a, ip, opts)).allowed).toBe(false); |
| 53 | |
| 54 | // Project B still has a full budget for the same IP. |
| 55 | expect((await checkIpRateLimit(b, ip, opts)).allowed).toBe(true); |
| 56 | }); |
| 57 | }); |