import { test } from 'node:test' import assert from 'node:assert/strict' import { readFileSync } from 'node:fs' import { ApiError } from '@/shared' import { fetchRevisions, revisionLoadErrorText } from '../components/history/revisionsSource' /** * 访客进「变更历史」时的口径(P2 A31)。 * * `/api/revisions` 是 recorder 起的接口,而面板未登录时会降级成「只读访客」。 * 以前:侧栏照样给出入口,点进去页面把服务端/状态码原样(「变更历史接口返回 HTTP 401」) * 摔在红横幅上——操作员只会以为系统坏了。 * 现在:侧栏对只读访客隐藏入口,页面把权限失败换成人话,其它错误仍原样透传。 */ interface Captured { url?: string status?: number } function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { status, headers: { 'Content-Type': 'application/json' }, }) } function stubFetch(reply: () => Response, captured: Captured): void { globalThis.fetch = (async (input: unknown) => { captured.url = String(input) const response = reply() captured.status = response.status return response }) as typeof fetch } test('403 按权限边界换成人话,不出现 HTTP 状态码', () => { const text = revisionLoadErrorText( new ApiError('FORBIDDEN', '无权访问该资源', 403, { required: 'recorder' }), ) assert.match(text, /录入员/) assert.match(text, /只读访客/) assert.doesNotMatch(text, /HTTP/) }) test('未登录(401 AUTH_REQUIRED)提示去登录', () => { const text = revisionLoadErrorText(new ApiError('AUTH_REQUIRED', '请先登录', 401)) assert.match(text, /登录/) assert.doesNotMatch(text, /HTTP/) }) test('其它错误原样透传,不被美化掉', () => { assert.equal(revisionLoadErrorText(new ApiError('INTERNAL', '数据库忙', 500)), '数据库忙') assert.equal(revisionLoadErrorText(new Error('boom')), 'boom') assert.equal(revisionLoadErrorText(undefined), '变更历史接口不可用') }) test('服务端 403 的 error.code / message 被读出来(不再只看状态码猜)', async () => { const captured: Captured = {} stubFetch( () => jsonResponse({ ok: false, error: { code: 'FORBIDDEN', message: '无权访问该资源' } }, 403), captured, ) await assert.rejects( () => fetchRevisions(), (error: unknown) => { assert.ok(error instanceof ApiError) assert.equal(error.code, 'FORBIDDEN') assert.equal(error.message, '无权访问该资源') assert.equal(revisionLoadErrorText(error).includes('无权访问该资源'), false) return true }, ) assert.equal(captured.url, '/api/revisions') }) test('只有一个 eventId 过滤条件时拼 query,成功路径照旧解析', async () => { const captured: Captured = {} stubFetch(() => jsonResponse({ ok: true, data: [] }), captured) const rows = await fetchRevisions({ eventId: 3 }) assert.equal(captured.url, '/api/revisions?eventId=3') assert.deepEqual(rows, []) }) test('连不上服务端仍是 NETWORK(页面据此降级示例数据)', async () => { globalThis.fetch = (async () => { throw new TypeError('fetch failed') }) as typeof fetch await assert.rejects( () => fetchRevisions(), (error: unknown) => error instanceof ApiError && error.code === 'NETWORK', ) }) function readSource(relative: string): string { return readFileSync(new URL(relative, import.meta.url), 'utf8') } const HISTORY_PAGE_SOURCE = readSource('../pages/HistoryPage.vue') const LAYOUT_SOURCE = readSource('../layouts/AdminLayout.vue') test('HistoryPage 的两个失败分支都走统一口径', () => { const hits = HISTORY_PAGE_SOURCE.match(/revisionLoadErrorText\(caught\)/g) ?? [] assert.equal(hits.length, 2, '列表与详情都要走同一套文案') assert.doesNotMatch(HISTORY_PAGE_SOURCE, /loadError\.value = caught instanceof Error/) }) test('侧栏对只读访客隐藏「变更历史」,不再让人点进权限墙', () => { assert.match(LAYOUT_SOURCE, /name: 'history'[^}]*recorderOnly: true/) assert.match(LAYOUT_SOURCE, /item\.recorderOnly === true && auth\.readOnly/) })