test(order): verify channel removal using customer-owned price books
This commit is contained in:
@@ -18,7 +18,7 @@ test('client registration distinguishes incomplete-registration replay from cred
|
||||
const helperSource = source.slice(source.indexOf('// BEGIN ORDER ACCEPTANCE HELPERS'), source.indexOf('// END ORDER ACCEPTANCE HELPERS'))
|
||||
const workflowSource = source.slice(source.indexOf('async function ownedOrderSnapshot('), source.indexOf('async function apiStep('))
|
||||
function runtime(extra = {}) {
|
||||
const context = vm.createContext({ Buffer, createHash, inflateRawSync, structuredClone, Blob, FormData,
|
||||
const context = vm.createContext({ Buffer, createHash, inflateRawSync, deflateRawSync, structuredClone, Blob, FormData,
|
||||
assertEqual: (actual, expected, label) => assert.equal(actual, expected, label),
|
||||
assertTrue: (value, label) => assert.ok(value, label),
|
||||
assertPresent: (value, label) => assert.ok(value !== null && value !== undefined && value !== '', label),
|
||||
@@ -28,6 +28,122 @@ function runtime(extra = {}) {
|
||||
}
|
||||
const plain = value => JSON.parse(JSON.stringify(value))
|
||||
|
||||
function priceBookSource(r) {
|
||||
const channels=[{channelCode:'MATSON_CLX_TRUCK',sourceSheetName:'美森正班海卡'},
|
||||
{channelCode:'HEDE_TRUCK',sourceSheetName:'合德海卡'}]
|
||||
const rates=channels.map(channel=>({channelCode:channel.channelCode,originWarehouseCode:'YIWU',originWarehouseId:'2',
|
||||
destinationType:'FBA',destinationCode:'ONT8',unitPrice:10}))
|
||||
const entries=new Map([
|
||||
['xl/workbook.xml','<workbook><sheets><sheet name="美森正班海卡" sheetId="1" r:id="rId1"/><sheet name="合德海卡" sheetId="2" r:id="rId2"/></sheets></workbook>'],
|
||||
['xl/_rels/workbook.xml.rels','<Relationships><Relationship Id="rId1" Target="worksheets/sheet1.xml"/><Relationship Id="rId2" Target="worksheets/sheet2.xml"/></Relationships>'],
|
||||
['xl/worksheets/sheet1.xml','<worksheet><sheetData/></worksheet>'],
|
||||
['xl/worksheets/sheet2.xml','<worksheet><sheetData/></worksheet>'],
|
||||
['xl/media/image1.png',Buffer.from([0x89,0x50,0x4e,0x47,0xff,0x00,0x80])],
|
||||
])
|
||||
const bytes=r.orderZipWrite(entries,true)
|
||||
const version={id:'100',customerId:'0',status:'ACTIVE',sourceSha256:createHash('sha256').update(bytes).digest('hex')}
|
||||
return {bytes,entries,reference:{version,channels,rates}}
|
||||
}
|
||||
|
||||
test('private B removes target sheet only, preserves binary source parts and uses published mapping IDs',()=>{
|
||||
const r=runtime(),source=priceBookSource(r)
|
||||
const b=r.channelGateWorkbook(source.bytes,source.reference,'MATSON_CLX_TRUCK'),entries=r.orderZipRead(b.bytes)
|
||||
assert.deepEqual(plain(b.removedCodes),['MATSON_CLX_TRUCK'])
|
||||
assert.doesNotMatch(entries.get('xl/workbook.xml'),/美森正班海卡/)
|
||||
assert.match(entries.get('xl/workbook.xml'),/合德海卡/)
|
||||
for(const [name,original] of source.entries)if(name!=='xl/workbook.xml')assert.deepEqual(entries.get(name),original)
|
||||
assert.deepEqual(plain(r.channelGateMappings({rates:[source.reference.rates[1]]},source.reference)),{
|
||||
originMappings:[{originWarehouseCode:'YIWU',warehouseId:'2'}],overseasMappings:[],
|
||||
})
|
||||
assert.throws(()=>r.channelGateWorkbook(source.bytes,{...source.reference,channels:[source.reference.channels[0]]},'MATSON_CLX_TRUCK'),/retain another/)
|
||||
assert.throws(()=>r.channelGateMappings({rates:[source.reference.rates[1]]},{rates:[{...source.reference.rates[1],originWarehouseId:null}]}),/unique published/)
|
||||
assert.throws(()=>r.channelGateMappings({rates:[source.reference.rates[1]]},{rates:[source.reference.rates[1],{...source.reference.rates[1],originWarehouseId:'9'}]}),/unique published/)
|
||||
})
|
||||
|
||||
test('shared parcel source sheet removes all its channel codes and cannot leave an empty price book',()=>{
|
||||
const r=runtime(),source=priceBookSource(r)
|
||||
source.reference.channels.push({channelCode:'SAME_SHEET_SECOND_CODE',sourceSheetName:'美森正班海卡'})
|
||||
assert.deepEqual(plain(r.channelGateWorkbook(source.bytes,source.reference,'MATSON_CLX_TRUCK').removedCodes),['MATSON_CLX_TRUCK','SAME_SHEET_SECOND_CODE'])
|
||||
})
|
||||
|
||||
function channelGateHarness(fault='') {
|
||||
const r=runtime(),source=priceBookSource(r),calls=[],books=new Map(),publicVersions=[structuredClone(source.reference.version)]
|
||||
const report={resources:{tenantId:'20',orderIds:[]},steps:[{module:'registration',name:'enterprise self-registration',passed:true,evidence:{tenantId:'20'}}],
|
||||
orderSubmissionGates:{balance:{restoration:'RESTORED_WITH_AUDIT'},remaining:['Channel-offline requires fixture','Other pending scope']}}
|
||||
const order={order:{id:'42',tenantId:'20',orderNo:'FL42',status:'DRAFT',version:7,channelCode:'MATSON_CLX_TRUCK'},
|
||||
cargoes:[{skuNo:'SKU'}],parties:[],internalNotes:[],statusHistory:[]}
|
||||
let nextId=201,nextKey=0,task=null
|
||||
if(fault==='unowned-customer')report.steps[0].evidence.tenantId='21'
|
||||
const base='/api/platform/v1/fba-pricing/carrier-rates/price-books'
|
||||
Object.assign(r,{report,marker:'channel-fixture',merchantToken:'owner',platformToken:'platform',newIdempotencyKey:()=>`key-${++nextKey}`,
|
||||
orderAcceptanceFixture:{masterData:{receiving:{id:'2'},channel:{priceBookVersionId:'100'}},payload:{channelCode:'MATSON_CLX_TRUCK',cargoes:[{skuNo:'SKU'}]}},
|
||||
apiStep:async(_module,name,request,validate)=>{
|
||||
calls.push({name,path:request.path,method:request.method,body:request.body,token:request.token,headers:request.headers})
|
||||
const url=new URL(request.path,'https://offline.invalid')
|
||||
let data=null,bytes=null,code='200',message='success'
|
||||
if(url.pathname.endsWith('/tenant/profile'))data={id:'20',status:'ACTIVE'}
|
||||
else if(url.pathname===base && request.method!=='POST')data=url.searchParams.get('customerId')==='0'?structuredClone(publicVersions):[...books.values()].map(book=>structuredClone(book.version))
|
||||
else if(request.path===`${base}/100`)data=structuredClone(source.reference)
|
||||
else if(request.path===`${base}/100/source`)bytes=source.bytes
|
||||
else if(url.pathname===base && request.method==='POST') {
|
||||
assert.equal(url.searchParams.get('customerId'),'20')
|
||||
assert.equal(request.token,'platform')
|
||||
const file=request.body.get('file'),raw=Buffer.from(await file.arrayBuffer()),entries=r.orderZipRead(raw)
|
||||
const containsTarget=entries.get('xl/workbook.xml').includes('美森正班海卡')
|
||||
const id=String(nextId++),channels=source.reference.channels.filter(channel=>containsTarget||channel.channelCode!=='MATSON_CLX_TRUCK')
|
||||
data={version:{id,customerId:fault==='wrong-version-owner'?'0':'20',status:'PENDING_CONFIRMATION',sourceSha256:createHash('sha256').update(raw).digest('hex')},
|
||||
channels:structuredClone(channels),rates:structuredClone(source.reference.rates.filter(rate=>channels.some(channel=>channel.channelCode===rate.channelCode)))}
|
||||
books.set(id,data)
|
||||
} else if(url.pathname.startsWith(`${base}/`)) {
|
||||
const id=url.pathname.slice(base.length+1).split('/')[0],book=books.get(id)
|
||||
assert.ok(book,'only this run uploaded version may be read/published')
|
||||
if(request.method==='POST') {
|
||||
assert.ok(request.path.endsWith('/publish'));assert.equal(book.version.customerId,'20')
|
||||
assert.deepEqual(plain(request.body),{originMappings:[{originWarehouseCode:'YIWU',warehouseId:'2'}],overseasMappings:[]})
|
||||
for(const previous of books.values())if(previous.version.status==='ACTIVE')previous.version.status='SUPERSEDED'
|
||||
book.version.status='ACTIVE';book.version.blockingErrorCount=0
|
||||
if(fault==='public-drift')publicVersions[0].status='SUPERSEDED'
|
||||
}
|
||||
data=structuredClone(book)
|
||||
} else if(url.pathname.endsWith('/pricing/channels')) {
|
||||
const active=[...books.values()].find(book=>book.version.status==='ACTIVE')
|
||||
let channels=active.channels
|
||||
if(fault==='channel-retained'&&active.version.id==='202')channels=source.reference.channels
|
||||
data=channels.map(channel=>({...channel,priceBookVersionId:active.version.id}))
|
||||
} else if(url.pathname==='/api/merchant/v1/orders/first-leg'&&request.method==='POST')data='42'
|
||||
else if(url.pathname==='/api/merchant/v1/orders/first-leg/42')data=structuredClone(order)
|
||||
else if(url.pathname.includes('/tasks/by-order/'))data=structuredClone(task)
|
||||
else if(url.pathname==='/api/platform/inbounds')data={page:1,total:0,records:[]}
|
||||
else if(url.pathname.endsWith('/submit')) {
|
||||
assert.equal(request.body.expectedVersion,7)
|
||||
code='A0409';message=fault==='wrong-conflict'?'订单版本已变化,请刷新后重试':'所选渠道已下线,请重新选择'
|
||||
if(fault==='order-drift')order.order.version++
|
||||
if(fault==='task-drift')task={id:'task',orderId:'42',tenantId:'20'}
|
||||
} else throw new Error(`unhandled offline request ${request.path}`)
|
||||
const response={status:200,json:{code,message,data},bytes};validate(response);return response
|
||||
},
|
||||
})
|
||||
return {r,report,calls,books}
|
||||
}
|
||||
|
||||
test('channel gate publishes private A then B, proves client removal and rejects both original-version submits',async()=>{
|
||||
const {r,report,calls,books}=channelGateHarness()
|
||||
await r.orderSubmissionChannelGateModule()
|
||||
assert.equal(report.orderSubmissionGates.channel.status,'REJECTIONS_VERIFIED')
|
||||
assert.equal(report.orderSubmissionGates.channel.publicVersionsUnchanged,true)
|
||||
assert.deepEqual(report.orderSubmissionGates.remaining,['Other pending scope'])
|
||||
assert.equal(books.get('201').version.status,'SUPERSEDED');assert.equal(books.get('202').version.status,'ACTIVE')
|
||||
assert.equal(calls.filter(call=>call.path.endsWith('/publish')).length,2)
|
||||
assert.equal(calls.filter(call=>call.path.includes('/submit')).length,2)
|
||||
assert.ok(calls.filter(call=>call.method==='POST').every(call=>!call.path.includes('customerId=0')&&!call.path.includes('/100/')))
|
||||
})
|
||||
|
||||
for(const fault of ['unowned-customer','wrong-version-owner','channel-retained','wrong-conflict','order-drift','task-drift','public-drift'])test(`channel gate fails closed: ${fault}`,async()=>{
|
||||
const {r,calls}=channelGateHarness(fault)
|
||||
await assert.rejects(r.orderSubmissionChannelGateModule())
|
||||
if(['unowned-customer','wrong-version-owner'].includes(fault))assert.equal(calls.some(call=>call.path.endsWith('/publish')),false)
|
||||
})
|
||||
|
||||
function balanceGateHarness(fault='') {
|
||||
const calls=[],ledger=[],report={resources:{tenantId:'20',orderIds:[]},steps:[{module:'registration',name:'enterprise self-registration',passed:true,evidence:{tenantId:'20'}}]}
|
||||
const wallet={currencyCode:'CNY',version:0,creditActive:false,actualBalance:0,availableBalance:0,frozenBalance:0,
|
||||
|
||||
Reference in New Issue
Block a user