Shumiland/tests/unit/lib/ezy.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

121 lines
4.1 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest'
const mockFetch = vi.fn()
vi.stubGlobal('fetch', mockFetch)
function okResponse(data: unknown): Response {
return {
ok: true,
status: 200,
json: async () => data,
} as unknown as Response
}
function errorResponse(status: number): Response {
return { ok: false, status, json: async () => ({}) } as unknown as Response
}
// Reset module cache between tests so tariffsCache is cleared
let getTariffs: () => Promise<unknown>
let createPayment: (
email: string,
items: Array<{ tariff: string; count: string }>
) => Promise<{ url: string }>
beforeEach(async () => {
vi.resetModules()
mockFetch.mockReset()
vi.stubEnv('EZY_ACTIVITY', 'test-activity')
vi.stubEnv('EZY_PARTNER_KEY', 'test-partner-key')
const mod = await import('@/lib/ezy')
getTariffs = mod.getTariffs
createPayment = mod.createPayment
})
const sampleTariffs = [
{ id: 1, name: 'Adult', price: 250 },
{ id: 2, name: 'Child', price: 150 },
]
describe('getTariffs', () => {
it('fetches and returns tariffs on success', async () => {
mockFetch.mockResolvedValueOnce(okResponse(sampleTariffs))
const result = await getTariffs()
expect(result).toEqual(sampleTariffs)
expect(mockFetch).toHaveBeenCalledOnce()
})
it('returns cached data on second call within 5 minutes', async () => {
mockFetch.mockResolvedValueOnce(okResponse(sampleTariffs))
await getTariffs()
await getTariffs() // should hit cache
expect(mockFetch).toHaveBeenCalledOnce()
})
it('throws when EZY_ACTIVITY env var is missing', async () => {
vi.stubEnv('EZY_ACTIVITY', '')
vi.resetModules()
const mod = await import('@/lib/ezy')
await expect(mod.getTariffs()).rejects.toThrow('EZY_ACTIVITY')
})
it('throws when the API returns a non-ok status', async () => {
mockFetch.mockResolvedValueOnce(errorResponse(503))
await expect(getTariffs()).rejects.toThrow('503')
})
it('throws when the API response fails Zod validation', async () => {
// Missing required `price` field
mockFetch.mockResolvedValueOnce(okResponse([{ id: 1, name: 'Bad' }]))
await expect(getTariffs()).rejects.toThrow()
})
it('retries up to 2 times on network failure, then throws', async () => {
const err = new Error('network error')
mockFetch.mockRejectedValue(err)
await expect(getTariffs()).rejects.toThrow('network error')
// 1 initial + 2 retries = 3 calls
expect(mockFetch).toHaveBeenCalledTimes(3)
})
})
describe('createPayment', () => {
it('returns the payment URL on success', async () => {
mockFetch.mockResolvedValueOnce(okResponse({ url: 'https://pay.example.com/order/123' }))
const result = await createPayment('user@example.com', [{ tariff: 't1', count: '2' }])
expect(result.url).toBe('https://pay.example.com/order/123')
})
it('throws when EZY_PARTNER_KEY env var is missing', async () => {
vi.stubEnv('EZY_PARTNER_KEY', '')
vi.resetModules()
const mod = await import('@/lib/ezy')
await expect(
mod.createPayment('user@example.com', [{ tariff: 't1', count: '1' }])
).rejects.toThrow('EZY_PARTNER_KEY')
})
it('throws when the API returns a non-ok status', async () => {
mockFetch.mockResolvedValueOnce(errorResponse(400))
await expect(createPayment('user@example.com', [{ tariff: 't1', count: '1' }])).rejects.toThrow(
'400'
)
})
it('throws when the API response fails Zod validation (missing url)', async () => {
mockFetch.mockResolvedValueOnce(okResponse({ form: null }))
await expect(
createPayment('user@example.com', [{ tariff: 't1', count: '1' }])
).rejects.toThrow()
})
it('includes email and items in the POST body', async () => {
mockFetch.mockResolvedValueOnce(okResponse({ url: 'https://pay.example.com/1' }))
await createPayment('buyer@example.com', [{ tariff: 'vip', count: '3' }])
const [, init] = mockFetch.mock.calls[0] as [string, RequestInit & { body: FormData }]
const body = init.body as FormData
expect(body.get('email')).toBe('buyer@example.com')
expect(body.get('partner_key')).toBe('test-partner-key')
})
})