- 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>
129 lines
4.1 KiB
TypeScript
129 lines
4.1 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||
|
||
const mockFetch = vi.fn()
|
||
vi.stubGlobal('fetch', mockFetch)
|
||
|
||
let sendLeadAlert: (lead: {
|
||
name: string
|
||
phone: string
|
||
email?: string
|
||
formSource: string
|
||
utmSource?: string
|
||
}) => Promise<void>
|
||
|
||
let sendOrderAlert: (order: {
|
||
email: string
|
||
items: Array<{ tariffId: string; count: number }>
|
||
amount: number
|
||
}) => Promise<void>
|
||
|
||
beforeEach(async () => {
|
||
vi.resetModules()
|
||
mockFetch.mockReset()
|
||
vi.stubEnv('TELEGRAM_BOT_TOKEN', 'test-bot-token')
|
||
vi.stubEnv('TELEGRAM_CHAT_ID', '123456789')
|
||
const mod = await import('@/lib/telegram')
|
||
sendLeadAlert = mod.sendLeadAlert
|
||
sendOrderAlert = mod.sendOrderAlert
|
||
})
|
||
|
||
function telegramOk(): Response {
|
||
return { ok: true, status: 200 } as Response
|
||
}
|
||
|
||
function telegramFail(): Response {
|
||
return { ok: false, status: 400 } as Response
|
||
}
|
||
|
||
describe('sendLeadAlert', () => {
|
||
it('sends a Telegram message with all fields', async () => {
|
||
mockFetch.mockResolvedValueOnce(telegramOk())
|
||
await sendLeadAlert({
|
||
name: 'Іван',
|
||
phone: '+380501234567',
|
||
email: 'ivan@example.com',
|
||
formSource: 'hero-form',
|
||
utmSource: 'google',
|
||
})
|
||
expect(mockFetch).toHaveBeenCalledOnce()
|
||
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]
|
||
const body = JSON.parse(init.body as string)
|
||
expect(body.text).toContain('Іван')
|
||
expect(body.text).toContain('+380501234567')
|
||
expect(body.text).toContain('ivan@example.com')
|
||
expect(body.text).toContain('google')
|
||
expect(body.chat_id).toBe('123456789')
|
||
})
|
||
|
||
it('omits email and utmSource lines when not provided', async () => {
|
||
mockFetch.mockResolvedValueOnce(telegramOk())
|
||
await sendLeadAlert({ name: 'Ганна', phone: '+380671234567', formSource: 'footer-form' })
|
||
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]
|
||
const body = JSON.parse(init.body as string)
|
||
expect(body.text).not.toContain('📧')
|
||
expect(body.text).not.toContain('🔗')
|
||
})
|
||
|
||
it('skips sending when BOT_TOKEN is not configured', async () => {
|
||
vi.stubEnv('TELEGRAM_BOT_TOKEN', '')
|
||
vi.resetModules()
|
||
const mod = await import('@/lib/telegram')
|
||
await mod.sendLeadAlert({ name: 'X', phone: '123', formSource: 'test' })
|
||
expect(mockFetch).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('skips sending when CHAT_ID is not configured', async () => {
|
||
vi.stubEnv('TELEGRAM_CHAT_ID', '')
|
||
vi.resetModules()
|
||
const mod = await import('@/lib/telegram')
|
||
await mod.sendLeadAlert({ name: 'X', phone: '123', formSource: 'test' })
|
||
expect(mockFetch).not.toHaveBeenCalled()
|
||
})
|
||
|
||
it('does not throw when Telegram API returns non-ok status', async () => {
|
||
mockFetch.mockResolvedValueOnce(telegramFail())
|
||
await expect(
|
||
sendLeadAlert({ name: 'X', phone: '123', formSource: 'test' })
|
||
).resolves.not.toThrow()
|
||
})
|
||
|
||
it('does not throw when fetch rejects (network error)', async () => {
|
||
mockFetch.mockRejectedValueOnce(new Error('network error'))
|
||
await expect(
|
||
sendLeadAlert({ name: 'X', phone: '123', formSource: 'test' })
|
||
).resolves.not.toThrow()
|
||
})
|
||
})
|
||
|
||
describe('sendOrderAlert', () => {
|
||
it('sends a Telegram message with order details', async () => {
|
||
mockFetch.mockResolvedValueOnce(telegramOk())
|
||
await sendOrderAlert({
|
||
email: 'buyer@example.com',
|
||
items: [{ tariffId: 'adult', count: 2 }],
|
||
amount: 500,
|
||
})
|
||
expect(mockFetch).toHaveBeenCalledOnce()
|
||
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]
|
||
const body = JSON.parse(init.body as string)
|
||
expect(body.text).toContain('buyer@example.com')
|
||
expect(body.text).toContain('adult')
|
||
expect(body.text).toContain('500')
|
||
})
|
||
|
||
it('formats multiple items in the message', async () => {
|
||
mockFetch.mockResolvedValueOnce(telegramOk())
|
||
await sendOrderAlert({
|
||
email: 'b@b.com',
|
||
items: [
|
||
{ tariffId: 'adult', count: 1 },
|
||
{ tariffId: 'child', count: 3 },
|
||
],
|
||
amount: 700,
|
||
})
|
||
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit]
|
||
const body = JSON.parse(init.body as string)
|
||
expect(body.text).toContain('adult × 1')
|
||
expect(body.text).toContain('child × 3')
|
||
})
|
||
})
|