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/); });