Files
safetynet/tests/broker-supervisor.test.mjs
T

67 lines
7.4 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {EventEmitter} from 'node:events';
import {createRequire} from 'node:module';
const {createBrokerSupervisor}=createRequire(import.meta.url)('../apps/browser/broker-supervisor.cjs');
function fixture(extra={}){const child=new EventEmitter();child.sent=[];child.send=(m,cb)=>{child.sent.push(m);cb();};child.kill=()=>{child.killed=true;child.exitCode=0;child.emit('exit',0);};let options,quarantines=0;const supervisor=createBrokerSupervisor({resourceRoot:'C:/trusted/broker',verifyPackage:()=>true,persistAudit:event=>({persisted:true,eventId:event.eventId}),acceptCertificate:()=>true,onQuarantine:()=>quarantines++,spawn:(entry,args,opts)=>{options={entry,args,opts};return child;},deadlineMs:100,...extra});return {child,supervisor,get options(){return options;},get quarantines(){return quarantines;}};}
async function starting(f){const pending=f.supervisor.start({policyVersion:3,port:43871,config:{synthetic:true}});await new Promise(resolve=>setImmediate(resolve));return {pending,context:(({version,generation,policyVersion})=>({version,generation,policyVersion}))(f.child.sent[0])};}
function ready(f,context){f.child.emit('message',{type:'ready',...context,authMode:'basic',sequence:0,port:43871,credentials:{username:'safetynet-'+'a'.repeat(32),password:'b'.repeat(43)}});}
test('fixed verified launch, sanitized environment and private authenticated generation',async()=>{const f=fixture();const {pending,context}=await starting(f);ready(f,context);assert.equal((await pending).port,43871);assert.deepEqual(f.options.opts.execArgv,[]);assert.equal(f.options.opts.env.NODE_OPTIONS,undefined);assert.equal(f.options.opts.env.NODE_PATH,undefined);assert.ok(Object.keys(f.options.opts.env).every(key=>['SystemRoot','WINDIR','TEMP','TMP','USERPROFILE','LOCALAPPDATA','APPDATA'].includes(key)));for(const key of ['USERPROFILE','LOCALAPPDATA','APPDATA'])assert.equal(f.options.opts.env[key],process.env[key]);assert.ok(f.options.entry.endsWith('production-entry.cjs'));assert.equal(f.supervisor.status().ready,true);await f.supervisor.close();assert.equal(f.child.killed,true);});
test('audit receipt preserves authenticated event ID',async()=>{const f=fixture();const {pending,context}=await starting(f);ready(f,context);await pending;const eventId='12345678-1234-1234-1234-123456789abc';f.child.emit('message',{type:'audit',...context,requestId:eventId,sequence:1,event:{eventId}});await new Promise(resolve=>setImmediate(resolve));assert.equal(f.child.sent[1].receipt.eventId,eventId);await f.supervisor.close();});
test('wrong generation quarantines with no retry',async()=>{const f=fixture();const {pending,context}=await starting(f);const rejected=assert.rejects(pending,/unavailable/);ready(f,{...context,generation:'wrong'});await rejected;assert.equal(f.quarantines,1);assert.equal(f.child.killed,true);await assert.rejects(f.supervisor.start({policyVersion:3,port:43871}),/invalid/);});
test('deadline and child crash fail closed',async()=>{const f=fixture({deadlineMs:5});const {pending}=await starting(f);await assert.rejects(pending,/unavailable/);assert.equal(f.quarantines,1);const g=fixture();const {pending:p,context}=await starting(g);ready(g,context);await p;g.child.emit('exit',1);assert.equal(g.supervisor.status().ready,false);assert.equal(g.quarantines,1);});
test('unverified package never spawns',async()=>{const f=fixture({verifyPackage:()=>false});await assert.rejects(f.supervisor.start({policyVersion:3,port:43871,config:{}}),/unavailable/);assert.equal(f.options,undefined);});
test('close waits terminal exit and rejects unconfirmed shutdown',async()=>{
const f=fixture({shutdownMs:10});const {pending,context}=await starting(f);ready(f,context);await pending;
f.child.kill=()=>{f.child.killed=true;};
await assert.rejects(f.supervisor.close(),/shutdown-unconfirmed/);
assert.equal(f.supervisor.status().failed,true);assert.equal(f.quarantines,1);
});
test('concurrent start reserves before asynchronous package verification',async()=>{
let release;const f=fixture({verifyPackage:()=>new Promise(resolve=>{release=resolve;})});
const first=f.supervisor.start({policyVersion:3,port:43871,config:{}});
await assert.rejects(f.supervisor.start({policyVersion:3,port:43871,config:{}}),/invalid/);
release(true);await new Promise(resolve=>setImmediate(resolve));
const context=(({version,generation,policyVersion})=>({version,generation,policyVersion}))(f.child.sent[0]);ready(f,context);await first;await f.supervisor.close();
});
test('monotonic stream rejects replay, gaps and out-of-order requests',async()=>{
for(const sequences of [[1,1],[2],[1,3],[1,0]]){
const f=fixture();const {pending,context}=await starting(f);ready(f,context);await pending;
for(const sequence of sequences)f.child.emit('message',{type:'audit',...context,sequence,requestId:'12345678-1234-1234-1234-123456789abc',event:{eventId:'12345678-1234-1234-1234-123456789abc'}});
assert.equal(f.supervisor.status().failed,true);assert.equal(f.quarantines,1);await f.supervisor.close();
}
});
test('more than 10000 legitimate requests retain constant replay state',async()=>{
const f=fixture();const {pending,context}=await starting(f);ready(f,context);await pending;
for(let sequence=1;sequence<=10001;sequence++){
const requestId=`12345678-1234-1234-1234-${sequence.toString(16).padStart(12,'0')}`;
f.child.emit('message',{type:'audit',...context,sequence,requestId,event:{eventId:requestId}});
assert.equal(f.child.sent.at(-1).sequence,sequence);
}
assert.equal(f.supervisor.status().ready,true);assert.equal(f.quarantines,0);await f.supervisor.close();
});
test('fixed startup diagnostic is local, bounded and never grants readiness',async()=>{
const f=fixture();const {pending,context}=await starting(f);const rejection=assert.rejects(pending,/unavailable/);
f.child.emit('message',{type:'startup-fault',...context,code:'broker-startup-inspector'});await rejection;
assert.equal(f.supervisor.status().startupCode,'broker-startup-inspector');assert.equal(f.supervisor.status().ready,false);await f.supervisor.close();
const g=fixture();const started=await starting(g);const rejected=assert.rejects(started.pending,/unavailable/);
g.child.emit('message',{type:'startup-fault',...started.context,code:'raw arbitrary exception'});await rejected;assert.equal(g.supervisor.status().startupCode,undefined);await g.supervisor.close();
});
test('diagnostics cannot skip or replace verification and cannot repeat protected work',async()=>{
for(const tracer of [()=>true,()=>{throw Error('fixed-diagnostics-failure');},async(_name,_attributes,run)=>{await run();try{await run();}catch{}return true;}]){
let calls=0;const f=fixture({verifyPackage:()=>{calls++;return false;},traceOperation:tracer});
await assert.rejects(f.supervisor.start({policyVersion:3,port:43871,config:{}}),/unavailable/);
assert.equal(calls,1);assert.equal(f.options,undefined);await f.supervisor.close();
}
});
test('diagnostic stages retain fixed names and original ready and shutdown outcomes',async()=>{
const stages=[];const f=fixture({traceOperation:async(name,attributes,run)=>{stages.push({name,attributes});await run();return {ready:false};}});
const {pending,context}=await starting(f);ready(f,context);
assert.equal((await pending).port,43871);await f.supervisor.close();
assert.deepEqual(stages.map(value=>value.name),['browser.broker.verify','browser.broker.start','browser.broker.shutdown']);
assert.ok(stages.every(value=>JSON.stringify(value.attributes)==='{"component":"network"}'));
});