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 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) }) it('re-fetches from API after the 5-minute cache TTL has expired', async () => { vi.useFakeTimers() mockFetch.mockResolvedValue(okResponse(sampleTariffs)) await getTariffs() // populates cache expect(mockFetch).toHaveBeenCalledTimes(1) vi.advanceTimersByTime(5 * 60 * 1_000 + 1) // expire cache await getTariffs() expect(mockFetch).toHaveBeenCalledTimes(2) vi.useRealTimers() }) }) 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') }) it('retries up to 2 times on network failure, then throws', async () => { mockFetch.mockRejectedValue(new Error('network error')) await expect( createPayment('user@example.com', [{ tariff: 't1', count: '1' }]) ).rejects.toThrow('network error') expect(mockFetch).toHaveBeenCalledTimes(3) }) it('serializes items as JSON in the order FormData field', async () => { mockFetch.mockResolvedValueOnce(okResponse({ url: 'https://pay.example.com/2' })) await createPayment('buyer@example.com', [ { tariff: 'adult', count: '2' }, { tariff: 'child', count: '1' }, ]) const [, init] = mockFetch.mock.calls[0] as [string, RequestInit & { body: FormData }] const order = JSON.parse((init.body as FormData).get('order') as string) expect(order).toEqual([ { tariff: 'adult', count: '2' }, { tariff: 'child', count: '1' }, ]) }) })