test(fixtures): capture redacted SimAdmin responses

This commit is contained in:
chick
2026-07-16 06:35:28 +08:00
parent 9dffadbec2
commit c7da857aff
87 changed files with 9673 additions and 0 deletions
@@ -0,0 +1,101 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFile, readdir, stat } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import path from 'node:path';
import { upstream58e2204Operations } from '../../operation-registry/src/upstream-58e2204.ts';
import { collectOne, selectReadonlyOperations, validateReadonlyOperation, DENY_REASONS } from '../scripts/collector.ts';
import { redact } from '../src/redactor.ts';
import { payloadShape, validateShapeNode, validateSensitivePayload, validateStatusContract } from '../src/verifier.ts';
const root = path.resolve(import.meta.dirname, '..');
const fixtureRoot = path.join(root, 'src/simadmin');
const allowedEnvelope = ['schemaVersion','upstreamBaseline','capturedAt','sourceInstanceAlias','operationId','method','pathTemplate','statusCategory','httpStatus','contentType','latencyBucket','redacted','payload'];
// Preserve business-schema keys such as `url` and channel `headers`; sensitive values
// are typed-redacted. The exact top-level envelope already excludes transport metadata.
const forbiddenKeys = /^(raw(response)?|instance(id|name))$/i;
const DENIED_PATHS = new Set(Object.keys(DENY_REASONS));
const leakPatterns = [
/https?:\/\//i, /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
/\b(?:\d{1,3}\.){3}\d{1,3}\b/, /(?:[0-9a-f]{2}:){5}[0-9a-f]{2}/i,
/\+?\d(?:[ ()-]*\d){9,14}/, /\b\d{14,22}\b/,
/\b(?:bearer\s+)?[A-Za-z0-9_-]{32,}\b/i,
];
async function jsonFiles(dir:string):Promise<string[]> { const out:string[]=[]; for(const e of await readdir(dir,{withFileTypes:true})){const p=path.join(dir,e.name); if(e.isDirectory()) out.push(...await jsonFiles(p)); else if(e.name.endsWith('.json')) out.push(p);} return out; }
function walkKeys(v:any, cb:(k:string)=>void){ if(Array.isArray(v)) return v.forEach(x=>walkKeys(x,cb)); if(v&&typeof v==='object') for(const [k,x] of Object.entries(v)){cb(k);walkKeys(x,cb);} }
function stringLeaves(v:any,out:string[]=[]){if(Array.isArray(v))v.forEach(x=>stringLeaves(x,out));else if(v&&typeof v==='object')Object.values(v).forEach(x=>stringLeaves(x,out));else if(typeof v==='string'&&v.length>=3)out.push(v);return out;}
test('selector is registry-derived passive GET R0 only with explicit denials', () => {
const { selected, denied } = selectReadonlyOperations(upstream58e2204Operations);
assert.ok(selected.length > 20);
assert.ok(denied.some((x:any)=>x.pathTemplate==='/api/network/operators/scan' && x.denyReason));
for(const op of selected){ assert.equal(op.method,'GET'); assert.equal(op.riskLevel,'R0'); assert.doesNotMatch(op.pathTemplate,/[{}]/); assert.notEqual(op.pathTemplate,'/api/network/operators/scan'); }
});
test('collector source has no write-method or authentication escape hatch', async () => {
const source = await readFile(path.join(root,'scripts/collect-readonly-fixtures.ts'),'utf8');
assert.doesNotMatch(source, /ensureAuthenticated|login\s*\(|--method|--path|--origin/i);
assert.doesNotMatch(source, /['"](?:POST|PUT|PATCH|DELETE)['"]/);
});
test('transport can only receive credential-free GET, manual redirect and timeout', async () => {
let init:any; const fake=async (_url:any, i:any)=>{init=i; return new Response(JSON.stringify({phone:'+15551234567',message:'private',ip:'10.1.2.3'}),{status:200,headers:{'content-type':'application/json','set-cookie':'bad=1'}})};
const op=upstream58e2204Operations.find(x=>x.operationId==='getHealth')!;
const f=await collectOne({origin:'http://192.168.1.2',alias:'instance-1'},op,fake as any);
assert.deepEqual({method:init.method,body:init.body,redirect:init.redirect,credentials:init.credentials},{method:'GET',body:undefined,redirect:'manual',credentials:'omit'});
assert.deepEqual(init.headers, { accept: 'application/json' }); assert.ok(init.signal);
assert.equal(f.payload.phone,'[REDACTED]'); assert.equal(f.payload.message,'private'); assert.equal(f.payload.ip,'[REDACTED]');
});
test('redactor covers sensitive keys and value patterns deterministically',()=>{
const x=redact({password:'p',TOKEN:'t',content:'body',phone:'x',other:['+155****4567','+155****4567','a@b.example','https://private.example/x','aa:bb:cc:dd:ee:ff','2001:db8::1']});
assert.equal(x.password,'[REDACTED]'); assert.equal(x.TOKEN,'[REDACTED]'); assert.equal(x.content,'[REDACTED]'); assert.equal(x.phone,'[REDACTED]');
for(const p of leakPatterns) assert.doesNotMatch(JSON.stringify(x),p);
});
test('Registry operation identity also rejects in-place structural mutation',()=>{
const op:any=upstream58e2204Operations.find(x=>x.operationId==='getHealth')!;
const original=structuredClone(op);
const mutations=[
(x:any)=>x.sensitiveFields.push({direction:'response',path:'$.response.data.secret'}),
(x:any)=>{delete x.pathTemplate;},
(x:any)=>{x.method='POST';},
(x:any)=>{x.sensitiveFields='changed';},
];
for(const mutate of mutations){
try{mutate(op);assert.throws(()=>validateReadonlyOperation(op),/Registry integrity/);}
finally{for(const key of Object.keys(op))delete op[key];Object.assign(op,structuredClone(original));}
}
validateReadonlyOperation(op);
});
test('collector categorizes status and JSON parse outcomes consistently',async()=>{
const op=upstream58e2204Operations.find(x=>x.operationId==='getHealth')!;
const run=(status:number,type:string,body:string|null)=>collectOne({origin:'http://192.168.1.2',alias:'instance-1'},op,async()=>new Response(body,{status,headers:{'content-type':type}}));
for(const [status,type,body,category] of [[200,'application/problem+json','{}','success'],[200,'application/json','bad','non-json'],[204,'text/plain',null,'non-json'],[401,'application/json','{}','auth-required'],[405,'application/json','{}','unsupported'],[500,'application/json','{}','http-error']] as const){
const f=await run(status,type,body);assert.equal(f.statusCategory,category);validateStatusContract(f);
}
const redirect=await run(302,'application/json','{}');assert.equal(redirect.statusCategory,'http-error');validateStatusContract(redirect);
assert.throws(()=>validateStatusContract({statusCategory:'success',httpStatus:401,contentType:'application/json',payload:{},latencyBucket:'<250ms'}));
});
test('real fixtures satisfy envelope, registry, leak scan, and manifest integrity', async()=>{
const manifest=JSON.parse(await readFile(path.join(root,'src/manifest.json'),'utf8'));assert.deepEqual(Object.keys(manifest).sort(),['domainCoverage','files','realFixtureCount','schemaVersion','shapeBaseline','syntheticFixtureCount','upstreamBaseline']); const files=(await jsonFiles(fixtureRoot)).filter(x=>!x.includes('/synthetic-errors/'));
const srcEntries=await readdir(path.join(root,'src'));assert.ok(!srcEntries.includes('synthetic-errors'));
const config=JSON.parse(await readFile(path.resolve(root,'../../config.json'),'utf8'));
const privateValues=stringLeaves(config);
const expected=selectReadonlyOperations(upstream58e2204Operations).selected;
assert.equal(files.length,expected.length*2); assert.equal(manifest.realFixtureCount,files.length);assert.equal(manifest.syntheticFixtureCount,0);
assert.equal(manifest.schemaVersion,1);assert.equal(manifest.upstreamBaseline,'58e2204');
const shapePath=path.join(root,'src/response-shapes-58e2204.json');const shapeText=await readFile(shapePath,'utf8');const shapeBaseline=JSON.parse(shapeText);
assert.deepEqual(Object.keys(shapeBaseline).sort(),['schemaVersion','shapes','upstreamBaseline']);assert.equal(shapeBaseline.schemaVersion,1);assert.equal(shapeBaseline.upstreamBaseline,'58e2204');assert.equal(Object.keys(shapeBaseline.shapes).length,78);
assert.deepEqual(Object.keys(manifest.shapeBaseline).sort(),['path','sha256','size']);assert.equal(manifest.shapeBaseline.path,'src/response-shapes-58e2204.json');assert.equal(manifest.shapeBaseline.size,(await stat(shapePath)).size);assert.equal(manifest.shapeBaseline.sha256,createHash('sha256').update(shapeText).digest('hex'));assert.ok(!manifest.files.some((x:any)=>x.path===manifest.shapeBaseline.path));
for(const node of Object.values(shapeBaseline.shapes))validateShapeNode(node);
const registry=new Map(upstream58e2204Operations.map(x=>[x.operationId,x])); const domains=new Map<string,number>();const pairs=new Set<string>();
const disk=files.map(file=>path.relative(root,file)).sort();const listed=manifest.files.map((x:any)=>x.path).sort();assert.deepEqual(listed,disk);assert.equal(new Set(listed).size,listed.length);
for(const file of files){const text=await readFile(file,'utf8'); const f=JSON.parse(text); assert.deepEqual(Object.keys(f).sort(),[...allowedEnvelope].sort()); assert.equal(f.schemaVersion,1);assert.equal(f.upstreamBaseline,'58e2204');assert.match(f.capturedAt,/^20(?:2[4-9]|[3-9]\d)-\d{2}-\d{2}$/);assert.ok(!Number.isNaN(Date.parse(`${f.capturedAt}T00:00:00Z`)));assert.ok(['instance-1','instance-2'].includes(f.sourceInstanceAlias));assert.equal(f.redacted,true); assert.equal(f.method,'GET'); assert.ok(!('synthetic' in f));assert.ok(['success','non-json','auth-required','unsupported','http-error','timeout','network-error'].includes(f.statusCategory));if(['timeout','network-error'].includes(f.statusCategory))assert.equal(f.httpStatus,null);else assert.ok(Number.isInteger(f.httpStatus)); const op:any=registry.get(f.operationId);assert.ok(op);validateStatusContract(f);validateSensitivePayload(f.payload,op);const shapeKey=`${f.sourceInstanceAlias}::${f.operationId}`;assert.deepEqual(payloadShape(f.payload),shapeBaseline.shapes[shapeKey]); assert.equal(op?.riskLevel,'R0');assert.equal(op?.method,'GET'); assert.equal(op?.pathTemplate,f.pathTemplate);assert.ok(!DENIED_PATHS.has(f.pathTemplate));domains.set(op.upstreamDomain,(domains.get(op.upstreamDomain)||0)+1);const pair=`${f.sourceInstanceAlias}:${f.operationId}`;assert.ok(!pairs.has(pair));pairs.add(pair); walkKeys(f,k=>assert.doesNotMatch(k,forbiddenKeys)); for(const p of leakPatterns) assert.doesNotMatch(text,p); for(const value of privateValues) assert.ok(!text.includes(value),'fixture contains configured value'); const rel=path.relative(root,file); const m=manifest.files.find((x:any)=>x.path===rel); assert.ok(m); assert.equal(m.size,(await stat(file)).size);assert.match(m.sha256,/^[a-f0-9]{64}$/); assert.equal(m.sha256,createHash('sha256').update(text).digest('hex')); }
assert.deepEqual(Object.fromEntries([...domains].sort()),manifest.domainCoverage);
assert.deepEqual(Object.keys(shapeBaseline.shapes).sort(),[...pairs].map(x=>x.replace(':','::')).sort());
assert.deepEqual([...pairs].sort(),expected.flatMap((op:any)=>['instance-1','instance-2'].map(alias=>`${alias}:${op.operationId}`)).sort());
});