From 7f3c0690cd4cb830a6c882f1428cfdc49076438a Mon Sep 17 00:00:00 2001 From: ouyangxq <775153912@qq.com> Date: Wed, 30 Sep 2026 13:30:27 +0800 Subject: [PATCH] test(shipping): validate platform customer scope separately from business IDs --- .../phase4-carrier-nodes-contract.test.mjs | 60 +++++++++++++++++++ .../phase4-carrier-nodes.mjs | 5 +- 2 files changed, 63 insertions(+), 2 deletions(-) diff --git a/scripts/admin-test-env-modules/phase4-carrier-nodes-contract.test.mjs b/scripts/admin-test-env-modules/phase4-carrier-nodes-contract.test.mjs index 31e660c3f..5603063c8 100644 --- a/scripts/admin-test-env-modules/phase4-carrier-nodes-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-carrier-nodes-contract.test.mjs @@ -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`) 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)) +}) diff --git a/scripts/admin-test-env-modules/phase4-carrier-nodes.mjs b/scripts/admin-test-env-modules/phase4-carrier-nodes.mjs index e25a3fdee..fe9d53590 100644 --- a/scripts/admin-test-env-modules/phase4-carrier-nodes.mjs +++ b/scripts/admin-test-env-modules/phase4-carrier-nodes.mjs @@ -16,8 +16,9 @@ function id(value) { return value == null ? '' : String(value) } function number(value) { return Number.isFinite(Number(value)) } function nullableText(value) { return value == null || typeof value === 'string' } 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 validCustomerScope(value) { return typeof value === 'string' && /^(?:0|[1-9]\d*)$/.test(value) } function validOperatorId(value) { return value === '0' || validNumericId(value) } function validContainerNo(value) { 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) { - 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?.containerBookingId) && task?.trackingType === 'CONTAINER_NO' && validContainerNo(task?.trackingNo) && nullableText(task?.latestActualNode)