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 支持而完全无法运行,顺手改为显式赋值使基建可用)
84 lines
3.2 KiB
TypeScript
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')
|
|
})
|