Files
safetynet/tests/file-upload-gate.test.mjs
T

52 lines
5.4 KiB
JavaScript
Raw Normal View History

import test from 'node:test';
import assert from 'node:assert/strict';
import {createRequire} from 'node:module';
const {createFileUploadGate}=createRequire(import.meta.url)('../apps/browser/file-upload-gate.cjs');
function fixture(inspectPart=async()=>({decision:'allow',coverage:{complete:true}})){
let current=true,alive=true,releases=0,acquires=0,lost;
const aborted=[];const context={};
const gate=createFileUploadGate({maxBytes:()=>100,isCurrent:()=>current,abort:id=>aborted.push(id),inspectPart,
createLease:async({paths,onLost})=>{acquires++;lost=onLost;return {files:paths.map(path=>({path,bytes:Buffer.from('file')})),isAlive:()=>alive,release:async()=>{releases++;}};}});
const details={id:1,url:'https://example.test/upload',uploadData:[{type:'rawData',bytes:Buffer.from('prefix')},{type:'file',file:'C:/synthetic',offset:0,length:-1},{type:'rawData',bytes:Buffer.from('suffix')}]};
return {gate,context,details,aborted,get releases(){return releases;},get acquires(){return acquires;},change(){current=false;},lose(){alive=false;lost();}};
}
test('approved full-file multipart holds leases through terminal and returns hash-free facts',async()=>{
const f=fixture();const result=await f.gate.inspect(f.details,f.context);assert.equal(result.decision,'allow');assert.deepEqual(result.request,{bytes:16,detectedType:'unknown'});assert.equal(f.releases,0);
assert.deepEqual(await f.gate.inspect(f.details,f.context),result);assert.equal(f.acquires,1);await f.gate.terminal(1);assert.equal(f.releases,1);
});
test('denied scan aborts but retains lock until native terminal',async()=>{
const f=fixture(async()=>({decision:'block',ruleId:'secret'}));assert.equal((await f.gate.inspect(f.details,f.context)).ruleId,'secret');assert.deepEqual(f.aborted,[1]);assert.equal(f.releases,0);await f.gate.terminal(1);assert.equal(f.releases,1);
});
test('ranges and data-pipe shapes fail before lease acquisition',async()=>{
for(const part of [{type:'file',file:'C:/synthetic',offset:1,length:-1},{type:'blob',blobUUID:'x'},{type:'rawData',bytes:Buffer.from('x'),file:'C:/x'}]){
const f=fixture();f.details.uploadData=[part];assert.equal((await f.gate.inspect(f.details,f.context)).decision,'block');assert.equal(f.acquires,0);await f.gate.terminal(1);
}
});
test('joined raw segments detect boundary split and never produce generic request hash',async()=>{
const f=fixture(async bytes=>bytes.toString()==='prefixsuffix'?{decision:'block',ruleId:'joined'}:{decision:'allow',coverage:{complete:true}});
assert.equal((await f.gate.inspect(f.details,f.context)).ruleId,'joined');await f.gate.terminal(1);
});
test('policy change during scan and helper loss retain lease until terminal',async()=>{
let f;f=fixture(async()=>{f.change();return {decision:'allow',coverage:{complete:true}};});assert.equal((await f.gate.inspect(f.details,f.context)).decision,'block');assert.equal(f.releases,0);await f.gate.terminal(1);
const g=fixture();await g.gate.inspect(g.details,g.context);g.lose();assert.deepEqual(g.aborted,[1]);assert.equal(g.releases,0);await g.gate.terminal(1);
});
test('missing complete coverage denies and concurrency includes denied pending leases',async()=>{
const f=fixture(async()=>({decision:'allow'}));for(let id=1;id<=8;id++)assert.equal((await f.gate.inspect({...f.details,id},f.context)).decision,'block');assert.equal((await f.gate.inspect({...f.details,id:9},f.context)).ruleId,'upload-capacity');f.gate.dispose();assert.equal(f.releases,0);for(let id=1;id<=8;id++)await f.gate.terminal(id);assert.equal(f.releases,8);
});
test('same native id cannot reuse approval for a changed body or method',async()=>{
const f=fixture();await f.gate.inspect(f.details,f.context);f.details.uploadData[0].bytes[0]=88;
assert.equal((await f.gate.inspect(f.details,f.context)).ruleId,'request-changed');assert.equal(f.releases,0);await f.gate.terminal(1);
const g=fixture();await g.gate.inspect(g.details,g.context);assert.equal((await g.gate.inspect({...g.details,method:'PUT'},g.context)).ruleId,'request-changed');await g.gate.terminal(1);
});
test('raw snapshots precede asynchronous acquisition and file scan receives exact path',async()=>{
const raw=Buffer.from('original'),seen=[];let released=0;
const gate=createFileUploadGate({maxBytes:()=>100,isCurrent:()=>true,abort:()=>{},
createLease:async({paths})=>{raw.fill(88);return {files:[{path:paths[0],bytes:Buffer.from('file')}],isAlive:()=>true,release:async()=>{released++;}};},
inspectPart:async(bytes,options)=>{seen.push({text:bytes.toString(),filename:options.filename});return {decision:'allow',coverage:{complete:true}};}});
assert.equal((await gate.inspect({id:1,url:'https://example.test',method:'POST',uploadData:[{type:'rawData',bytes:raw},{type:'file',file:'C:/specific.xls',offset:0,length:-1}]},{})).decision,'allow');
assert.equal(seen[0].text,'original');assert.equal(seen[1].filename,'C:/specific.xls');assert.equal(seen[2].text,'original');await gate.terminal(1);assert.equal(released,1);
});
test('terminal lease cleanup failures propagate without pretending success',async()=>{
const gate=createFileUploadGate({maxBytes:()=>100,isCurrent:()=>true,abort:()=>{},createLease:async({paths})=>({files:[{path:paths[0],bytes:Buffer.from('file')}],isAlive:()=>true,release:async()=>{throw Error('cleanup-failed');}}),inspectPart:async()=>({decision:'allow',coverage:{complete:true}})});
await gate.inspect({id:1,url:'https://example.test',uploadData:[{type:'file',file:'C:/specific.xls',offset:0,length:-1}]},{});await assert.rejects(gate.terminal(1),/cleanup-failed/);
});