Files
baiyue-firstleg/scripts/prod-release-order-stage.test.mjs
T

244 lines
13 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { mkdtemp, readFile, writeFile, rm } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fingerprint, validateOrderInput, withOrderJournal, runOrderStage, validateSubmitCheckpoint } from './prod-release-order-stage.mjs'
import { executeOrder } from './prod-release-acceptance.mjs'
function fixture() {
return { batch: 'release-fixture-001', environment: { mode: 'gray', apiBase: 'https://fixture.invalid/api' },
operator: { merchant: 'fixture-client', admin: 'fixture-admin' },
customer: { id: '1', name: 'fixture', code: 'fixture' }, origin: { id: '2', code: 'CN-YIWU-01' },
destination: { id: '3', code: 'FIXTURE', type: 'FBA' },
payload: { customerName: 'fixture', receivingWarehouseId: '2', destinationWarehouseId: '3',
shipmentId: 'SHIP-fixture', referenceId: 'REF-fixture',
amazonWarehouseCode: 'FIXTURE', transportMode: 'SEA', destinationType: 'FBA', fulfillmentType: 'STANDARD',
containerMode: 'LCL', pickupMethod: 'SELF_EXPRESS', channelCode: 'FIXTURE', pricingTaxMode: 'TAX_INCLUDED',
customerRemark: 'RELEASE-ACCEPTANCE:release-fixture-001',
cargoes: [{ skuNo: 'fixture-sku', packageCount: 2, unitsPerPackage: 1, totalQuantity: 2,
shipmentId: 'SHIP-fixture', referenceId: 'REF-fixture',
lengthCm: 40, widthCm: 30, heightCm: 30, grossWeightKg: 15, volumeCbm: 0.072,
productImageFileId: '4', woodenProduct: false, dangerousGoods: false }] } }
}
async function sandbox(t) {
const path = await mkdtemp(join(tmpdir(), 'prod-order-contract-'))
t.after(() => rm(path, { recursive: true, force: true }))
return join(path, 'journal.json')
}
function server(input, fail) {
let status = null
let failed = false
const calls = []
const state = { calls, created: 0, approved: 0, orderVersion: 5,
async transport(request) {
calls.push(request)
const { method, path, idempotencyKey } = request
if (path.endsWith('/inbound-code-policy')) return { mode: 'AUTO', manualEntryRequired: false }
if (method === 'GET') return { order: { ...input.payload, id: '5', tenantId: '1', customerId: '1',
status, version:state.orderVersion, orderNo: 'FIXTURE-ORDER', inboundCode: status === 'DRAFT' ? null : 'FIXTURE-INBOUND' }, cargoes: input.payload.cargoes }
let op
if (path.endsWith('/first-leg')) {
op = 'create'
assert.ok(idempotencyKey)
if (!status) { state.created++; status = 'DRAFT' }
} else if (path.endsWith('/submit')) {
op = 'submit'
assert.ok(idempotencyKey)
assert.deepEqual(request.body,{expectedVersion:state.orderVersion})
state.orderVersion++
status = 'SUBMITTED'
} else {
assert.match(path, /^\/platform\/v1\/orders\/first-leg\/5\/approve\?tenantId=1$/)
assert.equal(request.role, 'ADMIN')
op = 'approve'
state.approved++
if (fail !== 'approve-before') status = 'APPROVED'
}
if ((fail === op || fail === 'approve-before' && op === 'approve') && !failed) {
failed = true
throw new Error('simulated response loss')
}
return op === 'create' ? '5' : null
},
}
return state
}
async function run(path, input, api, resume = false, authorize = async () => {}) {
return withOrderJournal(path, input, (state, save) => runOrderStage({ input, state, save,
transport: api.transport, authorize, resume }))
}
test('fingerprint is stable for reordered keys', () => {
assert.equal(fingerprint({ a: 1, b: 2 }), fingerprint({ b: 2, a: 1 }))
})
test('submit-intent without original numeric version/key is blocked rather than repaired from current detail',()=>{
for(const submitCommand of [undefined,{body:{},idempotencyKey:'k'},{body:{expectedVersion:'5'},idempotencyKey:'k'},{body:{expectedVersion:5}}]) {
assert.throws(()=>validateSubmitCheckpoint({phase:'SUBMIT_INTENT',submitCommand}),/首次版本/)
}
validateSubmitCheckpoint({phase:'SUBMIT_INTENT',submitCommand:{body:{expectedVersion:0},idempotencyKey:'k'}})
})
test('uncertain submit persists request before transport and resume retains original version even after external edit',async t=>{
const path=await sandbox(t),input=fixture(),api=server(input)
const original=api.transport;let attempted=false
api.transport=async request=>{
if(request.path.endsWith('/submit')&&!attempted){
attempted=true
const disk=JSON.parse(await readFile(path,'utf8'))
assert.equal(disk.phase,'SUBMIT_INTENT');assert.deepEqual(disk.submitCommand.body,request.body)
assert.equal(disk.submitCommand.idempotencyKey,request.idempotencyKey)
throw new Error('uncertain-before-send')
}
return original(request)
}
await assert.rejects(run(path,input,api),/uncertain-before-send/)
const first=JSON.parse(await readFile(path,'utf8')).submitCommand
api.orderVersion=99
await assert.rejects(run(path,input,api,true)) // original version 5 must not be refreshed to 99
assert.deepEqual(api.calls.filter(call=>call.path.endsWith('/submit')).at(-1).body,first.body)
assert.deepEqual(JSON.parse(await readFile(path,'utf8')).submitCommand,first)
})
test('rejects multiple SKUs, excess cartons, incorrect company and volume', () => {
for (const change of [i => i.payload.cargoes.push(i.payload.cargoes[0]),
i => i.payload.cargoes[0].packageCount = 300,
i => i.payload.customerId = '999', i => i.origin.code = 'CN-OTHER',
i => i.payload.customerRemark += '-other', i => i.payload.cargoes[0].volumeCbm = 2]) {
const input = fixture(); change(input)
assert.throws(() => validateOrderInput(input))
}
})
test('one create, one submit, one approval; completed resume performs reads only', async t => {
const path = await sandbox(t), input = fixture(), api = server(input)
const result = await run(path, input, api)
assert.equal(result.status, 'ORDER_APPROVED')
assert.equal(result.fullFlowVerified, false)
const count = api.calls.filter(c => c.method === 'POST').length
await run(path, input, api, true)
assert.equal(api.calls.filter(c => c.method === 'POST').length, count)
assert.equal(api.created, 1)
assert.equal(api.approved, 1)
const disk = await readFile(path, 'utf8')
assert.doesNotMatch(disk, /fixture-client|fixture-admin|productImageFileId|https:/)
})
for (const failure of ['create', 'submit', 'approve']) {
test(`recover lost ${failure} response without duplicate business records`, async t => {
const path = await sandbox(t), input = fixture(), api = server(input, failure)
await assert.rejects(run(path, input, api), /response loss/)
await assert.rejects(run(path, input, api), /显式续跑|禁止重发/)
await run(path, input, api, true)
assert.equal(api.created, 1)
assert.equal(api.approved, 1)
if (failure === 'create') {
const creates = api.calls.filter(c => c.method === 'POST' && c.path.endsWith('/first-leg'))
assert.equal(creates.length, 2)
assert.equal(creates[0].idempotencyKey, creates[1].idempotencyKey)
}
})
}
test('uncertain approval that still reads submitted must not be resent', async t => {
const path = await sandbox(t), input = fixture(), api = server(input, 'approve-before')
await assert.rejects(run(path, input, api))
await assert.rejects(run(path, input, api, true), /禁止重发审核/)
assert.equal(api.approved, 1)
})
test('change in environment, account, payload or batch is blocked before HTTP', async t => {
const path = await sandbox(t), input = fixture(), api = server(input)
await run(path, input, api)
for (const change of [i => i.environment.apiBase = 'https://other.invalid/api', i => i.environment.mode = 'test', i => i.operator.merchant = 'other',
i => i.payload.channelCode = 'OTHER', i => { i.batch = 'release-fixture-002'; i.payload.customerRemark = `RELEASE-ACCEPTANCE:${i.batch}` }]) {
const other = fixture(); change(other)
const count = api.calls.length
await assert.rejects(run(path, other, api, true), /已变化/)
assert.equal(api.calls.length, count)
}
})
test('authorization failure prevents first write', async t => {
const path = await sandbox(t), input = fixture(), api = server(input)
await assert.rejects(run(path, input, api, false, async () => { throw new Error('wrong customer') }))
assert.equal(api.calls.length, 0)
})
test('foreign order detail blocks submission', async t => {
const path = await sandbox(t), input = fixture(), api = server(input)
const original = api.transport
api.transport = async req => {
const data = await original(req)
if (data?.order) data.order.customerId = '999'
return data
}
await assert.rejects(run(path, input, api), /归属/)
assert.equal(api.calls.filter(c => c.method === 'POST').length, 1)
})
test('concurrent journal process is rejected', async t => {
const path = await sandbox(t), input = fixture()
await withOrderJournal(path, input, async () => {
await assert.rejects(withOrderJournal(path, input, async () => {}), { code: 'EEXIST' })
})
})
test('corrupt journal is preserved and fails closed', async t => {
const path = await sandbox(t), input = fixture(), api = server(input)
await writeFile(path, '{broken')
await assert.rejects(run(path, input, api), /检查点损坏/)
assert.equal(await readFile(path, 'utf8'), '{broken')
assert.equal(api.calls.length, 0)
})
for (const quoteMismatch of [false, true]) {
test(`entry integration: customer/profile/route checks and HTTP headers (quote mismatch=${quoteMismatch})`, async t => {
const path = await sandbox(t), input = fixture(), api = server(input)
const settings = { environment: 'gray', apiBase: 'https://fixture.invalid/api', customer: input.customer,
credentials: Object.fromEntries(['ADMIN', 'MERCHANT', 'CN_PDA', 'OVERSEAS_LAX_PDA'].map(role => [role, { loginId: role, password: 'secret' }])) }
const fetcher = async (url, options) => {
assert.ok(url.startsWith('https://fixture.invalid/api/'))
const endpoint = new URL(url).pathname.replace(/^\/api/, '') + new URL(url).search
let data
if (endpoint === '/auth/login') {
const role = JSON.parse(options.body).loginId
data = { accessToken: role }
} else if (endpoint.includes('/iam/')) {
data = { id: '1', tenantCode: 'fixture', tenantName: 'fixture', status: 'ACTIVE', dimensionInitializationStatus: 'READY' }
} else if (endpoint.includes('/warehouse-admin?')) {
const domestic = endpoint.includes('warehouseType=DOMESTIC')
data = domestic
? { total: '2', records: [{ id: '2', warehouseCode: 'CN-YIWU-01', status: 'ACTIVE' }, { id: '4', warehouseCode: 'CN-DONGGUAN-01', status: 'ACTIVE' }] }
: { total: '1', records: [{ id: '6', warehouseCode: 'US-LAX-01', status: 'ACTIVE' }] }
} else if (endpoint.endsWith('/pda-loading/warehouses')) {
assert.equal(options.headers.Authorization, 'Bearer CN_PDA')
data = [{ id: '2', warehouseCode: 'CN-YIWU-01' }, { id: '4', warehouseCode: 'CN-DONGGUAN-01' }]
} else if (endpoint.endsWith('/warehouse-profile')) {
assert.equal(options.headers.Authorization, 'Bearer OVERSEAS_LAX_PDA')
data = { warehouseId: '6', warehouseCode: 'US-LAX-01' }
} else if (/\/platform\/v1\/(pallets|locations)\?/.test(endpoint)) {
data = { total: '1', records: [{ warehouseId: '2', status: 'AVAILABLE' }] }
} else if (endpoint === '/merchant/v1/warehouses/3') {
data = { warehouse: { id: '3', warehouseCode: 'FIXTURE', warehouseType: 'FBA', status: 'ACTIVE', warehouseName: 'fixture' } }
} else if (endpoint.includes('/pricing/channels?')) {
data = [{ channelCode: 'FIXTURE', priceBookVersionId: '7', pricingTaxModes: ['TAX_INCLUDED'] }]
} else if (endpoint.endsWith('/pricing/estimate')) {
assert.equal(options.headers.Authorization, 'Bearer MERCHANT')
const body = JSON.parse(options.body)
assert.equal(body.actualWeightKg, 15)
assert.equal(body.actualVolumeCbm, 0.072)
data = { channelCode: 'FIXTURE', priceBookVersionId: quoteMismatch ? '8' : '7', pricingTaxMode: 'TAX_INCLUDED', estimatedAmount: 10 }
} else {
const role = endpoint.includes('/approve?') ? 'ADMIN' : 'MERCHANT'
assert.equal(options.headers.Authorization, `Bearer ${role}`)
data = await api.transport({ role, path: endpoint, method: options.method,
body: options.body ? JSON.parse(options.body) : undefined, idempotencyKey: options.headers['Idempotency-Key'] })
}
return { ok: true, status: 200, json: async () => ({ code: '200', data }) }
}
if (quoteMismatch) {
await assert.rejects(executeOrder(settings, input, path, false, fetcher), /报价不匹配/)
assert.equal(api.created, 0)
} else {
assert.equal((await executeOrder(settings, input, path, false, fetcher)).status, 'ORDER_APPROVED')
assert.equal(api.created, 1)
assert.equal(api.approved, 1)
await executeOrder(settings, input, path, true, fetcher)
assert.equal(api.created, 1)
assert.equal(api.approved, 1)
}
})
}