Files
safetynet/tests/broker-audit-persistence.test.mjs

41 lines
3.8 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
import {randomUUID} from 'node:crypto';
const require=createRequire(import.meta.url);
const {createBrokerAuditPersistence}=require('../apps/browser/broker-audit-persistence.cjs');
const event=()=>({eventId:randomUUID(),kind:'upload.authorized',ruleId:'broker-approved',policyVersion:4,request:{bytes:6,detectedType:'text'}});
function fixture(overrides={}){const queue=[];let writes=0,failures=0;const sink=createBrokerAuditPersistence({policyVersion:4,current:()=>true,getQueue:()=>queue,persistQueue:()=>{writes++;},onFailure:()=>{failures++;},...overrides});return {sink,queue,writes:()=>writes,failures:()=>failures};}
test('receipt preserves broker event identity only after synchronous parent commit',()=>{
const f=fixture(),input=event(),receipt=f.sink.persist(input);
assert.deepEqual(receipt,{persisted:true,eventId:input.eventId});assert.equal(f.writes(),1);
assert.notEqual(f.queue[0],input);assert.ok(Object.isFrozen(f.queue[0]));assert.equal(f.queue[0].eventId,input.eventId);
});
test('write failure is sticky and never issues a receipt or retries uncertain commit',()=>{
let writes=0;const f=fixture({persistQueue:()=>{writes++;throw Error('private filesystem detail');}});
assert.throws(()=>f.sink.persist(event()),/^Error: broker-audit-parent-unavailable$/);
assert.throws(()=>f.sink.persist(event()),/broker-audit-parent-unavailable/);assert.equal(writes,1);
});
test('invalid policy, duplicate ID and queue capacity fail before write',()=>{
for(const mode of ['policy','duplicate','capacity']){const f=fixture(),input=event();if(mode==='policy')input.policyVersion=3;if(mode==='duplicate')f.queue.push({...input});if(mode==='capacity')f.queue.push(...Array.from({length:10000},()=>({eventId:randomUUID()})));
assert.throws(()=>f.sink.persist(input),/broker-audit-parent-unavailable/);assert.equal(f.writes(),0);
}
});
test('revocation during commit prevents acknowledgment and asynchronous enqueue is rejected',()=>{
let active=true;const f=fixture({current:()=>active,persistQueue:()=>{active=false;}});
assert.throws(()=>f.sink.persist(event()),/broker-audit-parent-unavailable/);
const asyncFixture=fixture({persistQueue:()=>Promise.resolve()});assert.throws(()=>asyncFixture.sink.persist(event()),/broker-audit-parent-unavailable/);
for(const result of [false,true,{queued:true}]){const uncertain=fixture({persistQueue:()=>result});assert.throws(()=>uncertain.sink.persist(event()),/broker-audit-parent-unavailable/);}
});
test('raw extra fields and top-level getters cannot become an audit channel',()=>{
const f=fixture();let read=false;const input=event();Object.defineProperty(input,'secret',{enumerable:true,get(){read=true;return 'private';}});
assert.throws(()=>f.sink.persist(input),/broker-audit-parent-unavailable/);assert.equal(read,false);assert.equal(f.writes(),0);
const raw=fixture();assert.throws(()=>raw.sink.persist({...event(),body:'private'}),/broker-audit-parent-unavailable/);assert.equal(raw.writes(),0);
const nested=fixture(),nestedInput=event();Object.defineProperty(nestedInput.request,'bytes',{enumerable:true,get(){read=true;return 6;}});
assert.throws(()=>nested.sink.persist(nestedInput),/broker-audit-parent-unavailable/);assert.equal(read,false);assert.equal(nested.writes(),0);
});
test('real download facts are immutable and linked without a generic request hash',()=>{
const input={eventId:randomUUID(),kind:'download.block',ruleId:'synthetic-malware',policyVersion:4,file:{bytes:128,detectedType:'pdf',sha256:'a'.repeat(64)},broker:{requestId:randomUUID(),phase:'response.block',reason:'broker-download-inspection-block',authorizedEventId:randomUUID()}};
const f=fixture();assert.equal(f.sink.persist(input).eventId,input.eventId);assert.ok(Object.isFrozen(f.queue[0].file));assert.equal(f.queue[0].request,undefined);
});