import { describe, it, expect, vi, beforeEach } from 'vitest' import { NextRequest } from 'next/server' import { createHmac } from 'crypto' const SECRET = 'test-hmac-secret' // matches vitest.setup.ts const mockPayloadFind = vi.fn() const mockPayloadUpdate = vi.fn() const mockGetPayload = vi.fn(() => ({ find: mockPayloadFind, update: mockPayloadUpdate, })) vi.mock('payload', () => ({ getPayload: mockGetPayload })) vi.mock('@payload-config', () => ({ default: {} })) function makeSignature(body: string): string { return createHmac('sha256', SECRET).update(body).digest('hex') } function makeRequest(body: string, signature: string): NextRequest { return new NextRequest('http://localhost/api/binotel/webhook', { method: 'POST', body, headers: { 'content-type': 'text/plain', 'x-binotel-signature': signature, }, }) } let POST: (req: NextRequest) => Promise beforeEach(async () => { vi.resetModules() mockPayloadFind.mockReset() mockPayloadUpdate.mockReset() vi.stubEnv('BINOTEL_HMAC_SECRET', SECRET) const mod = await import('@/app/api/binotel/webhook/route') POST = mod.POST }) describe('POST /api/binotel/webhook', () => { it('returns 401 for invalid HMAC signature', async () => { const req = makeRequest('{"externalNumber":"0501234567"}', 'bad-signature') const res = await POST(req) expect(res.status).toBe(401) }) it('returns 400 for valid signature but invalid JSON body', async () => { const body = 'not json' const req = makeRequest(body, makeSignature(body)) const res = await POST(req) expect(res.status).toBe(400) }) it('returns ok:true when externalNumber is missing from payload', async () => { const body = JSON.stringify({ event: 'call' }) const req = makeRequest(body, makeSignature(body)) const res = await POST(req) const json = await res.json() expect(res.status).toBe(200) expect(json.ok).toBe(true) expect(mockPayloadFind).not.toHaveBeenCalled() }) it('returns ok:true when externalNumber is not a string', async () => { const body = JSON.stringify({ externalNumber: 12345 }) const req = makeRequest(body, makeSignature(body)) const res = await POST(req) expect((await res.json()).ok).toBe(true) }) it('returns ok:true when phone cannot be normalized', async () => { const body = JSON.stringify({ externalNumber: 'INVALID' }) const req = makeRequest(body, makeSignature(body)) const res = await POST(req) expect((await res.json()).ok).toBe(true) expect(mockPayloadFind).not.toHaveBeenCalled() }) it('updates lead lastCallAt when matching lead is found', async () => { mockPayloadFind.mockResolvedValueOnce({ docs: [{ id: 'lead-42' }] }) mockPayloadUpdate.mockResolvedValueOnce({}) const body = JSON.stringify({ externalNumber: '0501234567' }) const req = makeRequest(body, makeSignature(body)) const res = await POST(req) expect(res.status).toBe(200) expect(mockPayloadFind).toHaveBeenCalledWith( expect.objectContaining({ collection: 'leads', where: { phone: { equals: '+380501234567' } }, }) ) expect(mockPayloadUpdate).toHaveBeenCalledWith( expect.objectContaining({ collection: 'leads', id: 'lead-42', data: expect.objectContaining({ lastCallAt: expect.any(String) }), }) ) }) it('returns ok:true when no matching lead is found (no update)', async () => { mockPayloadFind.mockResolvedValueOnce({ docs: [] }) const body = JSON.stringify({ externalNumber: '0501234567' }) const req = makeRequest(body, makeSignature(body)) const res = await POST(req) expect(res.status).toBe(200) expect(mockPayloadUpdate).not.toHaveBeenCalled() }) it('returns ok:true even when the DB call throws', async () => { mockPayloadFind.mockRejectedValueOnce(new Error('DB down')) const body = JSON.stringify({ externalNumber: '0501234567' }) const req = makeRequest(body, makeSignature(body)) const res = await POST(req) expect(res.status).toBe(200) expect((await res.json()).ok).toBe(true) }) it('returns 500 when BINOTEL_HMAC_SECRET is not configured', async () => { vi.stubEnv('BINOTEL_HMAC_SECRET', '') vi.resetModules() const mod = await import('@/app/api/binotel/webhook/route') const body = JSON.stringify({ externalNumber: '0501234567' }) const req = makeRequest(body, makeSignature(body)) const res = await mod.POST(req) expect(res.status).toBe(500) }) })