FormData 不强设 Content-Type(P1-2),补 api 客户端回归测试; 自动保存成功仅失效列表与公共键,防详情 refetch 回滚输入(P1-4); login/logout 以请求序号丢弃迟到 /api/me 响应,401 统一回调跳登录(P2-15); 首页不硬编码 page_size,写操作后统一 invalidatePublic,TagPage 补 error 分支(P2-15)。
68 lines
2.6 KiB
TypeScript
68 lines
2.6 KiB
TypeScript
// api 客户端单测(评审 round2 P1-2 / P2-15 的回归守护):
|
||
// 1. FormData 请求不得强设 Content-Type(multipart boundary 由浏览器生成);
|
||
// 2. 401 触发全局未授权回调(AuthProvider 据此跳登录页)。
|
||
import { describe, it, expect, vi, afterEach } from 'vitest'
|
||
import { api, ApiError, setUnauthorizedHandler } from '../src/lib/api'
|
||
|
||
function jsonResponse(status: number, body: unknown): Response {
|
||
return new Response(JSON.stringify(body), {
|
||
status,
|
||
headers: { 'Content-Type': 'application/json' },
|
||
})
|
||
}
|
||
|
||
describe('api 客户端', () => {
|
||
afterEach(() => {
|
||
vi.unstubAllGlobals()
|
||
setUnauthorizedHandler(null)
|
||
})
|
||
|
||
it('FormData 请求不强设 Content-Type(保留浏览器 multipart boundary)', async () => {
|
||
const fetchMock = vi.fn().mockResolvedValue(jsonResponse(201, { data: { id: 1, url: '/api/images/1' } }))
|
||
vi.stubGlobal('fetch', fetchMock)
|
||
|
||
const form = new FormData()
|
||
form.append('file', new Blob(['x'], { type: 'image/png' }), 'a.png')
|
||
await api<{ id: number }>('/api/admin/images', { method: 'POST', body: form })
|
||
|
||
expect(fetchMock).toHaveBeenCalledOnce()
|
||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||
const headers = init.headers as Headers
|
||
expect(headers.has('Content-Type')).toBe(false)
|
||
})
|
||
|
||
it('JSON 请求默认补 application/json 并携带 CSRF 头', async () => {
|
||
const fetchMock = vi.fn().mockResolvedValue(jsonResponse(200, { data: { ok: true } }))
|
||
vi.stubGlobal('fetch', fetchMock)
|
||
const { setCsrfToken } = await import('../src/lib/api')
|
||
setCsrfToken('token-abc')
|
||
|
||
await api('/api/admin/notes/1', { method: 'PUT', body: JSON.stringify({ title: 'x' }) })
|
||
|
||
const [, init] = fetchMock.mock.calls[0] as [string, RequestInit]
|
||
const headers = init.headers as Headers
|
||
expect(headers.get('Content-Type')).toBe('application/json')
|
||
expect(headers.get('X-CSRF-Token')).toBe('token-abc')
|
||
setCsrfToken(null)
|
||
})
|
||
|
||
it('401 触发全局未授权回调并抛出 ApiError', async () => {
|
||
const fetchMock = vi.fn().mockResolvedValue(
|
||
jsonResponse(401, { error: { code: 'unauthorized', message: '未登录或会话已过期' } }),
|
||
)
|
||
vi.stubGlobal('fetch', fetchMock)
|
||
const handler = vi.fn()
|
||
setUnauthorizedHandler(handler)
|
||
|
||
try {
|
||
await api('/api/admin/notes', { method: 'DELETE' })
|
||
expect.unreachable('应抛出 ApiError')
|
||
} catch (e) {
|
||
expect(e).toBeInstanceOf(ApiError)
|
||
expect((e as ApiError).status).toBe(401)
|
||
expect((e as ApiError).code).toBe('unauthorized')
|
||
}
|
||
expect(handler).toHaveBeenCalledOnce()
|
||
})
|
||
})
|