import { describe, it, expect, beforeEach } from 'vitest'; import { checkContactSubmission, resetRateLimiter } from '@/lib/antispam/contact-guard'; import { parseClientIp } from '@/lib/antispam/client-ip'; import { createFormToken } from '@/lib/antispam/form-token'; process.env.FORM_TOKEN_SECRET = 'test-secret'; const NOW = 1_000_000_000_000; const TOKEN_SECRET = 'test-secret'; const validInput = () => ({ honeypot: '', formToken: createFormToken(NOW - 10_000, TOKEN_SECRET), now: NOW, ip: '1.2.3.4', email: 'customer@example.com', message: 'I would like a quote for 50 cables.', }); describe('client IP extraction from X-Forwarded-For', () => { it('uses the last hop appended by our own proxy, not the client-controlled first hop', () => { expect(parseClientIp('203.0.113.7, 198.51.100.2')).toBe('198.51.100.2'); }); it('handles a single hop', () => { expect(parseClientIp('203.0.113.7')).toBe('203.0.113.7'); }); it('returns null for missing or empty headers', () => { expect(parseClientIp(null)).toBeNull(); expect(parseClientIp('')).toBeNull(); expect(parseClientIp(' ')).toBeNull(); }); }); describe('contact form anti-spam guard', () => { beforeEach(() => resetRateLimiter()); it('allows a legitimate submission', () => { const result = checkContactSubmission(validInput()); expect(result.allowed).toBe(true); }); it('blocks submissions where the honeypot field is filled', () => { const result = checkContactSubmission({ ...validInput(), honeypot: 'http://spam.example' }); expect(result.allowed).toBe(false); expect(result.reason).toBe('honeypot'); }); it('blocks submissions without a form token (direct POST without loading the form)', () => { const result = checkContactSubmission({ ...validInput(), formToken: null }); expect(result.allowed).toBe(false); expect(result.reason).toBe('invalid_token'); }); it('blocks submissions with a forged form token', () => { const result = checkContactSubmission({ ...validInput(), formToken: '1234567890.forged' }); expect(result.allowed).toBe(false); expect(result.reason).toBe('invalid_token'); }); it('blocks submissions sent faster than a human could fill the form', () => { const freshToken = createFormToken(NOW - 1_000, TOKEN_SECRET); const result = checkContactSubmission({ ...validInput(), formToken: freshToken }); expect(result.allowed).toBe(false); expect(result.reason).toBe('too_fast'); }); it('blocks submissions with a stale token (page loaded more than a day ago)', () => { const staleToken = createFormToken(NOW - 25 * 60 * 60 * 1000, TOKEN_SECRET); const result = checkContactSubmission({ ...validInput(), formToken: staleToken }); expect(result.allowed).toBe(false); expect(result.reason).toBe('expired_token'); }); it('blocks submissions with an invalid email address', () => { const result = checkContactSubmission({ ...validInput(), email: 'not-an-email' }); expect(result.allowed).toBe(false); expect(result.reason).toBe('invalid_email'); }); it('blocks messages containing more than 5 links (typical SEO spam)', () => { const spammy = Array.from({ length: 6 }, (_, i) => `https://spam${i}.example`).join(' '); const result = checkContactSubmission({ ...validInput(), message: spammy }); expect(result.allowed).toBe(false); expect(result.reason).toBe('too_many_links'); }); it('rate-limits an IP after 3 submissions within 10 minutes', () => { for (let i = 0; i < 3; i++) { expect(checkContactSubmission(validInput()).allowed).toBe(true); } const result = checkContactSubmission(validInput()); expect(result.allowed).toBe(false); expect(result.reason).toBe('rate_limited'); }); it('does not rate-limit different IPs independently', () => { for (let i = 0; i < 3; i++) { checkContactSubmission(validInput()); } const result = checkContactSubmission({ ...validInput(), ip: '5.6.7.8' }); expect(result.allowed).toBe(true); }); });