142 lines
7.2 KiB
JavaScript
142 lines
7.2 KiB
JavaScript
import assert from 'node:assert/strict'
|
|
import { readFileSync } from 'node:fs'
|
|
import test from 'node:test'
|
|
|
|
import { normalizeModuleOrder } from '../admin-test-env-acceptance.mjs'
|
|
import { run } from './phase4-cross-region-events.mjs'
|
|
|
|
const source = readFileSync(new URL('./phase4-cross-region-events.mjs', import.meta.url), 'utf8')
|
|
const system = readFileSync(new URL('./system.mjs', import.meta.url), 'utf8')
|
|
|
|
test('cross-region denial probes the current domestic container detail route', () => {
|
|
assert.ok(source.includes('/api/platform/v1/containers/domestic/${encodeURIComponent(id(phase3.containerBookingId))}'))
|
|
assert.ok(!source.includes('/api/platform/v1/containers/${encodeURIComponent(id(phase3.containerBookingId))}'))
|
|
})
|
|
|
|
function response(path, data, http = 200, code = '200') {
|
|
return { method: 'GET', path, http, body: { code, data }, requestId: 'request-1', traceId: 'trace-1' }
|
|
}
|
|
|
|
function context(resources = {}) {
|
|
const steps = []
|
|
const gaps = []
|
|
const calls = []
|
|
return {
|
|
marker: 'MARKER-1', token: 'domestic-token', overseasToken: 'overseas-token',
|
|
createdResources: resources, steps, gaps, calls,
|
|
addCoverageGap(module, scenario, name, reason, category, metadata) {
|
|
gaps.push({ module, scenario, name, reason, category, ...metadata })
|
|
},
|
|
addStep(module, name, value, passed, evidence, options) {
|
|
steps.push({ module, name, passed: Boolean(passed), evidence, options })
|
|
return Boolean(passed)
|
|
},
|
|
assert(module, name, value, passed, evidence, options) {
|
|
return this.addStep(module, name, value, passed, evidence, options)
|
|
},
|
|
async requestOverseas(method, path, body, headers, token) {
|
|
calls.push({ region: 'OVERSEAS', method, path, token: token || this.overseasToken })
|
|
if (token === this.token) return response(path, null, 403, 'A0403')
|
|
if (path.includes('/last-leg/orders/9001')) {
|
|
return response(path, { order: { id: '9001', containerNo: 'MSCU1234567' } })
|
|
}
|
|
return response(path, { records: [{ id: '7001', containerNo: 'MSCU1234567' }] })
|
|
},
|
|
async request(method, path, body, token) {
|
|
calls.push({ region: 'CN', method, path, token: token || this.token })
|
|
if (token === this.overseasToken) return response(path, null, 403, 'A0403')
|
|
if (path.includes('/overview')) return response(path, { container: {
|
|
id: '3001', containerNo: 'MSCU1234567', unloadingCompletedAt: '2026-08-24T10:00:00Z',
|
|
unloadingPhotoUploadedAt: '2026-08-24T09:00:00Z', estimatedEmptyReportedAt: '2026-08-26T09:00:00Z',
|
|
actualEmptyReportedAt: null, emptyReportSourceVersion: 7,
|
|
} })
|
|
return response(path, { records: [{ id: '3001', containerNo: 'MSCU1234567' }] })
|
|
},
|
|
}
|
|
}
|
|
|
|
test('entry orders marker-owned prerequisites before cross-region acceptance', () => {
|
|
const modules = normalizeModuleOrder(['phase4-cross-region-events'])
|
|
const cross = modules.indexOf('phase4-cross-region-events')
|
|
for (const prerequisite of ['system', 'phase3-loading-finalize', 'phase4-overseas-pda']) {
|
|
assert.ok(modules.indexOf(prerequisite) >= 0 && modules.indexOf(prerequisite) < cross, prerequisite)
|
|
}
|
|
})
|
|
|
|
test('cross-region normalization keeps existing producers ahead of every phase-three consumer', () => {
|
|
const requested = [
|
|
'merchant-qualification-fixture', 'orders', 'inbound-measurement', 'phase3-container-loading',
|
|
'phase3-loading-model-bridge', 'phase3-loading', 'ocean-bookings', 'phase3-loading-finalize',
|
|
'phase3-customs', 'phase3-outbound-dispatch', 'phase3-outbound-aggregate',
|
|
'phase4-overseas-pda', 'phase4-cross-region-events',
|
|
]
|
|
const modules = normalizeModuleOrder(requested)
|
|
for (const [producer, consumer] of [
|
|
['merchant-qualification-fixture', 'orders'], ['orders', 'inbound-measurement'],
|
|
['inbound-measurement', 'phase3-container-loading'], ['phase3-container-loading', 'phase3-loading'],
|
|
['phase3-loading', 'phase3-loading-model-bridge'], ['phase3-loading', 'ocean-bookings'],
|
|
['phase3-loading-finalize', 'phase3-customs'], ['phase3-customs', 'phase3-outbound-dispatch'],
|
|
['phase3-outbound-dispatch', 'phase3-outbound-aggregate'],
|
|
['phase4-overseas-pda', 'phase4-cross-region-events'],
|
|
]) assert.ok(modules.indexOf(producer) < modules.indexOf(consumer), `${producer} -> ${consumer}`)
|
|
assert.deepEqual(normalizeModuleOrder(modules), modules)
|
|
})
|
|
|
|
test('phase-four producer variants restore the primary orchestration fixture after each matrix branch', async () => {
|
|
const source = await import('node:fs/promises').then((fs) => fs.readFile(
|
|
new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8'))
|
|
const start = source.indexOf('const createdKeys = [', source.indexOf('async runPhase4ProducerVariant'))
|
|
const end = source.indexOf(']', start)
|
|
const createdKeys = source.slice(start, end)
|
|
for (const key of [
|
|
'phase3Loading', 'phase3LoadingCompletedProducer', 'orchestrationFixtures', 'oceanBooking',
|
|
'phase3CustomsSupplier', 'phase3Customs', 'serviceCenterFlow', 'portSchedule',
|
|
'phase3OutboundDispatch', 'phase3CarrierScheduleQuery', 'phase4CarrierPickupReady',
|
|
]) assert.match(createdKeys, new RegExp(`'${key}'`))
|
|
})
|
|
|
|
test('cross-region producers do not repeat the destructive A0 snapshot mutation', async () => {
|
|
const source = await import('node:fs/promises').then((fs) => fs.readFile(
|
|
new URL('../admin-test-env-acceptance.mjs', import.meta.url), 'utf8'))
|
|
const variantStart = source.indexOf('async runPhase4ProducerVariant')
|
|
const fixtureCall = source.slice(variantStart, source.indexOf('const loading =', variantStart))
|
|
assert.match(fixtureCall, /preparePhase3LoadingFixture\('', false\)/)
|
|
})
|
|
|
|
test('contract covers dual audience, canonical 403, both event directions and diagnostics', () => {
|
|
for (const token of [
|
|
'context.token)', 'context.overseasToken)', 'cn-overseas-event-not-delivered',
|
|
'overseas-cn-event-not-delivered', 'requestId=', 'traceId=', 'pollRequests=',
|
|
'装柜完成事件重试保持建单幂等', '海外柜完成事件重试保持回写幂等',
|
|
]) assert.ok(source.includes(token), token)
|
|
assert.match(system, /缺少canonical海外订单权限必须403/)
|
|
assert.match(system, /restrictedOverseasToken/)
|
|
})
|
|
|
|
test('full observable cross-region flow performs requests and passes', async () => {
|
|
const ctx = context({
|
|
phase3Loading: { marker: 'MARKER-1', finalizeSucceeded: true, containerBookingId: '3001',
|
|
containerNo: 'MSCU1234567', loadingPlanId: '4001' },
|
|
phase4OverseasPda: { marker: 'MARKER-1', finalStatus: 'COMPLETED', customerId: '100', orderId: '9001' },
|
|
})
|
|
|
|
await run(ctx)
|
|
|
|
assert.ok(ctx.calls.length >= 8)
|
|
assert.equal(ctx.steps.every((step) => step.passed), true)
|
|
assert.equal(ctx.gaps.length, 0)
|
|
assert.ok(ctx.calls.some((call) => call.region === 'CN' && call.token === 'overseas-token'))
|
|
assert.ok(ctx.calls.some((call) => call.region === 'OVERSEAS' && call.token === 'domestic-token'))
|
|
})
|
|
|
|
test('missing marker fixture is a blocking coverage gap after real security request', async () => {
|
|
const ctx = context()
|
|
|
|
await assert.rejects(() => run(ctx), /blocking coverage gap/)
|
|
|
|
assert.ok(ctx.calls.length > 0)
|
|
assert.equal(ctx.gaps.length, 1)
|
|
assert.equal(ctx.gaps[0].deploymentBlocker, true)
|
|
assert.equal(ctx.steps.some((step) => !step.passed && step.options?.deploymentBlocker), true)
|
|
})
|