Initial commit
This commit is contained in:
commit
89053153cc
8152 files changed
+2086461
No files matched your search
@@ -0,0 +1,80 @@
|
||||
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')
|
||||
})
|
||||
})
|
||||
Reference in new issue
Block a user