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 { 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();const pairs=new Set(); 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()); });