- 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>
81 lines
2.5 KiB
TypeScript
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()
|
|
})
|
|
})
|