test(order): verify platform submit and cancel command boundaries
This commit is contained in:
@@ -2,7 +2,92 @@ import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { fixtureInboundCode, orderPersistenceFacts, requestedOrderDestinationType, freezeOrderCommand, verifyPlatformDraftEdit } from './orders.mjs'
|
||||
import { fixtureInboundCode, orderPersistenceFacts, requestedOrderDestinationType, freezeOrderCommand, verifyPlatformDraftEdit, verifyPlatformSubmitCancel } from './orders.mjs'
|
||||
|
||||
function platformLifecycleFixture(fault = '', faultAction = 'submit') {
|
||||
const drafts = new Map(), memo = new Map(), calls = []
|
||||
let nextId = 10, historyId = 0
|
||||
const context = { marker:'CURRENT-RUN', token:'platform', createdResources:{orders:{}},
|
||||
apiSuccess(_module,name,response){assert.equal(response?.body?.code,'200',name);return response.body.data},
|
||||
assert(_module,name,_response,passed){assert.ok(passed,name)},
|
||||
async request(method,path,body,token,headers={}) {
|
||||
const url = new URL(path,'https://offline.invalid')
|
||||
assert.equal(url.searchParams.get('tenantId'),'20')
|
||||
if (method !== 'GET') assert.equal(token,'platform')
|
||||
calls.push(structuredClone({method,path,body,headers}))
|
||||
const match = url.pathname.match(/\/first-leg\/(\d+)(?:\/(submit|cancel))?$/)
|
||||
if (method === 'POST' && !match) {
|
||||
const id = String(nextId++)
|
||||
drafts.set(id,{order:{id,tenantId:'20',customerId:'20',orderNo:`FL${id}`,status:'DRAFT',version:0,customerRemark:body.customerRemark},
|
||||
cargoes:structuredClone(body.cargoes),parties:[],internalNotes:[],statusHistory:[]})
|
||||
return {http:200,body:{code:'200',data:id}}
|
||||
}
|
||||
assert.ok(match,path)
|
||||
const [,id,action] = match, detail = drafts.get(id)
|
||||
if (method === 'GET') return {http:200,body:{code:'200',data:structuredClone(detail)}}
|
||||
if (method === 'PUT') {
|
||||
assert.equal(body.expectedVersion,detail.order.version);assert.ok(headers['Idempotency-Key'])
|
||||
detail.order.version++
|
||||
detail.statusHistory.push({id:`h${++historyId}`,action:'UPDATE',fromStatus:'DRAFT',toStatus:'DRAFT',operatorId:'admin',occurredAt:'2026-10-02T00:00:00'})
|
||||
return {http:200,body:{code:'200'}}
|
||||
}
|
||||
assert.ok(['submit','cancel'].includes(action))
|
||||
const activeFault = action === faultAction ? fault : ''
|
||||
const key = headers['Idempotency-Key'], cacheKey = `${id}/${action}/${key}`, fingerprint = JSON.stringify(body)
|
||||
let code = '200'
|
||||
if (!key) code = 'A0400'
|
||||
else if (body.expectedVersion === undefined) code = 'A0422'
|
||||
else if (memo.has(cacheKey)) {
|
||||
code = memo.get(cacheKey) === fingerprint ? '200' : 'A0409'
|
||||
if (activeFault === 'replay-write' && code === '200') detail.order.version++
|
||||
} else if (body.expectedVersion !== detail.order.version) code = activeFault === 'stale-masked-by-validation' ? 'A0422' : 'A0409'
|
||||
else if (detail.order.status !== 'DRAFT') code = 'A0409'
|
||||
else {
|
||||
memo.set(cacheKey,fingerprint)
|
||||
detail.order.version++;detail.order.status = action === 'submit' ? 'SUBMITTED' : 'CANCELLED'
|
||||
if (activeFault !== 'missing-success-history') detail.statusHistory.push({id:`h${++historyId}`,action:detail.order.status,
|
||||
fromStatus:'DRAFT',toStatus:detail.order.status,reason:body.reason,operatorId:'admin',occurredAt:'2026-10-02T00:00:00'})
|
||||
}
|
||||
if (code !== '200' && activeFault === 'rejection-drift') detail.cargoes[0].packageCount++
|
||||
return {http:200,body:{code}}
|
||||
},
|
||||
}
|
||||
return {context,calls,drafts}
|
||||
}
|
||||
|
||||
test('Platform submit/cancel use separate fresh drafts and freeze first valid requests through replay',async()=>{
|
||||
const {context,calls,drafts} = platformLifecycleFixture()
|
||||
const proofs = await verifyPlatformSubmitCancel(context,'20',{customerRemark:'main',cargoes:[{skuNo:'sku',packageCount:2}]},
|
||||
{mode:'AUTO',manualEntryRequired:false})
|
||||
assert.equal(drafts.size,2)
|
||||
assert.deepEqual(proofs.map(proof=>proof.finalStatus),['SUBMITTED','CANCELLED'])
|
||||
assert.notEqual(proofs[0].orderId,proofs[1].orderId)
|
||||
for (const proof of proofs) {
|
||||
const commands = proof.commands
|
||||
assert.equal(commands.length,7)
|
||||
assert.equal(commands[0].body.expectedVersion,0) // valid old page; not an invalid negative version
|
||||
assert.equal(commands[1].body.expectedVersion,undefined)
|
||||
assert.equal(commands[2].headers['Idempotency-Key'],undefined)
|
||||
assert.equal(commands[2].body.expectedVersion,1)
|
||||
assert.equal(commands[3].label,'success');assert.equal(commands[4].label,'replay')
|
||||
assert.deepEqual(commands[4].body,commands[3].body)
|
||||
assert.deepEqual(commands[4].headers,commands[3].headers)
|
||||
assert.notDeepEqual(commands[5].body,commands[3].body)
|
||||
assert.deepEqual(commands[5].headers,commands[3].headers)
|
||||
assert.notEqual(commands[6].headers['Idempotency-Key'],commands[3].headers['Idempotency-Key'])
|
||||
assert.equal(commands[6].body.expectedVersion,2)
|
||||
assert.equal(proof.checks.length,5)
|
||||
}
|
||||
assert.ok(calls.every(call=>!call.path.includes('/approve')&&!call.path.includes('/start')))
|
||||
})
|
||||
|
||||
for (const action of ['submit','cancel']) for (const fault of ['replay-write','rejection-drift','stale-masked-by-validation','missing-success-history']) {
|
||||
test(`Platform ${action} evidence rejects ${fault}`,async()=>{
|
||||
const {context} = platformLifecycleFixture(fault, action)
|
||||
await assert.rejects(verifyPlatformSubmitCancel(context,'20',{cargoes:[{packageCount:2}]},
|
||||
{mode:'AUTO',manualEntryRequired:false}),/零漂移|首次旧页面版本拒绝|唯一动作历史/)
|
||||
})
|
||||
}
|
||||
|
||||
test('order command freezes original detail version/key and never accepts an absent version',()=>{
|
||||
const detail={order:{version:3}},draft={customerRemark:'original'}
|
||||
|
||||
Reference in New Issue
Block a user