test(shipping): validate platform customer scope separately from business IDs

This commit is contained in:
2026-09-30 13:30:27 +08:00
parent 74f0d823da
commit 7f3c0690cd
2 changed files with 63 additions and 2 deletions
@@ -251,3 +251,63 @@ test('marker-owned pre-PDA chain covers idempotency conflicts through pickup-rea
const firstPageQuery = fixture.context.calls.find((item) => item.path === `${BASE}?page=1&size=20`) const firstPageQuery = fixture.context.calls.find((item) => item.path === `${BASE}?page=1&size=20`)
assert.ok(firstPageQuery) assert.ok(firstPageQuery)
}) })
async function taskPageStructure(overrides) {
const fixture = markerOwnedContext()
const task = { ...fixture.task, ...overrides }
const checks = []
const originalRequest = fixture.context.request
fixture.context.createdResources = {}
fixture.context.request = async (method, requestPath, ...args) => {
if (method === 'GET' && requestPath === `${BASE}?page=1&size=20`) {
fixture.context.calls.push({ method, path: requestPath })
return ok({ records: [{ query: task, expired: false }], page: 1, size: 20, total: 1, pages: 1 })
}
return originalRequest(method, requestPath, ...args)
}
fixture.context.assert = (_module, name, _response, condition) => checks.push({ name, passed: Boolean(condition) })
fixture.context.addStep = (_module, name, _response, passed) => checks.push({ name, passed })
await runUntilPickupReady(fixture.context)
assert.ok(fixture.context.calls.every((call) => call.method === 'GET'))
assert.ok(checks.some((check) => check.name.includes('缺少本轮phase3出仓CarrierScheduleQuery') && !check.passed))
return checks.find((check) => check.name === '船节点分页与长ID结构稳定')?.passed
}
test('task page permits explicit platform scope zero and positive customer scopes as serialized longs', async () => {
for (const customerId of ['0', '7', CUSTOMER_ID]) {
assert.equal(await taskPageStructure({ customerId }), true, `customerId=${customerId}`)
}
})
test('task page still rejects negative, missing, numeric and noncanonical customer scopes', async () => {
for (const customerId of ['-1', '-9007199254740993002', '', '00', '01', '+0', '0.0', 'customer', null, undefined, 0, 7]) {
assert.equal(await taskPageStructure({ customerId }), false, `customerId=${String(customerId)}`)
}
})
test('allowing platform scope zero never relaxes positive long task, booking or container identifiers', async () => {
for (const field of ['id', 'oceanBookingId', 'containerBookingId']) {
for (const invalid of ['0', '0000000000000000', '09007199254740993', '-9007199254740993001', '7',
'9007199254740993.001', null, 9007199254740993]) {
assert.equal(await taskPageStructure({ customerId: '0', [field]: invalid }), false,
`${field}=${String(invalid)}`)
}
}
})
test('platform scope zero completes the same detail and manual-node lifecycle assertions without changing business identifiers', async () => {
const fixture = markerOwnedContext()
fixture.task.customerId = '0'
fixture.context.createdResources.phase3CarrierScheduleQuery.customerId = '0'
await runUntilPickupReady(fixture.context)
assert.equal(fixture.task.customerId, '0')
assert.equal(fixture.task.id, QUERY_ID)
assert.equal(fixture.task.oceanBookingId, OCEAN_ID)
assert.equal(fixture.task.containerBookingId, CONTAINER_ID)
assert.equal(fixture.task.latestActualNode, 'PICKUP_READY')
assert.equal(fixture.context.calls.filter((call) => call.method === 'POST').length, 15)
assert.ok(fixture.assertions.every((check) => check.passed))
assert.ok(fixture.steps.every((check) => check.passed))
})
@@ -16,8 +16,9 @@ function id(value) { return value == null ? '' : String(value) }
function number(value) { return Number.isFinite(Number(value)) } function number(value) { return Number.isFinite(Number(value)) }
function nullableText(value) { return value == null || typeof value === 'string' } function nullableText(value) { return value == null || typeof value === 'string' }
function nullableIso(value) { return value == null || (typeof value === 'string' && !Number.isNaN(Date.parse(value))) } function nullableIso(value) { return value == null || (typeof value === 'string' && !Number.isNaN(Date.parse(value))) }
function validLongId(value) { return typeof value === 'string' && /^\d{16,}$/.test(value) } function validLongId(value) { return typeof value === 'string' && /^[1-9]\d{15,}$/.test(value) }
function validNumericId(value) { return typeof value === 'string' && /^\d+$/.test(value) && value !== '0' } function validNumericId(value) { return typeof value === 'string' && /^\d+$/.test(value) && value !== '0' }
function validCustomerScope(value) { return typeof value === 'string' && /^(?:0|[1-9]\d*)$/.test(value) }
function validOperatorId(value) { return value === '0' || validNumericId(value) } function validOperatorId(value) { return value === '0' || validNumericId(value) }
function validContainerNo(value) { function validContainerNo(value) {
if (typeof value !== 'string' || !ISO_CONTAINER_PATTERN.test(value)) return false if (typeof value !== 'string' || !ISO_CONTAINER_PATTERN.test(value)) return false
@@ -69,7 +70,7 @@ export function manualNodePayload({ expectedVersion, standardNode, observedAt, e
} }
function validTask(task) { function validTask(task) {
return validLongId(task?.id) && Boolean(task?.queryNo) && validNumericId(task?.customerId) return validLongId(task?.id) && Boolean(task?.queryNo) && validCustomerScope(task?.customerId)
&& validLongId(task?.oceanBookingId) && QUERY_STATUSES.includes(task?.status) && validLongId(task?.oceanBookingId) && QUERY_STATUSES.includes(task?.status)
&& validLongId(task?.containerBookingId) && task?.trackingType === 'CONTAINER_NO' && validLongId(task?.containerBookingId) && task?.trackingType === 'CONTAINER_NO'
&& validContainerNo(task?.trackingNo) && nullableText(task?.latestActualNode) && validContainerNo(task?.trackingNo) && nullableText(task?.latestActualNode)