Files
safetynet/tests/evidence-viewer.test.mjs
T

14 lines
3.6 KiB
JavaScript
Raw Normal View History

import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const {createEvidenceViewer}=createRequire(import.meta.url)('../apps/server/public/evidence-viewer.js');
function fixture(){let principal={sub:'analyst',role:'analyst',permissions:['evidence.read']},timer;const requests=[],renders=[];const view=createEvidenceViewer({principal:()=>principal,request:path=>new Promise((resolve,reject)=>requests.push({path,resolve,reject})),render:(...args)=>renders.push(args),clear:()=>renders.push(['clear']),setTimer:(fn,ms)=>{assert.equal(ms,60000);timer=fn;return 1;},cancelTimer:()=>{timer=null;}});return {view,requests,renders,revoke:()=>{principal={...principal,permissions:[]};view.permissions();},identity:()=>{principal={...principal,sub:'other'};view.permissions();},expire:()=>timer()};}
const id='11111111-1111-4111-8111-111111111111';
async function open(f,incident='one'){const result=f.view.open(incident);f.requests.at(-1).resolve({evidence:[{id,availability:'available'}]});await result;}
test('metadata alone does not retrieve plaintext; purpose required',async()=>{const f=fixture();await open(f);assert.equal(f.requests.length,1);await f.view.reveal(id,'');assert.equal(f.requests.length,1);const reveal=f.view.reveal(id,'investigation');assert.match(f.requests.at(-1).path,/reason=investigation$/);f.requests.at(-1).resolve({id,type:'interaction',payload:{content:'<script>synthetic</script>'}});await reveal;assert.equal(f.renders.at(-1)[0],'plaintext');assert.match(f.renders.at(-1)[1],/<script>/);f.expire();assert.deepEqual(f.renders.at(-1),['clear']);});
for(const action of ['reset','revoke','identity'])test(`in-flight plaintext cannot repopulate after ${action}`,async()=>{const f=fixture();await open(f);const reveal=f.view.reveal(id,'incident-response');if(action==='reset')f.view.reset();else f[action]();f.requests.at(-1).resolve({id,type:'download',payload:{name:'synthetic'}});await reveal;assert.deepEqual(f.renders.at(-1),['clear']);});
test('older incident metadata cannot replace new selection',async()=>{const f=fixture(),old=f.view.open('old'),newer=f.view.open('new');f.requests[1].resolve({evidence:[{id}]});await newer;const expected=f.renders.length;f.requests[0].resolve({evidence:[{id:'old'}]});await old;assert.equal(f.renders.length,expected);});
test('errors are sanitized and payloads bounded',async()=>{const f=fixture();await open(f);let reveal=f.view.reveal(id,'compliance');f.requests.at(-1).reject(Error('SECRET backend detail'));await reveal;assert.doesNotMatch(f.renders.at(-1)[1],/SECRET/);reveal=f.view.reveal(id,'compliance');f.requests.at(-1).resolve({id,type:'interaction',payload:{content:'x'.repeat(70000)}});await reveal;assert.equal(f.renders.at(-1)[0],'error');});
test('viewer cannot fetch evidence outside the displayed incident',async()=>{const f=fixture();await open(f);const count=f.requests.length;await f.view.reveal('22222222-2222-4222-8222-222222222222','investigation');assert.equal(f.requests.length,count);assert.equal(f.renders.at(-1)[0],'error');});
test('viewer suppresses a mismatched source event in a decrypted response',async()=>{const f=fixture(),opening=f.view.open('one');f.requests.at(-1).resolve({evidence:[{id,availability:'available',sourceEventId:42}]});await opening;const revealing=f.view.reveal(id,'investigation');f.requests.at(-1).resolve({id,sourceEventId:43,type:'interaction',payload:{content:'must not render'}});await revealing;assert.equal(f.renders.at(-1)[0],'error');assert.equal(f.renders.some(([kind,value])=>kind==='plaintext'&&value.includes('must not render')),false);});