| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128 |
- /**
- * 票 22 离线回归:真实 AjaxResult 信封消费与业务错误映射(不联网)。
- *
- * 覆盖 2026-10-10 真实审计里三项 `frontend-*` 失败检查对应的**同一批领域出口**
- * (`fetchBaseArchives` / `fetchMapSnapshot` / `fetchCurrentEvents`),外加
- * mock 分支、裸载荷与业务错误类别白名单,用注入的 `fetchImpl` 复现同一形状。
- *
- * 与 `verify/t18-real-audit.mjs` 的分工:那份跑真实后端;本份可在无后端、
- * 无浏览器时锁定消费语义。真机复验仍以票 22 的待验证项 1/2 为准。
- */
- import assert from 'node:assert/strict'
- import { requestCockpitJson, registerMockTransport, resetMockTransportsForTest } from '../../../src/modules/cockpit/services/cockpitApi.js'
- import { fetchMapSnapshot } from '../../../src/modules/cockpit/services/mapApi.js'
- import { fetchBaseArchives } from '../../../src/modules/cockpit/services/infrastructureApi.js'
- import { fetchCurrentEvents } from '../../../src/modules/cockpit/services/eventApi.js'
- import { REQUEST_REASON } from '../../../src/modules/cockpit/domain/requestReason.js'
- import { SECTION_STATE, sectionStateFromBackend, sectionStateFromFreshness } from '../../../src/modules/cockpit/domain/sectionState.js'
- const config = { dataSource: 'real', api: { baseUrl: 'http://acceptance.invalid/api/cockpit/v1' } }
- const respond = (body, status = 200) => ({ ok: status >= 200 && status < 300, status, json: async () => body })
- const envelope = (data) => ({ code: 200, msg: 'success', data })
- let failed = 0
- async function check(name, action) {
- try {
- await action()
- console.log(`PASS ${name}`)
- } catch (error) {
- failed += 1
- console.error(`FAIL ${name}: ${String(error.message).split('\n')[0]}`)
- }
- }
- // 一、信封消费:三条真实失败检查对应的领域出口,载荷必须解包而不是整包透传。
- await check('base-archives envelope is unwrapped into a non-empty archive section', async () => {
- const section = await fetchBaseArchives({ config, fetchImpl: async () => respond(envelope({
- totals: { facilityCount: 1006 }, industryDistribution: [{ code: 'WATER_SUPPLY', count: 10 }],
- })) })
- assert.equal(section.state, 'success')
- assert.equal(section.data.facilityCount, 1006)
- })
- await check('map-snapshot envelope keeps total and snapshotAt', async () => {
- const section = await fetchMapSnapshot({ config, pageSize: 1000, fetchImpl: async () => respond(envelope({
- snapshotAt: '2026-10-10T10:25:00+08:00', total: 13, monitoringPoints: [], unassociatedEquipment: [],
- })) })
- assert.equal(section.state, 'success')
- assert.equal(section.data.total, 13)
- assert.equal(section.data.snapshotAt, '2026-10-10T10:25:00+08:00')
- })
- // 二、业务错误不得读成成功(HTTP 200 + code=500)。
- await check('HTTP200 business 500 is a failed section, never success', async () => {
- const section = await fetchCurrentEvents({ config, mode: 'ALARM', fetchImpl: async () => respond({ code: 500, msg: 'failure', data: null }) })
- assert.equal(section.state, 'failed')
- assert.equal(section.reason, REQUEST_REASON.UNAVAILABLE)
- })
- await check('HTTP200 business 401/403 stays a credential-class failure', async () => {
- const result = await requestCockpitJson('/map-snapshot', { config, fetchImpl: async () => respond({ code: 403, msg: 'denied', data: null }) })
- assert.equal(result.ok, false)
- assert.equal(result.reason, REQUEST_REASON.UNAUTHORIZED)
- })
- // 三、业务类别映射:白名单内可恢复;白名单外(含对象继承键)一律回空串。
- await check('known business category survives the HTTP200 envelope path', async () => {
- const result = await requestCockpitJson('/map-snapshot', { config, fetchImpl: async () => respond({ code: 500, msg: 'failure', data: { category: 'SNAPSHOT_EXPIRED' } }) })
- assert.equal(result.reason, REQUEST_REASON.SNAPSHOT_EXPIRED)
- })
- for (const category of ['toString', 'constructor', '__proto__', 'hasOwnProperty', 'valueOf']) {
- await check(`unknown business category "${category}" cannot leak a non-contract reason`, async () => {
- const result = await requestCockpitJson('/map-snapshot', { config, fetchImpl: async () => respond({ code: 500, msg: 'failure', data: { category } }) })
- assert.equal(result.reason, REQUEST_REASON.UNAVAILABLE)
- })
- }
- // 四、信封自身不完整 / 非 JSON 时不得冒充成功。
- await check('successful envelope without data is a bad payload', async () => {
- const result = await requestCockpitJson('/map-snapshot', { config, fetchImpl: async () => respond({ code: 200, msg: 'success' }) })
- assert.equal(result.ok, false)
- assert.equal(result.reason, REQUEST_REASON.BAD_PAYLOAD)
- })
- await check('non-JSON body stays a bad payload', async () => {
- const result = await requestCockpitJson('/map-snapshot', { config, fetchImpl: async () => ({
- ok: true, status: 200, json: async () => { throw new Error('Unexpected token <') }, text: async () => '<html>',
- }) })
- assert.equal(result.ok, false)
- assert.equal(result.reason, REQUEST_REASON.BAD_PAYLOAD)
- })
- // 五、mock 分支与裸载荷路径未被信封分支误伤。
- await check('plain compatibility payload is returned unchanged', async () => {
- const payload = { value: 0 }
- const result = await requestCockpitJson('/foundation', { config, fetchImpl: async () => respond(payload) })
- assert.equal(result.data, payload)
- })
- await check('mock handler result is returned as-is without envelope handling', async () => {
- resetMockTransportsForTest()
- registerMockTransport('/probe/envelope', () => ({ code: 500, msg: 'mock domain field, not a business failure', data: null }))
- const result = await requestCockpitJson('/probe/envelope', { config: { dataSource: 'mock' }, fetchImpl: async () => {
- throw new Error('mock mode must not fetch')
- } })
- assert.equal(result.ok, true)
- assert.equal(result.data.code, 500)
- resetMockTransportsForTest()
- })
- // 六、后端分区状态/新鲜度映射同样只认自有键(业务错误映射的另一半面)。
- for (const state of ['toString', 'constructor', '__proto__', 'valueOf']) {
- await check(`unknown backend section state "${state}" cannot leak a non-contract state`, async () => {
- assert.equal(sectionStateFromBackend(state), SECTION_STATE.FAILED)
- assert.equal(sectionStateFromFreshness(state), null)
- })
- }
- await check('known backend section states and freshness still map 1:1', async () => {
- assert.equal(sectionStateFromBackend('SUCCESS'), SECTION_STATE.SUCCESS)
- assert.equal(sectionStateFromBackend('UNAVAILABLE'), SECTION_STATE.UNAVAILABLE)
- assert.equal(sectionStateFromBackend('READY'), SECTION_STATE.SUCCESS)
- assert.equal(sectionStateFromFreshness('STALE'), SECTION_STATE.STALE)
- assert.equal(sectionStateFromFreshness('CURRENT'), SECTION_STATE.SUCCESS)
- })
- console.log(`T22 envelope consumption: ${failed ? 'FAILED' : 'passed'}`)
- process.exitCode = failed ? 1 : 0
|