49 lines
2.2 KiB
TypeScript
49 lines
2.2 KiB
TypeScript
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
import { z } from 'zod'
|
|
import { ApiClient } from '../../../src/api/client'
|
|
|
|
describe('ApiClient', () => {
|
|
afterEach(() => vi.restoreAllMocks())
|
|
|
|
it('handles empty 204 responses', async () => {
|
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 204 }))
|
|
await expect(new ApiClient('/api', () => 'token').request('/logout', { method: 'POST' })).resolves.toBeUndefined()
|
|
})
|
|
|
|
it('validates successful responses with zod', async () => {
|
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue(Response.json({ id: 'wrong' }))
|
|
await expect(
|
|
new ApiClient('/api', () => null).request('/me', { schema: z.object({ id: z.number() }) }),
|
|
).rejects.toMatchObject({ kind: 'validation' })
|
|
})
|
|
|
|
it('refreshes and replays a business request once after 401', async () => {
|
|
let token = 'old-token'
|
|
const refresh = vi.fn(async (force?: boolean) => {
|
|
if (!force) return false
|
|
token = 'new-token'
|
|
return true
|
|
})
|
|
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
|
.mockResolvedValueOnce(Response.json({ error: { message: '需要有效的访问令牌' } }, { status: 401 }))
|
|
.mockResolvedValueOnce(Response.json({ ok: true }))
|
|
|
|
await expect(new ApiClient('/api', () => token, refresh).request('/runs/1')).resolves.toEqual({ ok: true })
|
|
expect(refresh).toHaveBeenNthCalledWith(1, false)
|
|
expect(refresh).toHaveBeenCalledWith(true)
|
|
expect(fetchMock).toHaveBeenCalledTimes(2)
|
|
expect(new Headers(fetchMock.mock.calls[1][1]?.headers).get('Authorization')).toBe('Bearer new-token')
|
|
})
|
|
|
|
it('does not refresh or replay a request more than once', async () => {
|
|
const refresh = vi.fn().mockImplementation(async (force?: boolean) => Boolean(force))
|
|
vi.spyOn(globalThis, 'fetch').mockResolvedValue(
|
|
Response.json({ error: { message: '需要有效的访问令牌' } }, { status: 401 }),
|
|
)
|
|
await expect(new ApiClient('/api', () => 'token', refresh).request('/me')).rejects.toMatchObject({ kind: 'authentication' })
|
|
expect(refresh).toHaveBeenCalledTimes(2)
|
|
expect(refresh).toHaveBeenLastCalledWith(true)
|
|
expect(fetch).toHaveBeenCalledTimes(2)
|
|
})
|
|
})
|