test(order): verify warehouse faults and await recovery projection
This commit is contained in:
@@ -3,16 +3,47 @@ import assert from 'node:assert/strict'
|
||||
import { spawnSync } from 'node:child_process'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { buildOrderDependencyFaultPlan } from './admin-test-env-modules/orders.mjs'
|
||||
import { scopeFor, renderOrderFaultAction, createOrderFaultRuntime, runOrderFaultWindow } from './order-submit-dependency-fault-executor.mjs'
|
||||
import { scopeFor, renderOrderFaultAction, createOrderFaultRuntime, runOrderFaultWindow, waitForRecoveredSubmission } from './order-submit-dependency-fault-executor.mjs'
|
||||
|
||||
const image = 'sha256:' + 'a'.repeat(64)
|
||||
function fixture() {
|
||||
const detail = id => ({order:{id,tenantId:'20',orderNo:`FL${id}`,version:2,status:'DRAFT'},cargoes:[],parties:[],internalNotes:[],statusHistory:[]})
|
||||
const detail = id => ({order:{id,tenantId:'20',receivingWarehouseId:'30',orderNo:`FL${id}`,version:2,status:'DRAFT'},cargoes:[],parties:[],internalNotes:[],statusHistory:[]})
|
||||
const raw={marker:'TEST-DEPENDENCY',customerId:'20',registeredCustomerId:'20',merchant:detail('101'),platform:detail('102')}
|
||||
return {raw,plan:buildOrderDependencyFaultPlan(raw)}
|
||||
}
|
||||
const env={CHONGCHONG_ENV:'test',JENKINS_URL:'https://offline.invalid/',JENKINS_USER:'offline',JENKINS_API_TOKEN:'never-print-this',FBA_CORE_JENKINS_AGENT:'test-agent'}
|
||||
|
||||
function recoverySnapshot(projected=true) {
|
||||
const {raw,plan}=fixture(),detail=structuredClone(raw.merchant)
|
||||
detail.order.status='SUBMITTED';detail.order.version=3
|
||||
detail.statusHistory=[{action:'CREATE'},{action:'SUBMITTED'}]
|
||||
return {plan,snapshot:{detail,task:null,inbounds:projected?[{id:'1001',orderId:'101',orderNo:'FL101',tenantId:'20',orderStatusSnapshot:'SUBMITTED'}]:[],inboundPageComplete:true}}
|
||||
}
|
||||
test('recovery waits for the actual inbound projection even when two empty reads are stable',async()=>{
|
||||
const {plan,snapshot}=recoverySnapshot(),empty=recoverySnapshot(false).snapshot
|
||||
const values=[empty,empty,snapshot,snapshot];let reads=0
|
||||
const value=await waitForRecoveredSubmission({plan,role:'merchant',snapshot:async()=>values[Math.min(reads++,values.length-1)],wait:async()=>{}})
|
||||
assert.equal(reads,4);assert.deepEqual(value,snapshot)
|
||||
})
|
||||
test('missing submission projection cannot pass recovery after the bounded wait',async()=>{
|
||||
const {plan,snapshot}=recoverySnapshot(false);let reads=0
|
||||
await assert.rejects(waitForRecoveredSubmission({plan,role:'merchant',snapshot:async()=>{reads++;return snapshot},wait:async()=>{}}),/RECOVERY_PROJECTION_TIMEOUT/)
|
||||
assert.equal(reads,61)
|
||||
})
|
||||
test('recovery rejects duplicate or foreign projected inbounds instead of waiting them away',async()=>{
|
||||
for(const change of [s=>s.inbounds.push({...s.inbounds[0],id:'1002'}),s=>s.inbounds[0].tenantId='21',s=>s.inbounds[0].orderId='102']) {
|
||||
const {plan,snapshot}=recoverySnapshot();change(snapshot)
|
||||
await assert.rejects(waitForRecoveredSubmission({plan,role:'merchant',snapshot:async()=>snapshot,wait:async()=>{}}),/RECOVERY_SUBMISSION_FACTS_INVALID/)
|
||||
}
|
||||
})
|
||||
test('recovery projection wait rejects order fact drift and incomplete pagination',async()=>{
|
||||
const {plan,snapshot}=recoverySnapshot(),changed=structuredClone(snapshot);changed.detail.order.remark='late mutation'
|
||||
let reads=0
|
||||
await assert.rejects(waitForRecoveredSubmission({plan,role:'merchant',snapshot:async()=>reads++?changed:snapshot,wait:async()=>{}}),/RECOVERY_ORDER_DRIFT/)
|
||||
const incomplete=structuredClone(snapshot);incomplete.inboundPageComplete=false
|
||||
await assert.rejects(waitForRecoveredSubmission({plan,role:'merchant',snapshot:async()=>incomplete,wait:async()=>{}}),/RECOVERY_SUBMISSION_FACTS_INVALID/)
|
||||
})
|
||||
|
||||
test('runtime classifies dependency HTTP stages without exposing tokens, URLs or response bodies',()=>{
|
||||
const script=String.raw`
|
||||
import importlib.util,sys,urllib.error
|
||||
@@ -42,6 +73,23 @@ test('renderer binds TEST scope and emits no Jenkins credential; ambiguous remot
|
||||
await assert.rejects(runtime('activate'),/OUTCOME_UNKNOWN_USE_STATUS/)
|
||||
assert.equal(calls,1)
|
||||
})
|
||||
test('renderer chunks every embedded Groovy literal and preserves the complete runtime payload',()=>{
|
||||
const {plan}=fixture(),scope=scopeFor(plan)
|
||||
const routing={configurationReviewed:true,nacos:{baseUrl:'http://nacos.invalid:8848',loginPath:'/nacos/v3/auth/user/login',configPath:'/nacos/v3/admin/cs/config'},upstreams:{IAM:'http://iam.invalid:9201',FINANCE:'http://finance.invalid:9203',WAREHOUSE:'http://warehouse.invalid:9205'}}
|
||||
const rendered=renderOrderFaultAction('preflight',scope,'test-agent',{routing})
|
||||
const arrays=text=>Array.from(text.matchAll(/\[('[A-Za-z0-9+/=]+'(?:,'[A-Za-z0-9+/=]+')*)\]\.join\(''\)/g),match=>Array.from(match[1].matchAll(/'([^']+)'/g),part=>part[1]))
|
||||
const outer=arrays(rendered)
|
||||
assert.equal(outer.length,1)
|
||||
const inner=Buffer.from(outer[0].join(''),'base64').toString('utf8')
|
||||
assert.ok(Array.from(inner.matchAll(/'([^']*)'/g),match=>match[1].length).every(length=>length<=4000))
|
||||
const embedded=arrays(inner)
|
||||
assert.equal(embedded.length,2)
|
||||
const config=JSON.parse(Buffer.from(embedded[1].join(''),'base64').toString('utf8'))
|
||||
assert.deepEqual(config.scope,scope)
|
||||
assert.deepEqual(config.routing,routing)
|
||||
assert.equal(config.action,'preflight')
|
||||
assert.ok(Buffer.from(config.commonSource,'base64').length>0)
|
||||
})
|
||||
|
||||
function windowFixture(fault='') {
|
||||
const {raw,plan}=fixture(),calls=[],records=[]
|
||||
@@ -55,7 +103,7 @@ function windowFixture(fault='') {
|
||||
if(action==='health')return {healthy:true}
|
||||
if(action==='mode'){mode=extra.mode;return {mode}}
|
||||
if(action==='records')return {records}
|
||||
if(action==='verify-restored')return {originalImageId:image,currentImageId:image,originalFingerprint:'same',currentFingerprint:'same',upstreamReads:{IAM:true,FINANCE:true},nacosSourcesUnchanged:true}
|
||||
if(action==='verify-restored')return {originalImageId:image,currentImageId:image,originalFingerprint:'same',currentFingerprint:'same',upstreamReads:{IAM:true,FINANCE:true,WAREHOUSE:true},nacosSourcesUnchanged:true}
|
||||
if(action==='finish')return {restored:true,healthy:true,proxyStopped:true,orderRoutesVerified:true}
|
||||
return {}
|
||||
},
|
||||
@@ -72,9 +120,9 @@ function windowFixture(fault='') {
|
||||
return {options,calls}
|
||||
}
|
||||
|
||||
test('four observations restore original routes and verify business recovery before stopping proxy',async()=>{
|
||||
test('six observations restore original routes and verify business recovery before stopping proxy',async()=>{
|
||||
const {options,calls}=windowFixture(),report=await runOrderFaultWindow(options)
|
||||
assert.equal(report.success,true);assert.equal(report.observations.length,4)
|
||||
assert.equal(report.success,true);assert.equal(report.observations.length,6)
|
||||
assert.ok(calls.indexOf('restore')<calls.indexOf('verify-restored'))
|
||||
assert.ok(calls.indexOf('verify-restored')<calls.indexOf('business-recovery'))
|
||||
assert.ok(calls.indexOf('business-recovery')<calls.indexOf('finish'))
|
||||
@@ -84,6 +132,28 @@ test('failed business assertion still restores, but never reports fault verifica
|
||||
assert.equal(report.success,false);assert.equal(report.restoration.restored,true)
|
||||
assert.ok(calls.includes('finish'))
|
||||
})
|
||||
test('a redirected public warehouse plan stops before any runtime mutation',async()=>{
|
||||
const {options,calls}=windowFixture()
|
||||
options.plan.receivingWarehouseId='31'
|
||||
options.plan.phases[2].targetPath='/internal/warehouses/0/31/active'
|
||||
const report=await runOrderFaultWindow(options)
|
||||
assert.equal(report.success,false)
|
||||
assert.ok(!calls.includes('prepare'))
|
||||
assert.ok(!calls.includes('activate'))
|
||||
})
|
||||
test('unverified warehouse restoration blocks business recovery and proxy finish',async()=>{
|
||||
const {options,calls}=windowFixture(),original=options.action
|
||||
options.action=async(action,extra)=>{
|
||||
const result=await original(action,extra)
|
||||
if(action==='verify-restored')result.upstreamReads.WAREHOUSE=false
|
||||
return result
|
||||
}
|
||||
const report=await runOrderFaultWindow(options)
|
||||
assert.equal(report.success,false)
|
||||
assert.ok(calls.includes('restore'))
|
||||
assert.ok(!calls.includes('business-recovery'))
|
||||
assert.ok(!calls.includes('finish'))
|
||||
})
|
||||
for(const fault of ['prepare-unknown','restore-unknown','business-recovery-failed'])test(`unsafe cleanup blocks finish: ${fault}`,async()=>{
|
||||
const {options,calls}=windowFixture(fault),report=await runOrderFaultWindow(options)
|
||||
assert.equal(report.success,false);assert.equal(calls.filter(action=>action==='prepare').length,1)
|
||||
@@ -131,15 +201,37 @@ class Fake:
|
||||
class RuntimeTests(unittest.TestCase):
|
||||
def fixture(self):
|
||||
f=Fake();tools=dict(common);tools.update(inspect=f.inspect,docker=f.docker)
|
||||
scope={'marker':'OFFLINE','customerId':'7','orderIds':{'merchant':'10','platform':'11'},'planSha256':'b'*64}
|
||||
scope={'marker':'OFFLINE','customerId':'7','receivingWarehouseId':'30','orderIds':{'merchant':'10','platform':'11'},'planSha256':'b'*64}
|
||||
r=m.Runtime(scope,tools,Path('offline-not-created'));saved=[];controls=[]
|
||||
r.save=lambda state:saved.append(copy.deepcopy(state));r.control=lambda state,mode=None,ttl=0:(controls.append(mode) or {'mode':'PASS'})
|
||||
r.probes=lambda state:{'IAM':True,'FINANCE':True}
|
||||
r.probes=lambda state:{'IAM':True,'FINANCE':True,'WAREHOUSE':True}
|
||||
r.config_fingerprint=lambda container,routing:'reviewed-config'
|
||||
state={'scope':scope,'phase':'PROXY_STARTED','originalId':f.original['Id'],'originalImageId':f.original['Image'],
|
||||
'originalFingerprint':common['fingerprint'](f.original),'originalBody':common['recreation'](f.original,common['env_of'](f.original),full=True),
|
||||
'proxyPort':12345,'controlPort':12346,'mutation':None,'routing':{},'routingSourceSha256':'reviewed-config'}
|
||||
return f,r,state,saved,controls
|
||||
def test_warehouse_probe_requires_the_exact_active_public_domestic_warehouse(self):
|
||||
import io
|
||||
from unittest.mock import patch
|
||||
f,r,s,saved,controls=self.fixture()
|
||||
state={'internalToken':'never-public','upstreams':{'IAM':'http://iam.invalid','FINANCE':'http://finance.invalid','WAREHOUSE':'http://warehouse.invalid'}}
|
||||
warehouse={'id':30,'tenantId':0,'status':'ACTIVE','warehouseType':'DOMESTIC','warehouseName':'Scoped receiving warehouse'}
|
||||
class Response(io.BytesIO):status=200
|
||||
def response(request,timeout):
|
||||
url=request.full_url
|
||||
if '/internal/iam/' in url:data={'eligible':True}
|
||||
elif '/internal/finance/' in url:data={'actualBalance':0}
|
||||
else:
|
||||
self.assertTrue(url.endswith('/internal/warehouses/0/30/active'))
|
||||
data={'warehouse':warehouse}
|
||||
return Response(json.dumps({'code':'200','data':data}).encode())
|
||||
with patch.object(m,'http_open',side_effect=response):
|
||||
self.assertEqual(m.Runtime.probes(r,state),{'IAM':True,'FINANCE':True,'WAREHOUSE':True})
|
||||
for field,value in [('id',31),('tenantId',20),('status','DISABLED'),('warehouseType','FBA'),('warehouseName','')]:
|
||||
original=warehouse[field];warehouse[field]=value
|
||||
with self.assertRaisesRegex(RuntimeError,'RECEIVING_WAREHOUSE_NOT_READY'):m.Runtime.probes(r,state)
|
||||
warehouse[field]=original
|
||||
self.assertEqual(f.calls,[])
|
||||
def test_original_fingerprint_config_and_image_restored(self):
|
||||
f,r,s,saved,controls=self.fixture();r.activate(s)
|
||||
self.assertTrue(r.matches_injected(f.current,s));self.assertEqual(s['phase'],'INJECTED')
|
||||
@@ -227,5 +319,5 @@ sys.exit(0 if result.wasSuccessful() else 1)
|
||||
fileURLToPath(new URL('./order-submit-dependency-fault-runtime.py',import.meta.url)),
|
||||
fileURLToPath(new URL('./test-event-fault-runtime.py',import.meta.url))],{encoding:'utf8',env:{...process.env,PYTHONDONTWRITEBYTECODE:'1'}})
|
||||
assert.equal(result.status,0,result.stderr || result.error?.message)
|
||||
assert.match(result.stderr,/Ran 10 tests/)
|
||||
assert.match(result.stderr,/Ran 11 tests/)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user