Shumiland/tests/unit/lib/binotel.test.ts
Vadym Samoilenko 9b41fa447a
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: complete backend B1-B7 — Payload CMS, ezy payments, leads, deploy
- B1: Next.js 15 + Payload CMS 3.0 + Postgres 16, ESLint, Prettier, Husky, Vitest
- B2: 9 collections, 6 globals, 12 Page Builder blocks, access control, slugify/revalidate hooks
- B3: ezy.com.ua payments, Binotel HMAC webhook, leads API, Telegram bot, Resend email, rate limiting
- B4: Tariffs collection with ezy API sync (cron + manual), dynamic pricing source-of-truth
- B5: 13 test files covering unit libs and all API routes
- B6: Dockerfile multi-stage, docker-compose.prod.yml, nginx.conf SSL, GitHub Actions CI/CD, health endpoint
- B7: docs/admin-guide-ua.md (marketer guide), docs/deploy.md (VPS instructions), README quickstart

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 19:14:54 +01:00

81 lines
2.5 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import { verifyHMAC, normalizePhone } from '@/lib/binotel'
import { createHmac } from 'crypto'
const SECRET = 'test-secret'
function makeSignature(payload: string, secret: string): string {
return createHmac('sha256', secret).update(payload).digest('hex')
}
describe('verifyHMAC', () => {
it('returns true for a valid signature', () => {
const payload = '{"event":"call"}'
const sig = makeSignature(payload, SECRET)
expect(verifyHMAC(payload, sig, SECRET)).toBe(true)
})
it('returns false for a tampered payload', () => {
const sig = makeSignature('{"event":"call"}', SECRET)
expect(verifyHMAC('{"event":"tampered"}', sig, SECRET)).toBe(false)
})
it('returns false for a wrong secret', () => {
const payload = '{"event":"call"}'
const sig = makeSignature(payload, 'wrong-secret')
expect(verifyHMAC(payload, sig, SECRET)).toBe(false)
})
it('returns false for a signature of different length', () => {
// timingSafeEqual requires same length — the guard should catch this
expect(verifyHMAC('body', 'short', SECRET)).toBe(false)
})
it('returns false for an empty signature', () => {
expect(verifyHMAC('body', '', SECRET)).toBe(false)
})
it('returns true for an empty payload when signature matches', () => {
const sig = makeSignature('', SECRET)
expect(verifyHMAC('', sig, SECRET)).toBe(true)
})
})
describe('normalizePhone', () => {
it('normalizes 380-prefix 12-digit number', () => {
expect(normalizePhone('380501234567')).toBe('+380501234567')
})
it('normalizes 380-prefix with leading +', () => {
expect(normalizePhone('+380501234567')).toBe('+380501234567')
})
it('normalizes 38-prefix 11-digit number', () => {
expect(normalizePhone('38501234567')).toBe('+38501234567')
})
it('normalizes 0-prefix 10-digit number', () => {
expect(normalizePhone('0501234567')).toBe('+380501234567')
})
it('strips non-digit characters before normalizing', () => {
expect(normalizePhone('+38 (050) 123-45-67')).toBe('+380501234567')
})
it('returns null for a number too short', () => {
expect(normalizePhone('12345')).toBeNull()
})
it('returns null for a number too long with no matching prefix', () => {
expect(normalizePhone('123456789012345')).toBeNull()
})
it('returns null for an empty string', () => {
expect(normalizePhone('')).toBeNull()
})
it('returns null for non-Ukrainian prefix', () => {
// 11 digits but not starting with 38
expect(normalizePhone('12345678901')).toBeNull()
})
})