|
10 | 10 | import { beforeEach, describe, expect, it, vi } from 'vitest' |
11 | 11 |
|
12 | 12 | const { mockEnv } = vi.hoisted(() => ({ |
13 | | - mockEnv: { AUTH_TRUSTED_PROXIES: undefined as string | undefined }, |
| 13 | + mockEnv: { |
| 14 | + AUTH_TRUSTED_PROXIES: undefined as string | undefined, |
| 15 | + TRUST_PROXY_HEADERS: undefined as string | boolean | undefined, |
| 16 | + }, |
14 | 17 | })) |
15 | 18 |
|
16 | | -vi.mock('@/lib/core/config/env', () => ({ env: mockEnv })) |
| 19 | +vi.mock('@/lib/core/config/env', () => ({ |
| 20 | + env: mockEnv, |
| 21 | + isFalsy: (value: string | boolean | number | undefined) => |
| 22 | + value === false || value === 'false' || value === 0 || value === '0', |
| 23 | +})) |
17 | 24 | vi.unmock('@/lib/core/utils/client-ip') |
18 | 25 |
|
19 | 26 | /** |
20 | 27 | * The module parses the env once at import — that is the behavior under test — |
21 | 28 | * so each case needs a fresh module instance. This is the deliberate exception |
22 | 29 | * to the repo's "no `vi.resetModules()` + dynamic import" performance rule |
23 | 30 | * (`.cursor/rules/sim-testing.mdc`): module-init behavior cannot be observed any |
24 | | - * other way, and the cost here is four imports of a six-line module. |
| 31 | + * other way, and the cost here is a handful of imports of a tiny module. |
25 | 32 | */ |
26 | | -async function loadGetClientIp(trustedProxies: string | undefined) { |
| 33 | +async function loadGetClientIp( |
| 34 | + trustedProxies: string | undefined, |
| 35 | + trustProxyHeaders?: string | boolean |
| 36 | +) { |
27 | 37 | mockEnv.AUTH_TRUSTED_PROXIES = trustedProxies |
| 38 | + mockEnv.TRUST_PROXY_HEADERS = trustProxyHeaders |
28 | 39 | vi.resetModules() |
29 | 40 | return (await import('@/lib/core/utils/client-ip')).getClientIp |
30 | 41 | } |
@@ -64,4 +75,23 @@ describe('getClientIp', () => { |
64 | 75 |
|
65 | 76 | expect(getClientIp(req({}))).toBe('unknown') |
66 | 77 | }) |
| 78 | + |
| 79 | + it('declines to read forwarded headers when TRUST_PROXY_HEADERS is false', async () => { |
| 80 | + // No proxy in front: the whole header is caller-authored, so every caller |
| 81 | + // shares one bucket rather than each minting their own. |
| 82 | + const getClientIp = await loadGetClientIp(undefined, 'false') |
| 83 | + const keys = ['203.0.113.7, 10.0.0.1', '9.9.9.9', '2001:db8::1'].map((value) => |
| 84 | + getClientIp(req({ 'x-forwarded-for': value })) |
| 85 | + ) |
| 86 | + |
| 87 | + expect(new Set(keys)).toEqual(new Set(['unknown'])) |
| 88 | + expect(getClientIp(req({ 'x-real-ip': '203.0.113.7' }))).toBe('unknown') |
| 89 | + }) |
| 90 | + |
| 91 | + it('still reads forwarded headers when TRUST_PROXY_HEADERS is unset or true', async () => { |
| 92 | + for (const value of [undefined, 'true'] as const) { |
| 93 | + const getClientIp = await loadGetClientIp(undefined, value) |
| 94 | + expect(getClientIp(req({ 'x-forwarded-for': '203.0.113.7, 10.0.0.1' }))).toBe('10.0.0.1') |
| 95 | + } |
| 96 | + }) |
67 | 97 | }) |
0 commit comments