Files
pure-note/web/tests/api.test.ts
T
wangairnan b94e9b1721 fix(web): 修复图片上传、自动保存竞态、登录竞态并统一失效公共缓存
FormData 不强设 Content-Type(P1-2),补 api 客户端回归测试; 自动保存成功仅失效列表与公共键,防详情 refetch 回滚输入(P1-4); login/logout 以请求序号丢弃迟到 /api/me 响应,401 统一回调跳登录(P2-15); 首页不硬编码 page_size,写操作后统一 invalidatePublic,TagPage 补 error 分支(P2-15)。
2026-09-08 17:32:57 +08:00

68 lines
2.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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()
})
})