import { describe, it, expect, vi, beforeEach } from 'vitest' import { NextRequest } from 'next/server' const mockPayloadCreate = vi.fn() const mockGetPayload = vi.fn(() => ({ create: mockPayloadCreate })) vi.mock('payload', () => ({ getPayload: mockGetPayload })) vi.mock('@payload-config', () => ({ default: {} })) vi.mock('@/lib/telegram', () => ({ sendLeadAlert: vi.fn().mockResolvedValue(undefined) })) vi.mock('@/lib/resend', () => ({ sendLeadAlert: vi.fn().mockResolvedValue(undefined) })) const VALID_LEAD = { name: 'Іван Іванов', phone: '+380501234567', formSource: 'hero-form', } function makeRequest(body: unknown, ip = '1.2.3.4'): NextRequest { return new NextRequest('http://localhost/api/leads', { method: 'POST', body: typeof body === 'string' ? body : JSON.stringify(body), headers: { 'content-type': 'application/json', 'x-forwarded-for': ip, }, }) } let POST: (req: NextRequest) => Promise beforeEach(async () => { vi.resetModules() mockPayloadCreate.mockReset() // Re-import so rateLimit buckets are fresh const mod = await import('@/app/api/leads/route') POST = mod.POST }) describe('POST /api/leads', () => { it('returns 201 for a valid lead', async () => { mockPayloadCreate.mockResolvedValueOnce({ id: 'lead-1' }) const res = await POST(makeRequest(VALID_LEAD)) expect(res.status).toBe(201) expect((await res.json()).ok).toBe(true) }) it('returns 400 for invalid JSON body', async () => { const res = await POST(makeRequest('not-json')) expect(res.status).toBe(400) }) it('returns 422 when required fields are missing', async () => { const res = await POST(makeRequest({ phone: '123' })) expect(res.status).toBe(422) const json = await res.json() expect(json.error).toBe('Validation failed') expect(json.details).toBeDefined() }) it('returns 422 when name is empty string', async () => { const res = await POST(makeRequest({ ...VALID_LEAD, name: '' })) expect(res.status).toBe(422) }) it('returns 422 when phone is too short', async () => { const res = await POST(makeRequest({ ...VALID_LEAD, phone: '12345' })) expect(res.status).toBe(422) }) it('returns 422 when email is present but invalid', async () => { const res = await POST(makeRequest({ ...VALID_LEAD, email: 'not-an-email' })) expect(res.status).toBe(422) }) it('accepts optional UTM params and email', async () => { mockPayloadCreate.mockResolvedValueOnce({ id: 'lead-2' }) const res = await POST( makeRequest({ ...VALID_LEAD, email: 'test@example.com', utmSource: 'google', utmMedium: 'cpc', utmCampaign: 'summer', utmContent: 'banner', utmTerm: 'shoes', gclid: 'abc123', }) ) expect(res.status).toBe(201) }) it('returns 500 when DB create throws', async () => { mockPayloadCreate.mockRejectedValueOnce(new Error('DB error')) const res = await POST(makeRequest(VALID_LEAD, '9.9.9.1')) expect(res.status).toBe(500) }) it('returns 429 after 5 requests from the same IP', async () => { mockPayloadCreate.mockResolvedValue({ id: 'x' }) const ip = '5.5.5.5' for (let i = 0; i < 5; i++) { await POST(makeRequest(VALID_LEAD, ip)) } const res = await POST(makeRequest(VALID_LEAD, ip)) expect(res.status).toBe(429) }) it('uses "unknown" when x-forwarded-for header is absent', async () => { mockPayloadCreate.mockResolvedValueOnce({ id: 'lead-3' }) const req = new NextRequest('http://localhost/api/leads', { method: 'POST', body: JSON.stringify(VALID_LEAD), headers: { 'content-type': 'application/json' }, }) const res = await POST(req) expect(res.status).toBe(201) }) })