81 lines
2.5 KiB
TypeScript
81 lines
2.5 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { ApiError, apiFetch } from '../src/api/client'
|
|
|
|
/* fetch is stubbed with Response-shaped objects — the client only needs
|
|
status/ok/json, and this keeps the test independent of jsdom's fetch. */
|
|
|
|
function stubFetch(body: unknown, status = 200) {
|
|
const mock = vi.fn().mockResolvedValue({
|
|
status,
|
|
ok: status >= 200 && status < 300,
|
|
statusText: 'status text',
|
|
json: () => Promise.resolve(body),
|
|
})
|
|
vi.stubGlobal('fetch', mock)
|
|
return mock
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.unstubAllGlobals()
|
|
})
|
|
|
|
describe('apiFetch', () => {
|
|
it('decodes a JSON response', async () => {
|
|
stubFetch([{ id: 1, title: 'First' }])
|
|
|
|
await expect(apiFetch('/api/v1/items')).resolves.toEqual([{ id: 1, title: 'First' }])
|
|
})
|
|
|
|
it('sends method, JSON headers and a JSON body', async () => {
|
|
const mock = stubFetch({ id: 1 })
|
|
|
|
await apiFetch('/api/v1/items', { method: 'POST', body: JSON.stringify({ title: 'x' }) })
|
|
|
|
const [path, init] = mock.mock.calls[0]! as [string, RequestInit]
|
|
expect(path).toBe('/api/v1/items')
|
|
expect(init.method).toBe('POST')
|
|
expect(init.body).toBe(JSON.stringify({ title: 'x' }))
|
|
expect(init.headers).toMatchObject({ 'Content-Type': 'application/json' })
|
|
})
|
|
|
|
it('returns undefined for 204 responses', async () => {
|
|
stubFetch(null, 204)
|
|
|
|
await expect(apiFetch<void>('/api/v1/items/1', { method: 'DELETE' })).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('turns the backend error envelope into an ApiError', async () => {
|
|
stubFetch({ error: { code: 'validation_failed', message: 'title: required' } }, 400)
|
|
|
|
const err = await apiFetch('/api/v1/items', { method: 'POST' }).catch(e => e)
|
|
|
|
expect(err).toBeInstanceOf(ApiError)
|
|
expect(err).toMatchObject({
|
|
status: 400,
|
|
code: 'validation_failed',
|
|
message: 'title: required',
|
|
})
|
|
})
|
|
|
|
it('falls back to statusText when the body is not the envelope', async () => {
|
|
stubFetch('<html>gateway timeout</html>', 504)
|
|
|
|
const err = await apiFetch('/api/v1/items').catch(e => e)
|
|
|
|
expect(err).toMatchObject({ status: 504, code: 'unknown', message: 'status text' })
|
|
})
|
|
|
|
it('wraps network failures (backend down) as a network ApiError', async () => {
|
|
vi.stubGlobal(
|
|
'fetch',
|
|
vi.fn().mockRejectedValue(new TypeError('fetch failed')),
|
|
)
|
|
|
|
const err = await apiFetch('/api/v1/items').catch(e => e)
|
|
|
|
expect(err).toBeInstanceOf(ApiError)
|
|
expect(err.code).toBe('network')
|
|
expect(err.message).toContain('Go server')
|
|
})
|
|
})
|