Shumiland/tests/api/binotel-webhook.test.ts
Vadym Samoilenko cca4ea1d55
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions
feat: implement full frontend — all sections, components, Figma Code Connect
- All 8 home page sections: Hero, Locations slider, WhyParents accordion,
  Birthday pricing cards, Video, Gallery, Reviews slider, News
- UI components: NavLink, BtnPrimary, BtnGradient, BtnDetails, AccordionItem
- Layout: sticky Header (NavLink + BtnPrimary), Footer with logo
- Figma Code Connect: 5 components published (.figma.tsx + figma.config.json)
- Public assets: all Figma images and SVGs exported
- Pages: /kvytky, /lokatsii, /blog, /dni-narodzhennia, /grupovi-vidviduvannia
- Tests: Vitest unit/api suites, Playwright e2e screenshots
- Payload CMS: blocks, collections, seed data updates
- Hero negative-margin to extend behind sticky header
- Custom Tailwind breakpoints: lg=1440px, xl=1920px
- Fix ESLint config: drop FlatCompat, use eslint-config-next flat export
- Add tsconfig.tsbuildinfo, test-results/, agentdb.rvf* to .gitignore

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-10 16:40:56 +01:00

137 lines
4.5 KiB
TypeScript

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<Response>
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)
})
})