import test from 'node:test'; import assert from 'node:assert/strict'; import {createRequire} from 'node:module'; const {deliverAuditRecord,stripPendingEvidence,collectionAllowed,validatedEvidence}=createRequire(import.meta.url)('../apps/browser/evidence-delivery.cjs'); function fixture(){ const payload={sourceUrl:'https://example.com/report',name:'report.pdf',sha256:'a'.repeat(64),bytes:12,mime:'application/pdf',decision:'allow',ruleId:'scanner-allow'}; const record={eventId:'b7a17c1d-680e-4c90-9a3c-3d18f50a4e3f',kind:'download.released',file:{sha256:payload.sha256,bytes:payload.bytes},protectedEvidence:{deviceId:'device-a',payload}}; const context={ready:true,deviceId:'device-a',policy:{expiresAt:new Date(Date.now()+60000).toISOString(),evidence:{enabled:true,captureDownloads:true}}}; return {payload,record,context}; } test('event-first retries preserve original UUID and atomic tuple without leaking evidence into events',async()=>{ const f=fixture(),calls=[],snapshot=structuredClone(f.record);let failure=true; const send=async(route,body)=>{calls.push({route,body});if(route.endsWith('/evidence')){if(failure){const error=Error('temporarily unavailable');error.status=503;throw error;}return {id:'00000000-0000-4000-8000-000000000001',sourceEventId:7};}return {};}; await assert.rejects(deliverAuditRecord({record:f.record,send,getContext:()=>f.context}),/temporarily unavailable/);assert.deepEqual(f.record,snapshot); failure=false;assert.equal((await deliverAuditRecord({record:f.record,send,getContext:()=>f.context})).evidenceDelivered,true); assert.deepEqual(calls.map(c=>c.route),['/api/device/events','/api/device/evidence','/api/device/events','/api/device/evidence']); for(const call of calls){assert.equal(call.body.eventId,f.record.eventId);if(call.route.endsWith('/events'))assert.equal('protectedEvidence' in call.body,false);else assert.deepEqual(call.body,{eventId:f.record.eventId,type:'download',payload:f.payload});} }); test('403 discards evidence after source event; unavailable evidence service keeps record retryable',async()=>{ for(const status of [403,503]){const f=fixture(),routes=[];const task=deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>{routes.push(route);if(route.endsWith('/evidence'))throw Object.assign(Error('rejected'),{status});return {};}}); if(status===403)assert.deepEqual(await task,{eventDelivered:true,evidenceDiscarded:true});else await assert.rejects(task,/rejected/);assert.deepEqual(routes,['/api/device/events','/api/device/evidence']);assert.ok(f.record.protectedEvidence);} }); test('device change, central disable and expired policy discard only evidence while preserving event',async()=>{ for(const change of [f=>f.context.deviceId='device-b',f=>f.context.policy.evidence.enabled=false,f=>f.context.policy.evidence.captureDownloads=false,f=>f.context.policy.expiresAt='2000-01-01T00:00:00Z']){const f=fixture(),routes=[];change(f);assert.deepEqual(await deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>{routes.push(route);return {};}}),{eventDelivered:true,evidenceDiscarded:true});assert.deepEqual(routes,['/api/device/events']);} const f=fixture(),stripped=stripPendingEvidence([f.record]);assert.equal(stripped[0].eventId,f.record.eventId);assert.deepEqual(stripped[0].file,f.record.file);assert.equal('protectedEvidence' in stripped[0],false);assert.ok(f.record.protectedEvidence); }); test('not-ready managed context pauses evidence after source event without losing tuple',async()=>{ const f=fixture(),routes=[];f.context.ready=false;await assert.rejects(deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>{routes.push(route);return {};}}),/paused/);assert.deepEqual(routes,['/api/device/events']);assert.ok(f.record.protectedEvidence); }); test('invalid evidence acknowledgement and invalid payload never mark evidence delivered',async()=>{ for(const acknowledgement of [undefined,{}, {id:7,sourceEventId:1},{id:'id',sourceEventId:1.5},{id:'00000000-0000-4000-8000-000000000001',sourceEventId:0},{id:'',sourceEventId:1}]){const f=fixture();await assert.rejects(deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>route.endsWith('/evidence')?acknowledgement:{}}),/acknowledgement invalid/);} const f=fixture(),routes=[];f.record.protectedEvidence.payload.secret='forbidden';await assert.rejects(deliverAuditRecord({record:f.record,getContext:()=>f.context,send:async route=>{routes.push(route);return {};}}),/Invalid protected/);assert.deepEqual(routes,['/api/device/events']); }); test('collection requires enabled current ready context and validation returns independent clone',()=>{ const f=fixture();assert.equal(collectionAllowed(f.context),true);assert.equal(collectionAllowed({...f.context,ready:false}),false);assert.equal(collectionAllowed({...f.context,policy:{...f.context.policy,expiresAt:'invalid'}}),false);const clone=validatedEvidence(f.payload);clone.name='changed';assert.equal(f.payload.name,'report.pdf'); });