211 lines
19 KiB
JavaScript
211 lines
19 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {parseArguments,validateEnvironment,runAcceptance,businessRejected,offRejected,evidence,pin,fixtureDate,validateSyntheticNotes,syntheticCases} from './selfoperated-test-env-acceptance.mjs';
|
|
|
|
const env={CHONGCHONG_ENV:'TEST',CHONGCHONG_ADMIN_API_BASE_URL:'https://test.example.invalid/api',
|
|
CHONGCHONG_ADMIN_USERNAME:'admin-secret',CHONGCHONG_ADMIN_PASSWORD:'password-secret',
|
|
CHONGCHONG_MERCHANT_USERNAME:'merchant-secret',CHONGCHONG_MERCHANT_PASSWORD:'merchant-password-secret',
|
|
SELFOP_READONLY_USERNAME:'readonly-secret',SELFOP_READONLY_PASSWORD:'readonly-password-secret',
|
|
SELFOP_DENIED_USERNAME:'denied-secret',SELFOP_DENIED_PASSWORD:'denied-password-secret'};
|
|
const options={execute:true,environment:'TEST',probeOff:true,account:'fixture-account',warehouse:'W1',otherWarehouse:'W2',report:'unused.json'};
|
|
const READ='platform:self-operated-analytics:list',SYNC='platform:self-operated-analytics:sync';
|
|
function fixture({mode='OFF',offCode='B0001',offMessage='采集功能未启用,当前环境不允许执行采集或重试',acceptCommand=false,changeMode=false}={}){
|
|
const requests=[];
|
|
const context={salesRunId:'11',inventoryRunId:'22',salesScopeHash:'sales-hash',inventoryScopeHash:'stock-hash',queryAsOf:new Date().toISOString()};
|
|
const sales=[{sourceAccount:options.account,warehouseCode:'W1',customerCode:'owner-secret-A',sku:'sku-secret',observedQuantity:4},
|
|
{sourceAccount:options.account,warehouseCode:'W1',customerCode:'owner-secret-B',sku:'sku-secret',observedQuantity:8}];
|
|
const response=(code='200',data=null,http=200,message='do-not-report-sensitive-message')=>({status:http,headers:new Headers({'x-request-id':'request-1','x-trace-id':'trace-1'}),
|
|
json:async()=>({code,data,requestId:'request-1',traceId:'trace-1',message})});
|
|
const fetchImpl=async(url,init)=>{
|
|
const u=new URL(url),path=u.pathname.substring(4),params=u.searchParams,body=init.body?JSON.parse(init.body):null;
|
|
requests.push({url,method:init.method,body,headers:init.headers});
|
|
if(path==='/auth/login'){
|
|
const label=body.loginId.split('-')[0];const authorities=label==='admin'?[READ,SYNC]:label==='readonly'?[READ]:[];
|
|
return response('200',{accessToken:'token-secret-'+label,refreshToken:'refresh-secret-'+label,authorities});
|
|
}
|
|
if(path==='/auth/logout')return response();
|
|
const bearer=init.headers.Authorization??'';
|
|
if(!bearer)return response('A0401',null,401);
|
|
if(bearer.endsWith('denied')||bearer.endsWith('merchant'))return response('A0403');
|
|
if(init.method==='POST'){
|
|
if(bearer.endsWith('readonly'))return response('A0403');
|
|
return response(acceptCommand?'200':offCode,acceptCommand?{taskNo:'unexpected'}:null,200,offMessage);
|
|
}
|
|
const route=path.split('/').at(-1),warehouse=params.get('warehouseCode');
|
|
if(params.get('sourceAccount')?.startsWith('SELFOP-UNCONFIGURED')||warehouse?.startsWith('SELFOP-UNCONFIGURED'))return response('B0001');
|
|
if(route==='sync-overview'){
|
|
const count=requests.filter(r=>new URL(r.url).pathname.endsWith('/sync-overview')).length;
|
|
return response('200',{collectionMode:changeMode&&count>6?'BUSINESS':mode,warehouses:[{warehouseCode:warehouse,executionEnabled:false,publicationEnabled:false}]});
|
|
}
|
|
if(params.has('queryAsOf')&&(params.get('queryAsOf')!==context.queryAsOf||warehouse==='W2'))return response('B0001');
|
|
if(route==='sku-sales-summary')return response('200',{context,records:params.get('current')==='2'?[]:sales.filter(r=>
|
|
!params.has('customerCode')||r.customerCode===params.get('customerCode')),total:2});
|
|
if(route==='sku-daily-sales')return response('200',{context,summary:{customerCode:params.get('customerCode'),sku:params.get('sku')},points:[
|
|
{businessDate:'2026-10-10',dataStatus:'COMPLETE',outboundQuantity:4,observedQuantity:4},
|
|
{businessDate:'2026-10-11',dataStatus:'UNKNOWN',outboundQuantity:null,observedQuantity:null},
|
|
{businessDate:'2026-10-12',dataStatus:'PARTIAL',outboundQuantity:null,observedQuantity:1}]});
|
|
if(route==='owner-overview')return params.has('current')||params.has('size')?response('B0001'):response('200',{context,customerCode:params.get('customerCode')});
|
|
if(path.includes('/sync-tasks/'))return warehouse==='W1'?response('200',{task:{taskNo:'TASK-FIXTURE',warehouseCode:'W1'}}):response('B0001');
|
|
if(route==='sync-tasks')return response('200',{records:[{taskNo:'TASK-FIXTURE',warehouseCode:warehouse,taskRole:'WAREHOUSE_SYNC'}],total:1});
|
|
if(route==='sync-runs')return response('200',{records:[{taskNo:'TASK-FIXTURE'}],total:1});
|
|
return response('200',{context,records:[],total:0});
|
|
};
|
|
return {requests,fetchImpl};
|
|
}
|
|
|
|
test('explicit TEST execution and independent warehouse are required',()=>{
|
|
assert.throws(()=>parseArguments(['--environment','prod']),/TEST/);
|
|
assert.throws(()=>parseArguments(['--environment','TEST','--execute']),/SCOPE/);
|
|
assert.throws(()=>parseArguments(['--environment','TEST','--warehouse','W1','--other-warehouse','W1']),/DIFFER/);
|
|
assert.throws(()=>parseArguments(['--environment','TEST','--execute','--execute']),/DUPLICATE/);
|
|
assert.equal(parseArguments(['--environment','TEST']).execute,false);
|
|
assert.throws(()=>validateEnvironment({...env,CHONGCHONG_ENV:'DEV'}),/TEST/);
|
|
assert.throws(()=>validateEnvironment({...env,CHONGCHONG_ADMIN_API_BASE_URL:'https://user:secret@test.example.invalid/api'}),/GATEWAY/);
|
|
assert.throws(()=>validateEnvironment({...env,CHONGCHONG_ADMIN_API_BASE_URL:'https://test.example.invalid/api?token=secret'}),/GATEWAY/);
|
|
});
|
|
test('plan makes no network requests and never reads env files',async()=>{
|
|
const p=await runAcceptance({execute:false},{env:{},fetchImpl:()=>{throw Error('network');}});
|
|
assert.equal(p.mode,'PLAN');assert.equal(p.fixtureWrites,false);assert.equal(p.thirdPartyCalls,false);
|
|
});
|
|
test('real protocol coverage passes without printing or saving sensitive payloads',async()=>{
|
|
const f=fixture(),snapshots=[];const result=await runAcceptance(options,{env,fetchImpl:f.fetchImpl,save:r=>snapshots.push(JSON.stringify(r))});
|
|
assert.equal(result.outcome,'PASS',JSON.stringify(result));
|
|
for(const id of ['scope.same-sku-other-owner','context.cross-warehouse','sync.off-controlled-rejection','sync.off-no-task-created'])
|
|
assert.equal(result.cases.find(c=>c.id===id)?.status,'PASS',id);
|
|
const serialized=snapshots.join('');
|
|
for(const forbidden of ['password-secret','token-secret','refresh-secret','owner-secret','sku-secret','test.example.invalid','sensitive-message'])assert.ok(!serialized.includes(forbidden),forbidden);
|
|
const commands=f.requests.filter(r=>r.method==='POST'&&!new URL(r.url).pathname.startsWith('/api/auth/'));
|
|
assert.equal(commands.length,5);assert.ok(commands.every(r=>new URL(r.url).pathname.endsWith('/sync-tasks')&&r.body.action==='INVENTORY'));
|
|
assert.equal(f.requests.filter(r=>new URL(r.url).pathname.endsWith('/auth/logout')).length,4);
|
|
});
|
|
test('OFF preflight blocks every command when mode is not OFF',async()=>{
|
|
const f=fixture({mode:'VALIDATION'});const result=await runAcceptance(options,{env,fetchImpl:f.fetchImpl});
|
|
assert.equal(result.outcome,'FAIL');assert.ok(!f.requests.some(r=>r.method==='POST'&&new URL(r.url).pathname.endsWith('/sync-tasks')));
|
|
});
|
|
test('without opt-in no sync POST occurs; missing permission identity stays blocked',async()=>{
|
|
const f=fixture();const result=await runAcceptance({...options,probeOff:false},{env:{...env,SELFOP_READONLY_PASSWORD:''},fetchImpl:f.fetchImpl});
|
|
assert.equal(result.outcome,'BLOCKED');assert.ok(!f.requests.some(r=>r.method==='POST'&&new URL(r.url).pathname.endsWith('/sync-tasks')));
|
|
});
|
|
test('a changed OFF preflight stops before all sync POST requests',async()=>{
|
|
const f=fixture({changeMode:true});const result=await runAcceptance(options,{env,fetchImpl:f.fetchImpl});
|
|
assert.equal(result.outcome,'FAIL');assert.ok(!f.requests.some(r=>r.method==='POST'&&new URL(r.url).pathname.endsWith('/sync-tasks')));
|
|
assert.equal(result.cases.find(c=>c.id==='run.blocker').reason,'OFF_CHANGED_STOPPED');
|
|
});
|
|
test('internal errors never pass as controlled OFF rejection; unexpected acceptance aborts',async()=>{
|
|
assert.equal(businessRejected({http:200,body:{code:'S0500'}}),false);
|
|
assert.equal(businessRejected({http:503,body:{code:'B0001'}}),false);
|
|
assert.equal(offRejected({http:200,body:{code:'B0001',message:'SELFOP_WAREHOUSE_UNCONFIGURED'}}),false);
|
|
for(const config of [{offCode:'S0500'},{offMessage:'SELFOP_WAREHOUSE_UNCONFIGURED'},{acceptCommand:true}]){
|
|
const f=fixture(config);const result=await runAcceptance(options,{env,fetchImpl:f.fetchImpl});
|
|
assert.equal(result.outcome,'FAIL');assert.equal(result.cases.find(c=>c.id==='sync.off-controlled-rejection').status,'FAIL');
|
|
}
|
|
});
|
|
test('evidence whitelist retains diagnosis and drops arbitrary body content',()=>{
|
|
assert.deepEqual(evidence({http:200,body:{code:'B0001',requestId:'request-2',traceId:'trace-2',password:'secret',data:{accessToken:'secret'}}}),
|
|
{http:200,code:'B0001',requestId:'request-2',traceId:'trace-2'});
|
|
assert.equal(evidence({http:500,body:{code:'secret with spaces'}}).code,null);
|
|
assert.throws(()=>pin({salesRunId:0,inventoryRunId:0}),/CONTEXT/);
|
|
assert.equal(pin({salesRunId:0,inventoryRunId:0,queryAsOf:'2026-10-12T00:00:00Z'}).salesRunId,0);
|
|
});
|
|
|
|
function syntheticFixture(){
|
|
const notes={synthetic:true,fixtureId:'SYNTHETIC-SELFOP-20261012',sourceAccount:'lingxing-primary',hashes:{A:'hash-A',B:'hash-B'},
|
|
currentVersions:{A:{salesRunId:'202',inventoryRunId:'203'},B:{salesRunId:'206',inventoryRunId:'207'}},oldSalesRunId:'201',assertions:[
|
|
{endpoint:'owners',expected:{total:2,customerCodes:['SYNTH-OWNER-A','SYNTH-OWNER-B']}},
|
|
{endpoint:'sku-sales-summary',expected:{total:2,records:{'SYNTH-OWNER-A':{outboundQuantity:30,observedQuantity:30,dataStatus:'COMPLETE',completeDayCount:2,partialDayCount:0,unknownDayCount:0},
|
|
'SYNTH-OWNER-B':{outboundQuantity:10,observedQuantity:10,dataStatus:'COMPLETE'}}}},
|
|
{endpoint:'sku-daily-sales',expected:{points:[{businessDate:'D-2',outboundQuantity:10,dataStatus:'COMPLETE'},{businessDate:'D-1',outboundQuantity:20,dataStatus:'COMPLETE'}],summaryOutboundQuantity:30}},
|
|
{endpoint:'sku-sales-summary',params:{rangePreset:'TODAY'},expected:{'SYNTH-OWNER-A':{outboundQuantity:null,observedQuantity:5,dataStatus:'PARTIAL'},'SYNTH-OWNER-B':{outboundQuantity:null,observedQuantity:2,dataStatus:'PARTIAL'}}},
|
|
{endpoint:'sku-sales-summary',params:{salesRunId:'201'},expected:{outboundQuantity:3,observedQuantity:3,dataStatus:'COMPLETE'}},
|
|
{endpoint:'inventory-coverage',expected:{total:5,sharedByOwner:{'SYNTH-OWNER-A':{physicalQuantity:60,availableQuantity:60,outboundQuantity30d:30,inventoryCoverageDays:60},
|
|
'SYNTH-OWNER-B':{physicalQuantity:20,availableQuantity:20,outboundQuantity30d:10,inventoryCoverageDays:60}},zero:{sku:'SYNTH-ZERO'},unknown:{sku:'SYNTH-UNKNOWN'},new:{sku:'SYNTH-NEW'}}},
|
|
{endpoint:'hot-skus',expected:{total:2,order:['SYNTH-OWNER-A','SYNTH-OWNER-B'],sku:'SYNTH-SHARED-SKU',outboundQuantities:[30,10]}},
|
|
{endpoint:'slow-skus',expected:{total:2,order:['SYNTH-OWNER-B','SYNTH-OWNER-A'],sku:'SYNTH-SHARED-SKU',outboundQuantities:[10,30]}},
|
|
{endpoint:'sku-sales-summary',params:{warehouseCode:'SELFOP-TEST-B'},expected:{total:1,customerCode:'SYNTH-OWNER-A',outboundQuantity:999}},
|
|
{endpoint:'sync-overview',expected:{A:{sampleCount:2,sampleDates:['D-3','D-1'],accumulationStartedOn:'D-3'},B:{sampleCount:0,accumulationStartedOn:null}}},
|
|
]};
|
|
const context={...notes.currentVersions.A,salesScopeHash:'hash-A',inventoryScopeHash:'hash-A',queryAsOf:'2026-10-12T04:00:00Z',anchorDate:'2026-10-12'};
|
|
const cases=syntheticCases(notes,context),payloads=new Map();
|
|
const row=(customerCode,sku,fields={})=>({sourceAccount:notes.sourceAccount,warehouseCode:'SELFOP-TEST-A',customerCode,sku,...fields});
|
|
for(const scenario of cases){
|
|
const id=scenario.id.substring(10),data={context:{...context},records:[],total:0};
|
|
if(id==='owners'){data.records=['SYNTH-OWNER-A','SYNTH-OWNER-B'].map(customerCode=>({customerCode,customerName:null}));data.total=2;}
|
|
if(id==='date-range-and-owner-isolation'){data.records=Object.entries(notes.assertions[1].expected.records).map(([code,fields])=>row(code,'SYNTH-SHARED-SKU',fields));data.total=2;}
|
|
if(id==='daily-exact-axis'){data.summary={customerCode:'SYNTH-OWNER-A',sku:'SYNTH-SHARED-SKU',outboundQuantity:30};data.points=notes.assertions[2].expected.points.map((p,i)=>({...p,businessDate:fixtureDate(context.anchorDate,i-2)}));}
|
|
if(id==='today-partial'){data.records=Object.entries(notes.assertions[3].expected).map(([code,fields])=>row(code,'SYNTH-SHARED-SKU',fields));data.total=2;}
|
|
if(id==='old-version-fixed'){data.context.salesRunId='201';data.records=[row('SYNTH-OWNER-A','SYNTH-SHARED-SKU',notes.assertions[4].expected)];data.total=1;}
|
|
if(id==='inventory-zero-unknown-and-age'){
|
|
data.records=Object.entries(notes.assertions[5].expected.sharedByOwner).map(([code,fields])=>row(code,'SYNTH-SHARED-SKU',fields));
|
|
data.records.push(row('SYNTH-OWNER-A','SYNTH-ZERO',{physicalQuantity:0,availableQuantity:0}),
|
|
row('SYNTH-OWNER-A','SYNTH-UNKNOWN',{physicalQuantity:null,availableQuantity:null,inventoryCoverageDays:null,quantityStatuses:{physicalQuantity:'UNKNOWN',availableQuantity:'UNKNOWN'}}),
|
|
row('SYNTH-OWNER-A','SYNTH-NEW',{observationEligible:false,inventoryCoverageDays:null}));data.total=5;
|
|
}
|
|
if(id==='hot-skus-eligibility-order'||id==='slow-skus-eligibility-order'){
|
|
const expected=notes.assertions[id.startsWith('hot')?6:7].expected;
|
|
data.records=expected.order.map((code,i)=>row(code,expected.sku,{outboundQuantity:expected.outboundQuantities[i]}));data.total=2;
|
|
}
|
|
if(id==='warehouse-b-positive'){
|
|
data.context={...context,...notes.currentVersions.B,salesScopeHash:'hash-B',inventoryScopeHash:'hash-B'};
|
|
data.records=[{...row('SYNTH-OWNER-A','SYNTH-SHARED-SKU',{outboundQuantity:999}),warehouseCode:'SELFOP-TEST-B'}];data.total=1;
|
|
}
|
|
if(id==='zero-sales-versus-unknown')data.points=[-2,-1].map(offset=>({businessDate:fixtureDate(context.anchorDate,offset),dataStatus:'COMPLETE',outboundQuantity:'0.000000',observedQuantity:0}));
|
|
if(id==='pre-existence-is-unknown')data.points=[{businessDate:'2026-10-10',dataStatus:'UNKNOWN',outboundQuantity:null,observedQuantity:null},
|
|
{businessDate:'2026-10-11',dataStatus:'COMPLETE',outboundQuantity:0}];
|
|
if(id.startsWith('daily-sample-gaps-')){
|
|
const letter=id.at(-1),expected=notes.assertions[9].expected[letter],present=new Set((expected.sampleDates??[]).map(p=>fixtureDate(context.anchorDate,Number(p.slice(1)))));
|
|
data.collectionMode='OFF';data.warehouses=[{warehouseCode:'SELFOP-TEST-'+letter,dataFamily:'INVENTORY',executionEnabled:false,publicationEnabled:false,
|
|
inventoryDailySamples:{windowFrom:'2026-09-12',windowUntil:'2026-10-12',expectedDays:30,sampleCount:expected.sampleCount,
|
|
accumulationStartedOn:expected.accumulationStartedOn===null?null:'2026-10-09',missingDates:Array.from({length:30},(_,i)=>fixtureDate(context.anchorDate,i-30)).filter(p=>!present.has(p))}}];
|
|
}
|
|
payloads.set(scenario.id,data);
|
|
}
|
|
return {notes,context,cases,payloads};
|
|
}
|
|
test('reviewed synthetic fixture covers all thirteen business checks with bound old/current version pairs',()=>{
|
|
const f=syntheticFixture();assert.equal(f.cases.length,13);
|
|
validateSyntheticNotes(f.notes,{account:'lingxing-primary',warehouse:'SELFOP-TEST-A',otherWarehouse:'SELFOP-TEST-B'});
|
|
assert.throws(()=>validateSyntheticNotes({...f.notes,synthetic:false},options),/SYNTHETIC/);
|
|
for(const c of f.cases)assert.equal(c.verify(f.payloads.get(c.id)),true,c.id);
|
|
const old=f.cases.find(c=>c.id==='synthetic.old-version-fixed');
|
|
assert.equal(old.params.salesRunId,'201');assert.equal(old.params.inventoryRunId,'203');assert.equal(old.params.queryAsOf,f.context.queryAsOf);
|
|
assert.ok(f.cases.every(c=>c.route!=='sync-tasks'&&!c.route.includes('publish')));
|
|
assert.equal(fixtureDate('2026-03-01',-1),'2026-02-28');
|
|
});
|
|
test('synthetic checks reject mixed owners/warehouses, silent current-version fallback, zero-filling and ineligible Top rows',()=>{
|
|
const f=syntheticFixture();
|
|
const reject=(id,mutate)=>{const c=f.cases.find(c=>c.id==='synthetic.'+id),data=structuredClone(f.payloads.get(c.id));mutate(data);assert.equal(c.verify(data),false,id);};
|
|
reject('date-range-and-owner-isolation',d=>{d.records[0].observedQuantity+=999;});
|
|
reject('date-range-and-owner-isolation',d=>{d.records[1].customerCode='SYNTH-OWNER-A';});
|
|
reject('warehouse-b-positive',d=>{d.records[0].warehouseCode='SELFOP-TEST-A';});
|
|
reject('old-version-fixed',d=>{d.context.salesRunId='202';});
|
|
reject('inventory-zero-unknown-and-age',d=>{d.records[3].availableQuantity=0;});
|
|
reject('pre-existence-is-unknown',d=>{d.points[0].observedQuantity=0;});
|
|
reject('daily-exact-axis',d=>{d.points[1].businessDate='2026-10-12';});
|
|
reject('hot-skus-eligibility-order',d=>{d.records[0].sku='SYNTH-NEW';});
|
|
reject('slow-skus-eligibility-order',d=>{d.records.reverse();});
|
|
reject('daily-sample-gaps-A',d=>{d.warehouses[0].inventoryDailySamples.missingDates.push('2026-10-11');});
|
|
reject('daily-sample-gaps-B',d=>{d.warehouses[0].inventoryDailySamples.sampleCount=1;});
|
|
});
|
|
test('synthetic report explicitly marks provider evidence and elapsed expiration as unverified without adding a wait blocker',async()=>{
|
|
const f=syntheticFixture();const result=await runAcceptance({...options,account:'lingxing-primary',warehouse:'SELFOP-TEST-A',otherWarehouse:'SELFOP-TEST-B'},
|
|
{env:{CHONGCHONG_ENV:'TEST',CHONGCHONG_ADMIN_API_BASE_URL:env.CHONGCHONG_ADMIN_API_BASE_URL},syntheticNotes:f.notes,
|
|
fetchImpl:async()=>({status:401,headers:new Headers(),json:async()=>({code:'A0401'})})});
|
|
assert.equal(result.synthetic,true);assert.equal(result.evidenceType,'SYNTHETIC_QUERY_FIXTURE_NOT_PROVIDER_EVIDENCE');
|
|
assert.ok(result.uncovered.some(item=>item.includes('30 elapsed minutes')));
|
|
assert.ok(!result.cases.some(c=>c.id==='context.expired'));
|
|
assert.equal(parseArguments(['--environment','TEST','--synthetic-fixture','notes.json']).syntheticFixture,'notes.json');
|
|
});
|
|
test('optional nullable fields survive NON_NULL wire serialization while zero-filling stays rejected',()=>{
|
|
const f=syntheticFixture();
|
|
const omitNulls=value=>Array.isArray(value)?value.map(omitNulls):value&&typeof value==='object'?
|
|
Object.fromEntries(Object.entries(value).filter(([,v])=>v!==null).map(([k,v])=>[k,omitNulls(v)])):value;
|
|
for(const c of f.cases)assert.equal(c.verify(omitNulls(f.payloads.get(c.id))),true,c.id);
|
|
for(const id of ['synthetic.today-partial','synthetic.pre-existence-is-unknown','synthetic.inventory-zero-unknown-and-age']){
|
|
const c=f.cases.find(c=>c.id===id),data=omitNulls(f.payloads.get(id));
|
|
if(id.endsWith('today-partial'))data.records[0].outboundQuantity=0;
|
|
else if(id.endsWith('pre-existence-is-unknown'))data.points[0].observedQuantity=0;
|
|
else data.records[3].availableQuantity=0;
|
|
assert.equal(c.verify(data),false,id);
|
|
}
|
|
});
|