119 lines
6.7 KiB
JavaScript
119 lines
6.7 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import test from 'node:test'
|
|
import { readFileSync } from 'node:fs'
|
|
import { run as runPdaReleases } from './pda-releases.mjs'
|
|
import { run as runAudit } from './audit.mjs'
|
|
import { run as runSailingCalendar } from './sailing-calendar.mjs'
|
|
|
|
const entry = readFileSync(new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8')
|
|
|
|
function context(handler) {
|
|
const calls = []
|
|
const gaps = []
|
|
return {
|
|
marker: 'ADMIN-API-OFFLINE',
|
|
calls,
|
|
gaps,
|
|
async request(method, path, body) {
|
|
calls.push({ method, path, body })
|
|
return handler(method, path, body)
|
|
},
|
|
async requestMultipart(method, path, form) {
|
|
calls.push({ method, path, multipart: form })
|
|
return handler(method, path, form)
|
|
},
|
|
apiSuccess(_module, _name, response) {
|
|
assert.equal(response.http, 200)
|
|
assert.equal(String(response.body?.code), '200')
|
|
return response.body.data
|
|
},
|
|
assert(_module, name, _response, condition, evidence) {
|
|
assert.equal(Boolean(condition), true, `${name}: ${evidence}`)
|
|
return true
|
|
},
|
|
addCoverageGap(module, scenario, name, reason, category, metadata) {
|
|
gaps.push({ module, scenario, name, reason, category, metadata })
|
|
},
|
|
}
|
|
}
|
|
|
|
test('PDA 版本模块注册为默认模块并只读核对双端发布列表', async () => {
|
|
assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'pda-releases'/)
|
|
const releases = (app) => [{
|
|
id: '1', app, appid: `__UNI__${app}`, packageType: 'apk', versionName: '1.0.0', versionCode: 1,
|
|
minAppVersionCode: 1, mandatory: false, releaseNotes: 'offline', status: 'PUBLISHED', current: true,
|
|
sha256: 'a'.repeat(64), sizeBytes: 1024, downloadUrl: '/download', createdAt: '2026-10-01T00:00:00Z',
|
|
publishedAt: '2026-10-01T00:00:00Z',
|
|
}]
|
|
const ctx = context((method, path) => {
|
|
if (method === 'GET') return { http: 200, body: { code: '200', data: releases(path.endsWith('/domestic') ? 'domestic' : 'overseas') } }
|
|
if (method === 'POST' && path.includes('/publish')) return { http: 200, body: { code: 'A0404', message: '不存在' } }
|
|
if (method === 'POST' && path.includes('/withdraw')) return { http: 200, body: { code: 'A0404', message: '不存在' } }
|
|
return { http: 400, body: { code: 'A0400', message: '文件不能为空' } }
|
|
})
|
|
await runPdaReleases(ctx)
|
|
assert.deepEqual(ctx.calls.map((row) => row.method), ['GET', 'GET', 'POST', 'POST', 'POST'])
|
|
assert.ok(ctx.calls[0].path.endsWith('/domestic'))
|
|
assert.ok(ctx.calls[1].path.endsWith('/overseas'))
|
|
assert.ok(ctx.gaps.some((gap) => gap.name === '真实 APK/WGT 上传、发布、停发与下载' && gap.metadata.blocking === false))
|
|
})
|
|
|
|
test('平台审计模块注册为默认模块,列表不泄漏快照且详情身份一致', async () => {
|
|
assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'audit'/)
|
|
const row = { id: '7', eventId: 'e-7', eventType: 'ORDER_SUBMIT', resourceType: 'ORDER', resourceId: '1',
|
|
accountId: '0', operatorTenantId: '0', userId: '1', occurredAt: '2026-10-01T00:00:00Z', success: true }
|
|
const detailCtx = context((method, path) => {
|
|
if (path.endsWith('/999999999999999')) return { http: 200, body: { code: 'A0404', message: '不存在' } }
|
|
if (path.endsWith('/7')) return { http: 200, body: { code: '200', data: { ...row, httpMethod: 'POST' } } }
|
|
return { http: 200, body: { code: '200', data: { records: [row], page: 1, size: 20, total: 1, pages: 1 } } }
|
|
})
|
|
const withSnapshot = context((method, path) => {
|
|
if (path.includes('/audit/logs/')) return { http: 200, body: { code: 'A0404', message: '不存在' } }
|
|
return { http: 200, body: { code: '200', data: { records: [{ ...row, beforeData: '{}' }], page: 1, size: 20, total: 1, pages: 1 } } }
|
|
})
|
|
await runAudit(detailCtx)
|
|
assert.equal(detailCtx.calls[0].method, 'GET')
|
|
assert.ok(detailCtx.calls[0].path.includes('/audit/logs?page=1&size=20'))
|
|
assert.equal(detailCtx.calls[1].path, '/api/platform/v1/audit/logs/7')
|
|
assert.ok(detailCtx.calls[2].path.endsWith('/999999999999999'))
|
|
await assert.rejects(runAudit(withSnapshot), /不返回变更前后快照/)
|
|
})
|
|
|
|
test('船期日历模块注册为默认模块并核对月历、渠道、模板与异常分配', async () => {
|
|
assert.match(entry, /const CONFIGURED_MODULES = \[[^\n]*'sailing-calendar'/)
|
|
const summary = {
|
|
actualReceivedCbm: 1, catchableForecastCbm: 2, riskUnmatchedCbm: 0, bookedContainerCount: 1,
|
|
recommendedContainerCount: 1, pendingContainerCount: 0, lateBatchCount: 0, overCapacity: false,
|
|
planningCapacityCbm: 10, bookedPlanningCapacityCbm: 5, catchableReceivedCbm: 1, lateReceivedCbm: 0,
|
|
snapshotAt: null, sailingCount: 1,
|
|
}
|
|
const sailing = {
|
|
id: '11', sailingNo: 'SAIL-11', warehouseId: '1', channelCode: 'SEA', channelName: '海运', templateId: null,
|
|
channelId: null, etaAt: '2026-10-20T00:00:00', sourceType: 'MANUAL', plannedCapacityCbm: 10,
|
|
volumeSummary: summary, version: 0,
|
|
}
|
|
const ctx = context((method, path) => {
|
|
if (path.includes('/calendar')) return { http: 200, body: { code: '200', data: {
|
|
records: [sailing], summary, filterOptions: { warehouses: [], channels: [], departurePorts: [], destinationPorts: [] } } } }
|
|
if (path.includes('/sailing-channels')) return { http: 200, body: { code: '200', data: [{ id: '1', channelCode: 'SEA', defaultCutoffDays: 3, version: 0 }] } }
|
|
if (path.includes('/sailing-templates')) return { http: 200, body: { code: '200', data: { records: [{ id: '1', channelId: '1', warehouseId: '1', weekdays: [1, 2], cutoffDays: 3, version: 0 }] } } }
|
|
if (path.includes('/sailing-assignments/exceptions')) return { http: 200, body: { code: '200', data: { records: [{ assignmentId: '1', volumeCbm: 1 }], total: 1 } } }
|
|
if (path.endsWith('/11')) return { http: 200, body: { code: '200', data: {
|
|
sailing: { ...sailing, volumeSummary: summary }, histories: [] } } }
|
|
if (path.endsWith('/999999999999999')) return { http: 200, body: { code: 'A0404', message: '不存在' } }
|
|
throw new Error(`unexpected ${method} ${path}`)
|
|
})
|
|
await runSailingCalendar(ctx)
|
|
assert.deepEqual(ctx.calls.map((row) => row.method), ['GET', 'GET', 'GET', 'GET', 'GET', 'GET'])
|
|
assert.ok(ctx.calls[0].path.includes('/sailings/calendar?month='))
|
|
assert.ok(ctx.calls[1].path.endsWith('/sailing-channels'))
|
|
assert.ok(ctx.calls[2].path.includes('/sailing-templates?page=1&size=200'))
|
|
assert.ok(ctx.calls[3].path.includes('/sailing-assignments/exceptions'))
|
|
assert.ok(ctx.calls[4].path.endsWith('/11'))
|
|
assert.ok(ctx.calls[5].path.endsWith('/999999999999999'))
|
|
delete sailing.etaAt
|
|
await runSailingCalendar(ctx)
|
|
sailing.etaAt = 'invalid-eta'
|
|
await assert.rejects(runSailingCalendar(ctx), /月历记录、汇总与筛选选项结构完整/)
|
|
})
|