60 lines
6.8 KiB
JavaScript
60 lines
6.8 KiB
JavaScript
import test from 'node:test';
|
|||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import {mkdtempSync,rmSync} from 'node:fs';
|
||
|
|
import {tmpdir} from 'node:os';
|
||
|
|
import {join} from 'node:path';
|
||
|
|
import {openStore,digest} from '../apps/server/store.mjs';
|
||
|
|
import {createApp} from '../apps/server/app.mjs';
|
||
|
|
async function fixture(t){
|
||
|
|
const dir=mkdtempSync(join(tmpdir(),'safetynet-review-')),store=openStore(dir),origin='http://127.0.0.1:4310';
|
||
|
|
const server=createApp(store,{publicUrl:origin,secure:false,requireIndependentPolicyApproval:true}).listen(0,'127.0.0.1');await new Promise(r=>server.once('listening',r));
|
||
|
|
t.after(async()=>{await new Promise(r=>server.close(r));store.db.close();rmSync(dir,{recursive:true,force:true});});
|
||
|
|
async function api(path,method='GET',body,subject='author',role='admin'){
|
||
|
|
const credential=`synthetic-review-session-${subject}-12345678901234567890`;
|
||
|
|
store.db.prepare('INSERT OR REPLACE INTO sessions VALUES(?,?,?)').run(digest(credential),Date.now()+60000,JSON.stringify({sub:subject,role}));
|
||
|
|
const response=await fetch(`http://127.0.0.1:${server.address().port}${path}`,{method,headers:{cookie:`sn_session=${credential}`,origin,'content-type':'application/json'},body:body===undefined?undefined:JSON.stringify(body)});
|
||
|
|
return {status:response.status,body:await response.json()};
|
||
|
|
}
|
||
|
|
function candidate(){const p=store.get('policy');p.version++;p.brand.name='Reviewed Brand';return p;}
|
||
|
|
async function approve(r,subject='reviewer',role='admin'){return api(`/api/admin/policy-reviews/${r.reviewId}/approve`,'POST',{payloadHash:r.payloadHash,expectedVersion:r.expectedVersion,approve:true},subject,role);}
|
||
|
|
return {dir,store,api,candidate,approve};
|
||
|
|
}
|
||
|
|
test('mandatory independent approval prevents self approval, operator publication, tampering and duplicate release',async t=>{
|
||
|
|
const f=await fixture(t),p=f.candidate(),queued=await f.api('/api/admin/policy','PUT',p,'operator','operator');
|
||
|
|
assert.equal(queued.status,202);assert.equal(f.store.get('policy').version,1);
|
||
|
|
assert.equal((await f.approve(queued.body,'operator','admin')).status,403);
|
||
|
|
assert.equal((await f.approve(queued.body,'reviewer','operator')).status,403);
|
||
|
|
assert.equal((await f.approve({...queued.body,payloadHash:'0'.repeat(64)})).status,409);
|
||
|
|
const approved=await f.approve(queued.body);assert.equal(approved.status,200);assert.equal(f.store.get('policy').brand.name,'Reviewed Brand');
|
||
|
|
assert.equal((await f.approve(queued.body)).status,409);
|
||
|
|
const audit=JSON.parse(f.store.db.prepare("SELECT metadata FROM events WHERE kind='policy.review-approved'").get().metadata);assert.equal(audit.author,'operator');assert.equal(audit.actor,'reviewer');assert.equal(JSON.stringify(audit).includes('Reviewed Brand'),false);
|
||
|
|
});
|
||
|
|
test('queue persists, presents exact diff and hash, rejects stale CAS and supports independent rejection',async t=>{
|
||
|
|
const f=await fixture(t),first=(await f.api('/api/admin/policy-reviews','POST',{policy:f.candidate()})).body;
|
||
|
|
const second=(await f.api('/api/admin/policy','PUT',f.candidate())).body;
|
||
|
|
const detail=await f.api(`/api/admin/policy-reviews/${first.reviewId}`);assert.ok(detail.body.diff.changes.some(c=>c.path==='/brand/name'));assert.equal(detail.body.payloadHash,first.payloadHash);
|
||
|
|
const secondConnection=openStore(f.dir);assert.equal(secondConnection.db.prepare('SELECT payload_hash FROM policy_reviews WHERE id=?').get(first.reviewId).payload_hash,first.payloadHash);secondConnection.db.close();
|
||
|
|
assert.equal((await f.approve(first)).status,200);assert.equal((await f.approve(second)).status,409);
|
||
|
|
const list=await f.api('/api/admin/policy-reviews');assert.equal(list.body.reviews.find(r=>r.id===second.reviewId).status,'stale');
|
||
|
|
const third=(await f.api('/api/admin/policy','PUT',f.candidate())).body;
|
||
|
|
assert.equal((await f.api(`/api/admin/policy-reviews/${third.reviewId}/reject`,'POST',{payloadHash:third.payloadHash,reject:true})).status,403);
|
||
|
|
assert.equal((await f.api(`/api/admin/policy-reviews/${third.reviewId}/reject`,'POST',{payloadHash:third.payloadHash,reject:true},'reviewer')).status,200);
|
||
|
|
assert.equal((await f.approve(third)).status,409);assert.equal(f.store.get('policy').version,2);
|
||
|
|
});
|
||
|
|
test('all alternate publication paths queue and device and teaching effects wait for reviewer',async t=>{
|
||
|
|
const f=await fixture(t);
|
||
|
|
f.store.db.prepare('INSERT INTO devices(id,name,platform,user,token_hash) VALUES(?,?,?,?,?)').run('device','Device','win32','user','hash');
|
||
|
|
let queued=await f.api('/api/admin/devices/device/inspection','PATCH',{expectedVersion:1,documentAnalysisEnabled:false},'operator','operator');assert.equal(queued.status,202);assert.equal(f.store.db.prepare('SELECT document_analysis_enabled AS enabled FROM devices').get().enabled,null);
|
||
|
|
assert.equal((await f.approve(queued.body)).status,200);assert.equal(f.store.db.prepare('SELECT document_analysis_enabled AS enabled FROM devices').get().enabled,0);
|
||
|
|
queued=await f.api('/api/admin/security/blocklists','PUT',{expectedVersion:2,blockedHosts:['blocked.example'],blockedIPs:[]},'operator','operator');assert.equal(queued.status,202);assert.equal(f.store.get('policy').security.dns.blockedHosts.includes('blocked.example'),false);assert.equal((await f.approve(queued.body)).status,200);
|
||
|
|
queued=await f.api('/api/admin/policy/restore','POST',{sourceVersion:1,expectedVersion:3,approve:true},'operator','operator');assert.equal(queued.status,202);assert.equal(f.store.get('policy').version,3);assert.equal((await f.approve(queued.body)).status,200);
|
||
|
|
f.store.db.prepare('INSERT INTO teaching VALUES(?,?,?,?,?,?,?,?)').run('teaching',new Date().toISOString(),'author','Sensitive','draft',JSON.stringify({registrationReady:true,sha256:'a'.repeat(64),fragmentHashes:[]}),null,null);
|
||
|
|
queued=await f.api('/api/admin/teaching/teaching/publish','POST',{expectedVersion:4,approve:true},'operator','operator');assert.equal(queued.status,202);assert.equal(f.store.db.prepare('SELECT status FROM teaching').get().status,'draft');assert.equal((await f.approve(queued.body)).status,200);assert.equal(f.store.db.prepare('SELECT reviewer FROM teaching').get().reviewer,'reviewer');
|
||
|
|
});
|
||
|
|
test('durable candidate corruption and changing device state fail closed atomically',async t=>{
|
||
|
|
const f=await fixture(t),queued=(await f.api('/api/admin/policy','PUT',f.candidate())).body;
|
||
|
|
const policy=f.candidate();policy.brand.name='Tampered';f.store.db.prepare('UPDATE policy_reviews SET policy=? WHERE id=?').run(JSON.stringify(policy),queued.reviewId);
|
||
|
|
assert.equal((await f.approve(queued)).status,409);assert.equal(f.store.get('policy').version,1);
|
||
|
|
f.store.db.prepare('INSERT INTO devices(id,name,platform,user,token_hash) VALUES(?,?,?,?,?)').run('device','Device','win32','user','hash');
|
||
|
|
const device=(await f.api('/api/admin/devices/device/inspection','PATCH',{expectedVersion:1,documentAnalysisEnabled:false})).body;f.store.db.prepare('UPDATE devices SET revoked=1').run();assert.equal((await f.approve(device)).status,409);assert.equal(f.store.get('policy').version,1);
|
||
|
|
});
|