import { createHash } from 'crypto'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { RATE_LIMIT_CONFIGS, checkRateLimit, getClientIp, rateLimit, rateLimitHeaders, } from '@/lib/rate-limit'; const dbMock = vi.hoisted(() => ({ $queryRaw: vi.fn(), $executeRaw: vi.fn(), })); vi.mock('@/lib/db', () => ({ db: dbMock, default: dbMock, disconnectDb: vi.fn() })); function requestWith(headers: Record): Request { return new Request('https://example.com/api/comments', { headers }); } /** * The interpolated values of the most recent `$queryRaw` tagged template, in order: * the stored key, the stored action, then the window length twice. */ function valuesOfLastQuery(): unknown[] { const calls = dbMock.$queryRaw.mock.calls; const last = calls[calls.length - 1]; if (!last) throw new Error('no $queryRaw call was recorded'); return last.slice(1); } beforeEach(() => { vi.stubEnv('TRUSTED_PROXY_MODE', undefined); vi.stubEnv('DISABLE_RATE_LIMIT', undefined); dbMock.$queryRaw.mockReset(); dbMock.$executeRaw.mockReset(); }); afterEach(() => { vi.unstubAllEnvs(); }); describe('getClientIp without TRUSTED_PROXY_MODE', () => { it('ignores every proxy header and returns the loopback address', () => { const request = requestWith({ 'cf-connecting-ip': '203.0.113.7', 'x-real-ip': '203.0.113.8', 'x-forwarded-for': '203.0.113.9', }); expect(getClientIp(request)).toBe('127.0.0.1'); }); it('returns the loopback address when no headers are present at all', () => { expect(getClientIp(requestWith({}))).toBe('127.0.0.1'); }); it('ignores an unrecognised proxy mode', () => { vi.stubEnv('TRUSTED_PROXY_MODE', 'apache'); expect(getClientIp(requestWith({ 'x-real-ip': '203.0.113.8' }))).toBe('127.0.0.1'); }); }); describe('getClientIp in cloudflare mode', () => { beforeEach(() => { vi.stubEnv('TRUSTED_PROXY_MODE', 'cloudflare'); }); it('trusts cf-connecting-ip', () => { expect(getClientIp(requestWith({ 'cf-connecting-ip': '203.0.113.7' }))).toBe('203.0.113.7'); }); it('normalises a padded and mixed-case mode value', () => { vi.stubEnv('TRUSTED_PROXY_MODE', ' CloudFlare '); expect(getClientIp(requestWith({ 'cf-connecting-ip': '203.0.113.7' }))).toBe('203.0.113.7'); }); it('does not fall back to x-forwarded-for, which a client can set', () => { const request = requestWith({ 'x-forwarded-for': '203.0.113.9', 'x-real-ip': '203.0.113.8' }); expect(getClientIp(request)).toBe('127.0.0.1'); }); it('rejects an implausible cf-connecting-ip rather than trusting it', () => { expect(getClientIp(requestWith({ 'cf-connecting-ip': 'not-an-ip' }))).toBe('127.0.0.1'); }); it('rejects a cf-connecting-ip longer than 45 characters', () => { const tooLong = '1'.repeat(46); expect(getClientIp(requestWith({ 'cf-connecting-ip': tooLong }))).toBe('127.0.0.1'); }); it('accepts an IPv6 address', () => { expect(getClientIp(requestWith({ 'cf-connecting-ip': '2001:db8::1' }))).toBe('2001:db8::1'); }); }); describe('getClientIp in nginx mode', () => { beforeEach(() => { vi.stubEnv('TRUSTED_PROXY_MODE', 'nginx'); }); it('prefers x-real-ip over x-forwarded-for', () => { const request = requestWith({ 'x-real-ip': '203.0.113.8', 'x-forwarded-for': '203.0.113.9' }); expect(getClientIp(request)).toBe('203.0.113.8'); }); it('takes the last x-forwarded-for entry so a spoofed prefix is ignored', () => { const request = requestWith({ 'x-forwarded-for': '1.1.1.1, 2.2.2.2, 203.0.113.9' }); expect(getClientIp(request)).toBe('203.0.113.9'); }); it('trims whitespace around the last x-forwarded-for entry', () => { expect(getClientIp(requestWith({ 'x-forwarded-for': '1.1.1.1, 203.0.113.9 ' }))).toBe( '203.0.113.9' ); }); it('handles a single-entry x-forwarded-for', () => { expect(getClientIp(requestWith({ 'x-forwarded-for': '203.0.113.9' }))).toBe('203.0.113.9'); }); it('falls back to x-forwarded-for when x-real-ip is implausible', () => { const request = requestWith({ 'x-real-ip': 'evil