Files
SGF/scripts/budget-workspace-http-proof.mjs
2026-10-29 18:43:29 +02:00

59 lines
7.2 KiB
JavaScript

import fs from 'node:fs/promises';
import assert from 'node:assert/strict';
import {randomUUID,createHash} from 'node:crypto';
const fixture=JSON.parse(await fs.readFile('C:/laragon/data/sgf-tempo/secrets/budget-proof-access.json','utf8'));
const base='http://127.0.0.1:8004', checks=[];
function client(){const cookies=new Map();let csrf='',context='';return {
async request(path,method='GET',body,options={}){
const r=await fetch(base+path,{method,headers:{Accept:'application/json','Content-Type':'application/json',Cookie:[...cookies].map(([k,v])=>`${k}=${v}`).join('; '),'X-CSRF-TOKEN':options.noCsrf?'':csrf,'X-SGF-Context':options.stale?'stale-context':context,...(options.key?{'Idempotency-Key':options.key}:{})},...(body===undefined?{}:{body:JSON.stringify(body)})});
for(const c of r.headers.getSetCookie()){const pair=c.split(';',1)[0],i=pair.indexOf('=');cookies.set(pair.slice(0,i),pair.slice(i+1));}
const data=await r.json().catch(()=>({}));if(data.csrf)csrf=data.csrf;if(data.context?.token)context=data.context.token;
return {status:r.status,data};
},
async login(role){assert.equal((await this.request('/api/session')).status,200);const a=fixture.accounts[role];assert.equal((await this.request('/api/login','POST',{email:a.email,password:a.password})).status,200);assert.equal((await this.request('/api/context','POST',{tenant_id:fixture.tenant,company_id:fixture.company,unit_id:fixture.unit})).status,200);},
async send(action,input,key=randomUUID(),expected=200){const r=await this.request('/api/purchasing-budget/'+action,'POST',input,{key});assert.equal(r.status,expected,action+': '+(r.data.message??''));return r.data;},
async list(){const r=await this.request('/api/purchasing-budget');assert.equal(r.status,200);return r.data;}
};}
const anonymous=client();assert.equal((await anonymous.request('/api/purchasing-budget')).status,401);
const p=client(),v=client(),o=client(),q=client();
await p.login('preparacao');await v.login('validacao');await o.login('operacao');await q.login('consulta');
const initial=await q.list();assert.equal(initial.budgets.length,0);checks.push('four_real_sessions_and_empty_initial_demo');
const allocation={cost_center_id:fixture.centre,year:initial.current_year,kind:'operating',includes_tax:true,amount:'1000.00',reason:'Dotação exclusivamente sintética para prova HTTP'};
const key=randomUUID(),created=await p.send('create',allocation,key);assert.equal((await p.send('create',allocation,key)).replayed,true);
await p.send('create',{...allocation,amount:'1001.00'},key,409);await q.send('create',allocation,randomUUID(),403);
await p.send('approve',{budget_id:created.id},randomUUID(),403);await v.send('approve',{budget_id:created.id});
const snapshots=[];
for(const [kind,amount,reserved,committed] of [['reserve','300.00',30000,0],['award','350.00',35000,0],['commit','350.00',0,35000]]){
const input={origin_id:fixture.origin,kind,total:amount,subtotal:amount},k=randomUUID();
await o.send('movement',input,k);assert.equal((await o.send('movement',input,k)).replayed,true);
const data=await q.list(),b=data.budgets.find(x=>x.id===created.id);assert.equal(b.balance.reserved_minor,reserved);assert.equal(b.balance.committed_minor,committed);snapshots.push({step:kind,balance:b.balance});
if(kind==='award')await o.send('movement',{origin_id:fixture.release_origin,kind:'reserve',total:'700.00',subtotal:'700.00'},randomUUID(),422);
}
await o.send('movement',{origin_id:fixture.origin,kind:'release',total:'0.00',subtotal:'0.00'},randomUUID(),409);
await o.send('movement',{origin_id:fixture.origin,kind:'reserve',total:'301.00',subtotal:'301.00'},randomUUID(),422);
await o.send('consume',{origin_id:fixture.origin,reference:'SIM-CONSUMO-HTTP',total:'100.001',subtotal:'100.00'},randomUUID(),422);
const consumption={origin_id:fixture.origin,reference:'SIM-CONSUMO-HTTP',total:'100.00',subtotal:'100.00'};
await o.send('consume',{...consumption,total:'400.00',subtotal:'400.00'},randomUUID(),422);
assert.equal((await q.list()).consumption_sources.length,0,'Failed consumption left a synthetic reference behind.');
await o.send('consume',consumption);await o.send('consume',consumption);
let data=await q.list(),b=data.budgets.find(x=>x.id===created.id);
assert.equal(data.entries.length,4);assert.equal(data.consumptions.length,1);assert.equal(data.consumption_sources.length,1);assert.equal(b.balance.committed_minor,25000);assert.equal(b.balance.consumed_minor,10000);assert.equal(b.balance.available_minor,65000);snapshots.push({step:'consume',balance:b.balance});
await o.send('consume',{...consumption,total:'101.00'},randomUUID(),409);
const proposed=await p.send('reinforcement',{budget_id:created.id,amount:'200.00',reference:'SIM-REFORCO-HTTP',reason:'Reforço sintético HTTP'});
assert.equal((await q.list()).budgets[0].balance.available_minor,65000);
await p.send('review',{reinforcement_id:proposed.id,decision:'approved',reason:'Tentativa sem direito'},randomUUID(),403);
await v.send('review',{reinforcement_id:proposed.id,decision:'approved',reason:'Revisão sintética distinta por HTTP'});
data=await q.list();assert.equal(data.budgets[0].balance.available_minor,85000);snapshots.push({step:'reinforcement',balance:data.budgets[0].balance});
for(const kind of ['reserve','release'])await o.send('movement',{origin_id:fixture.release_origin,kind,total:'200.00',subtotal:'200.00'});
assert.equal((await q.list()).budgets[0].balance.available_minor,85000);
await o.send('movement',{origin_id:fixture.release_origin,kind:'commit',total:'200.00',subtotal:'200.00'},randomUUID(),409);
await o.send('movement',{origin_id:999999,kind:'reserve',total:'1.00',subtotal:'1.00'},randomUUID(),403);
assert.equal((await o.request('/api/purchasing-budget/movement','POST',{origin_id:fixture.origin,kind:'reserve',total:'300.00',subtotal:'300.00'},{key:randomUUID(),stale:true})).status,409);
assert.equal((await o.request('/api/purchasing-budget/movement','POST',{}, {key:randomUUID(),noCsrf:true})).status,419);
assert.equal((await q.request('/api/context','POST',{tenant_id:fixture.tenant,company_id:fixture.company,unit_id:fixture.unit-1})).status,403);
checks.push('allocation_distinct_approval','same_command_recovery','changed_key_conflict','reservation_award_commit_equivalent','insufficient_balance','committed_release_refused','decimal_precision_rejected','failed_consumption_rollback_and_recovery','single_consumption_per_reference','pending_reinforcement_no_effect','distinct_reinforcement_review','release_once_no_reopening','foreign_origin_refused','stale_context_refused','csrf_enabled','reserved_unit_refused','reader_cannot_write');
for(const c of [p,v,o,q])assert.equal((await c.request('/api/logout','POST')).status,200);
const source=await fs.readFile('packages/budget-control/src/BudgetControl.php');
await fs.writeFile('docs/proofs/budget-workspace-http.json',JSON.stringify({version:'SGF-BUDGET-WORKSPACE-SIM-v1',checked_at:new Date().toISOString(),passed:true,core_sha256:createHash('sha256').update(source).digest('hex'),checks,snapshots,visual_review:false,team_demo_changed:false},null,2)+'\n');
console.log('Budget workspace HTTP proof passed: four functions, exact balances, traceable effects, no duplicate consumption, isolation and CSRF. Team demo unchanged.');