import { describe, expect, it, vi } from 'vitest'; import { ApiError, createApiClient } from '../../src/api/createApiClient'; describe('createApiClient', () => { it('sends json requests and returns parsed payload', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: true, status: 200, json: vi.fn().mockResolvedValue({ ok: true }), } as unknown as Response); const client = createApiClient({ baseUrl: 'https://api.example.com', fetchImpl: fetchMock as typeof fetch, }); const result = await client.request<{ ok: boolean }>('/users', { method: 'POST', token: 'token-123', body: { hello: 'world' }, }); expect(result).toEqual({ ok: true }); expect(fetchMock).toHaveBeenCalledTimes(1); expect(fetchMock).toHaveBeenCalledWith('https://api.example.com/users', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: 'Bearer token-123', }, body: JSON.stringify({ hello: 'world' }), }); }); it('maps api error payload through custom resolver', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 401, json: vi.fn().mockResolvedValue({ code: 'AUTH_UNAUTHORIZED', error: 'unauthorized', requestId: 'req-1', details: { reason: 'expired' }, }), } as unknown as Response); const resolveError = vi.fn(() => 'Unauthorized access. Please sign in again.'); const client = createApiClient({ baseUrl: 'https://api.example.com', fetchImpl: fetchMock as typeof fetch, resolveError, }); await expect(client.request('/users')).rejects.toMatchObject({ name: 'ApiError', status: 401, code: 'AUTH_UNAUTHORIZED', requestId: 'req-1', details: { reason: 'expired' }, rawMessage: 'unauthorized', message: 'Unauthorized access. Please sign in again.', }); expect(resolveError).toHaveBeenCalledWith({ code: 'AUTH_UNAUTHORIZED', status: 401, fallbackMessage: 'unauthorized', }); }); it('infers an error code from status when payload has no code', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 404, json: vi.fn().mockResolvedValue({ error: 'missing resource' }), } as unknown as Response); const inferErrorCodeFromStatus = vi.fn((status?: number | null) => status === 404 ? 'USER_NOT_FOUND' : undefined, ); const client = createApiClient({ baseUrl: 'https://api.example.com', fetchImpl: fetchMock as typeof fetch, inferErrorCodeFromStatus, resolveError: ({ code }) => (code === 'USER_NOT_FOUND' ? 'User not found.' : 'Unknown'), }); await expect(client.request('/users/missing')).rejects.toMatchObject({ code: 'USER_NOT_FOUND', message: 'User not found.', }); }); it('falls back to default messages when response is not valid json', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 500, json: vi.fn().mockRejectedValue(new Error('invalid json')), } as unknown as Response); const client = createApiClient({ baseUrl: 'https://api.example.com', fetchImpl: fetchMock as typeof fetch, }); await expect(client.request('/users')).rejects.toMatchObject({ code: undefined, rawMessage: undefined, message: 'Request failed (500).', }); }); it('uses generic default message when status is unavailable', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: undefined, json: vi.fn().mockResolvedValue(null), } as unknown as Response); const client = createApiClient({ baseUrl: 'https://api.example.com', fetchImpl: fetchMock as typeof fetch, }); let thrown: unknown; try { await client.request('/users'); } catch (err) { thrown = err; } expect(thrown).toBeInstanceOf(ApiError); expect((thrown as ApiError).message).toBe('Request failed. Please try again.'); }); it('uses raw fallback error text with the default resolver when present', async () => { const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 400, json: vi.fn().mockResolvedValue({ error: 'Validation failed in backend.', }), } as unknown as Response); const client = createApiClient({ baseUrl: 'https://api.example.com', fetchImpl: fetchMock as typeof fetch, }); await expect(client.request('/users')).rejects.toMatchObject({ message: 'Validation failed in backend.', rawMessage: 'Validation failed in backend.', }); }); });