import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import test from 'node:test'; import { auditPlatform, scanInternalIdLeaks } from './audit-check.mjs'; const resourcesSource = readFileSync('src/api/resources.ts', 'utf8'); test('资源定义使用 safe 和 audit 前缀', () => { assert.match(resourcesSource, /define\('safe_event', '\/safety\/safe_event'/); assert.match(resourcesSource, /define\('audit_approval', '\/audit\/audit_approval'/); }); test('只读页面将状态变更视为违规写操作', () => { const failures = auditPlatform({ manifest: { resources: [], routes: [] }, resources: [], readOnlyPage: '', routeSources: [], viewSources: new Map(), apiSources: new Map(), }); assert.deepEqual(failures, ['readonly: mutation action exposed (updateStatus)']); }); test('扫描 API 和页面中用于展示或请求的内部 ID', () => { const failures = scanInternalIdLeaks(new Map([ ['src/api/leak.ts', "resourceApi.create('/gas/gas_basic', { gas_basic_id: 7 })"], ['src/views/leak.vue', ''], ])); assert.deepEqual(failures, [ 'src/api/leak.ts: internal identifier gas_basic_id', 'src/views/leak.vue: internal identifier id', ]); }); test('防护表达式不能掩盖同一行的内部 ID 泄漏', () => { const failures = scanInternalIdLeaks(new Map([ ['src/views/leak.vue', "const visible = row.id; const safe = key !== 'id';"], ['src/api/leak.ts', "send({ gas_basic_id: 7 }); const safe = key.endsWith('_id');"], ])); assert.deepEqual(failures, [ 'src/views/leak.vue: internal identifier id', 'src/api/leak.ts: internal identifier gas_basic_id', ]); }); test('每个资源必须由带菜单元数据的路由实际加载对应页面', () => { const failures = auditPlatform({ manifest: { resources: [{ domain: 'gas', name: 'gas_basic', path: '/gas/gas_basic', mode: 'writable', pageKind: 'list' }], routes: [ { method: 'GET', path: '/gas/gas_basic' }, { method: 'POST', path: '/gas/gas_basic' }, { method: 'GET', path: '/gas/gas_basic/:identity' }, { method: 'PUT', path: '/gas/gas_basic/:identity' }, { method: 'PATCH', path: '/gas/gas_basic/:identity/status' }, { method: 'DELETE', path: '/gas/gas_basic/:identity' }, ] }, resources: [{ name: 'gas_basic', resource: '/gas/gas_basic', mode: 'writable', pageKind: 'list', title: '气站管理', fields: [{ key: 'name', label: '名称' }] }], readOnlyPage: '', routeSources: ["{ component: () => import('@/views/gas/gas_basic/ListPage.vue') }"], viewSources: new Map([['src/views/gas/gas_basic/ListPage.vue', "getResource('/gas/gas_basic')"]]), apiSources: new Map(), }); assert.deepEqual(failures, ['gas/gas_basic: missing menu route']); }); test('仅追加处置必须挂在安全事件详情动作且不得有独立页面', () => { const failures = auditPlatform({ manifest: { resources: [{ domain: 'safety', name: 'saf_event_disposal', path: '/safety/saf_event/:identity/disposals', mode: 'append_only', pageKind: 'list' }], routes: [ { method: 'GET', path: '/safety/saf_event/:identity/disposals' }, { method: 'POST', path: '/safety/saf_event/:identity/disposals' }, ] }, resources: [{ name: 'saf_event_disposal', resource: '/safety/saf_event/:identity/disposals', mode: 'append_only', pageKind: 'list', title: '事件处置', fields: [{ key: 'action', label: '处置动作' }] }], readOnlyPage: '', routeSources: [], viewSources: new Map([['src/views/safety/saf_event_disposal/ListPage.vue', '