- 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>
138 lines
5 KiB
TypeScript
138 lines
5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
import { NextRequest } from 'next/server'
|
|
|
|
const mockCreatePayment = vi.fn()
|
|
const mockPayloadCreate = vi.fn()
|
|
const mockGetPayload = vi.fn(() => ({ create: mockPayloadCreate }))
|
|
const mockSendOrderAlert = vi.fn().mockResolvedValue(undefined)
|
|
|
|
vi.mock('@/lib/ezy', () => ({ createPayment: mockCreatePayment }))
|
|
vi.mock('@/lib/telegram', () => ({ sendOrderAlert: mockSendOrderAlert }))
|
|
vi.mock('payload', () => ({ getPayload: mockGetPayload }))
|
|
vi.mock('@payload-config', () => ({ default: {} }))
|
|
|
|
const VALID_BODY = {
|
|
email: 'buyer@example.com',
|
|
items: [{ tariff: 'adult-2026', count: '2' }],
|
|
}
|
|
|
|
function makeRequest(body: unknown): NextRequest {
|
|
return new NextRequest('http://localhost/api/tickets/checkout', {
|
|
method: 'POST',
|
|
body: JSON.stringify(body),
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
}
|
|
|
|
let POST: (req: NextRequest) => Promise<Response>
|
|
|
|
beforeEach(async () => {
|
|
vi.resetModules()
|
|
mockCreatePayment.mockReset()
|
|
mockPayloadCreate.mockReset()
|
|
mockSendOrderAlert.mockReset()
|
|
const mod = await import('@/app/api/tickets/checkout/route')
|
|
POST = mod.POST
|
|
})
|
|
|
|
describe('POST /api/tickets/checkout', () => {
|
|
it('returns 400 for invalid JSON body', async () => {
|
|
const req = new NextRequest('http://localhost/api/tickets/checkout', {
|
|
method: 'POST',
|
|
body: 'not-json',
|
|
headers: { 'content-type': 'application/json' },
|
|
})
|
|
const res = await POST(req)
|
|
expect(res.status).toBe(400)
|
|
})
|
|
|
|
it('returns 422 when email is missing', async () => {
|
|
const res = await POST(makeRequest({ items: VALID_BODY.items }))
|
|
expect(res.status).toBe(422)
|
|
})
|
|
|
|
it('returns 422 when email is invalid', async () => {
|
|
const res = await POST(makeRequest({ ...VALID_BODY, email: 'not-email' }))
|
|
expect(res.status).toBe(422)
|
|
})
|
|
|
|
it('returns 422 when items array is empty', async () => {
|
|
const res = await POST(makeRequest({ ...VALID_BODY, items: [] }))
|
|
expect(res.status).toBe(422)
|
|
})
|
|
|
|
it('returns 422 when count is not a numeric string', async () => {
|
|
const res = await POST(makeRequest({ ...VALID_BODY, items: [{ tariff: 'x', count: 'abc' }] }))
|
|
expect(res.status).toBe(422)
|
|
})
|
|
|
|
it('returns 422 when count is 0 (below minimum)', async () => {
|
|
const res = await POST(makeRequest({ ...VALID_BODY, items: [{ tariff: 'x', count: '0' }] }))
|
|
expect(res.status).toBe(422)
|
|
})
|
|
|
|
it('returns 422 when count exceeds 10', async () => {
|
|
const res = await POST(makeRequest({ ...VALID_BODY, items: [{ tariff: 'x', count: '11' }] }))
|
|
expect(res.status).toBe(422)
|
|
})
|
|
|
|
it('returns 502 when ezy createPayment fails', async () => {
|
|
mockCreatePayment.mockRejectedValueOnce(new Error('ezy down'))
|
|
const res = await POST(makeRequest(VALID_BODY))
|
|
expect(res.status).toBe(502)
|
|
})
|
|
|
|
it('returns 200 with payment URL on success', async () => {
|
|
mockCreatePayment.mockResolvedValueOnce({ url: 'https://pay.ezy.com/abc' })
|
|
mockPayloadCreate.mockResolvedValueOnce({ id: 'order-1' })
|
|
const res = await POST(makeRequest(VALID_BODY))
|
|
expect(res.status).toBe(200)
|
|
const json = await res.json()
|
|
expect(json.url).toBe('https://pay.ezy.com/abc')
|
|
})
|
|
|
|
it('returns 200 even when DB order creation fails', async () => {
|
|
mockCreatePayment.mockResolvedValueOnce({ url: 'https://pay.ezy.com/xyz' })
|
|
mockPayloadCreate.mockRejectedValueOnce(new Error('DB error'))
|
|
const res = await POST(makeRequest(VALID_BODY))
|
|
expect(res.status).toBe(200)
|
|
expect((await res.json()).url).toBe('https://pay.ezy.com/xyz')
|
|
})
|
|
|
|
it('accepts count of exactly 10 (max valid boundary)', async () => {
|
|
mockCreatePayment.mockResolvedValueOnce({ url: 'https://pay.ezy.com/max' })
|
|
mockPayloadCreate.mockResolvedValueOnce({})
|
|
const res = await POST(makeRequest({ ...VALID_BODY, items: [{ tariff: 'adult', count: '10' }] }))
|
|
expect(res.status).toBe(200)
|
|
})
|
|
|
|
it('returns 422 when items array has more than 20 entries', async () => {
|
|
const items = Array.from({ length: 21 }, (_, i) => ({ tariff: `t${i}`, count: '1' }))
|
|
const res = await POST(makeRequest({ ...VALID_BODY, items }))
|
|
expect(res.status).toBe(422)
|
|
})
|
|
|
|
it('fires sendOrderAlert even when DB order creation fails', async () => {
|
|
mockCreatePayment.mockResolvedValueOnce({ url: 'https://pay.ezy.com/alert' })
|
|
mockPayloadCreate.mockRejectedValueOnce(new Error('DB error'))
|
|
await POST(makeRequest(VALID_BODY))
|
|
// fire-and-forget — wait a tick
|
|
await new Promise((r) => setTimeout(r, 0))
|
|
expect(mockSendOrderAlert).toHaveBeenCalledOnce()
|
|
})
|
|
|
|
it('accepts items array with multiple entries', async () => {
|
|
mockCreatePayment.mockResolvedValueOnce({ url: 'https://pay.ezy.com/multi' })
|
|
mockPayloadCreate.mockResolvedValueOnce({})
|
|
const res = await POST(
|
|
makeRequest({
|
|
email: 'a@b.com',
|
|
items: [
|
|
{ tariff: 'adult', count: '2' },
|
|
{ tariff: 'child', count: '1' },
|
|
],
|
|
})
|
|
)
|
|
expect(res.status).toBe(200)
|
|
})
|
|
})
|