// 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() }) })