Files
Profeto/frontend/src/lib/http.test.ts
T
WorkBuddy 1422afd6f6 fix(admin): /health 请求被误加 /api/v1 前缀,健康状态永远「系统异常」
build_url 对所有路径强制加 /api/v1,但后端 /health 与 /health/ready
挂在根路径(app.py,与 compose healthcheck 一致)→ 前端请求 /api/v1/health
404 → fetchHealth 吞错返回 status=unknown → 右上角/监控页永远「系统异常」。

- build_url 对 /health 与 /health/* 豁免前缀
- http.test.ts 补 URL 豁免用例(该测试此前因 ApiError 参数属性不被
  strip-types 支持而完全无法运行,顺手改为显式赋值使基建可用)
2026-09-22 14:22:51 +08:00

84 lines
3.2 KiB
TypeScript

/**
* P0-00: HTTP client method/body/headers 可信度测试。
* 运行: node --experimental-strip-types frontend/src/lib/http.test.ts
*
* 最小环境 polyfill:Node 22 自带 fetch/AbortController,本测试不触发 401 路径,
* 故 window.dispatchEvent 不会被调用,无需完整 DOM。
*/
import { test } from 'node:test'
import assert from 'node:assert/strict'
// 最小浏览器环境 polyfill(仅覆盖 http.ts 在 happy path 用到的全局)
const store: Record<string, string> = {}
// @ts-expect-error 测试用最小 window stub
globalThis.window = {
dispatchEvent: () => false,
localStorage: {
getItem: (k: string) => store[k] ?? null,
setItem: (k: string, v: string) => { store[k] = v },
removeItem: (k: string) => { delete store[k] },
},
}
// 捕获每次 fetch 的入参供断言
let lastUrl = ''
let lastInit: RequestInit | undefined
globalThis.fetch = async (url: string | URL | Request, init?: RequestInit) => {
lastUrl = String(url)
lastInit = init
return new Response(JSON.stringify({ ok: true }), { status: 200, headers: { 'content-type': 'application/json' } })
}
const { http } = await import('./http.ts')
test('GET: method=GET, 无 body, 无 Content-Type', async () => {
await http.get('/api/v1/matches')
assert.equal(lastInit?.method, 'GET')
assert.equal(lastInit?.body, undefined)
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], undefined)
})
test('POST: method=POST, 序列化 body, 有 Content-Type', async () => {
await http.post('/api/v1/matches', { a: 1 })
assert.equal(lastInit?.method, 'POST')
assert.equal(lastInit?.body, JSON.stringify({ a: 1 }))
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], 'application/json')
})
test('POST 空 body: 不设 Content-Type', async () => {
await http.post('/api/v1/matches', undefined)
assert.equal(lastInit?.method, 'POST')
assert.equal(lastInit?.body, undefined)
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], undefined)
})
test('PUT: method=PUT, 有 body 与 Content-Type', async () => {
await http.put('/api/v1/x', { b: 2 })
assert.equal(lastInit?.method, 'PUT')
assert.equal(lastInit?.body, JSON.stringify({ b: 2 }))
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], 'application/json')
})
test('DELETE: method=DELETE, 无 body, 无 Content-Type', async () => {
await http.delete('/api/v1/x/1')
assert.equal(lastInit?.method, 'DELETE')
assert.equal(lastInit?.body, undefined)
assert.equal((lastInit?.headers as Record<string, string>)?.['Content-Type'], undefined)
})
// ── 健康检查豁免: /health* 挂在根路径(后端 app.py 不在 /api/v1 下),
// 加前缀会 404 → 管理后台右上角永远「系统异常」 ──
test('build_url: /health 与 /health/ready 不加 /api/v1 前缀', async () => {
await http.get('/health')
assert.equal(lastUrl, '/health')
await http.get('/health/ready')
assert.equal(lastUrl, '/health/ready')
})
test('build_url: 常规 API 路径仍统一加 /api/v1', async () => {
await http.get('/admin/stats')
assert.equal(lastUrl, '/api/v1/admin/stats')
await http.get('/matches')
assert.equal(lastUrl, '/api/v1/matches')
})