Files
SGF/scripts/payable-history-sgf-http.mjs
2026-11-02 20:29:34 +02:00

49 lines
5.8 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import net from 'node:net';
import crypto from 'node:crypto';
import assert from 'node:assert/strict';
import {spawn} from 'node:child_process';
const read=p=>JSON.parse(fs.readFileSync(p,'utf8'));
const state=read('C:/laragon/data/sgf-tempo/run/modular-regression-current.json'),tag=process.argv[2];
if(state.port!==55439||!/^[a-f0-9]{12}$/.test(tag||''))throw Error('Wrong QA context');
const root=path.join(state.root,'private/payable-history-'+tag),settings=path.join(root,'settings.json');
const owned=read(settings);if(owned.tag!==tag||owned.cluster!=='sgf_modular_regression_'+state.id)throw Error('Wrong owned fixture');
const port=8047;
await new Promise((resolve,reject)=>{const probe=net.createServer();probe.once('error',()=>reject(Error('QA HTTP port occupied; no process stopped')));probe.listen(port,'127.0.0.1',()=>probe.close(resolve));});
const log=fs.openSync(path.join(root,'http-server.log'),'a');
const child=spawn('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['-S','127.0.0.1:'+port,path.resolve('scripts/payable-history-sgf-http-router.php')],{cwd:process.cwd(),env:{...process.env,PAYABLE_HISTORY_QA_SETTINGS:settings},windowsHide:true,stdio:['ignore',log,log]});
let assertions=0;const check=(value,message)=>{assertions++;assert(value,message);};
const client=()=>{const cookies=new Map();let auth;
const request=async(url,body,options={})=>{
const headers={Accept:'application/json',Cookie:[...cookies].map(([k,v])=>k+'='+v).join('; '),'X-SGF-Context':auth?.context??''};
if(body){headers['Content-Type']='application/json';headers['X-CSRF-TOKEN']=auth?.csrf??'';headers['X-SGF-Context']=auth?.context??'';headers['Idempotency-Key']=options.key??crypto.randomUUID();}
Object.assign(headers,options.headers??{});
const r=await fetch('http://127.0.0.1:'+port+url,{method:body?'POST':'GET',headers,body:body?JSON.stringify(body):undefined,redirect:'manual',signal:AbortSignal.timeout(8000)});
for(const cookie of r.headers.getSetCookie()){const pair=cookie.split(';')[0],i=pair.indexOf('=');cookies.set(pair.slice(0,i),pair.slice(i+1));}
const text=await r.text();let data;try{data=JSON.parse(text);}catch{}return {status:r.status,data,text,headers:r.headers};
};
return {request,login:async role=>{const r=await request('/qa/enter/'+role);check(r.status===200,'QA role entry');auth=r.data;return auth;}};
};
try {
const preparer=client();let ready=false;for(let n=0;n<50&&!ready;n++){try{await preparer.login('prepare');ready=true;}catch{await new Promise(resolve=>setTimeout(resolve,100));}}check(ready,'Owned HTTP host ready');
const readOnly=client(),validator=client();const actor=await readOnly.login('read');await validator.login('validate');
const listing=await readOnly.request('/api/supplier-payables');check(listing.status===200,'Authenticated history listing: '+listing.status+' '+(listing.data?.message??''));check(listing.data.history_import_available===true&&listing.data.history_decisions_available===false,'History linked without pending-payment decisions');
const o=listing.data.obligations[0];check(o.principal_minor===6000&&o.balance_minor===3000&&o.pending_payments_minor===1000&&o.rejected_payments_minor===500,'Same authoritative monetary position');check(listing.headers.get('cache-control').includes('no-store'),'Private finance response');
const body={id:actor.source_id,reason:'Synthetic HTTP historical source review.'};
check((await readOnly.request('/api/supplier-payables/prepare_history',body)).status===403,'Reader cannot prepare');
check((await preparer.request('/api/supplier-payables/prepare_history',body,{headers:{'X-SGF-Context':'obsolete'}})).status===409,'Obsolete context refused');
check((await preparer.request('/api/supplier-payables/prepare_history',body,{headers:{'X-CSRF-TOKEN':''}})).status===419,'Missing CSRF refused');
check((await preparer.request('/api/supplier-payables/prepare_history',{...body,company_id:999})).status===422,'Context injection refused');
const key=crypto.randomUUID();const first=await preparer.request('/api/supplier-payables/prepare_history',body,{key});check(first.status===200&&first.data.state==='recognised','HTTP reuses recognised historical origin');
const again=await preparer.request('/api/supplier-payables/prepare_history',body,{key});check(again.status===200&&again.data.replayed===true,'HTTP response loss can replay original');
const page=await readOnly.request('/supplier-payables');check(page.status===200&&page.text.includes('Saldo por liquidar: 30.00 MZN')&&page.text.includes('sem abatimento'),'Existing HTML displays original history and balance');check(page.text.includes('Não há leitura histórica em tempo real nem passagem operacional de responsabilidade validada.'),'Receipt and operational cutover distinguished visibly');
const final=await validator.request('/api/supplier-payables');check(final.status===200&&final.data.obligations.length===1&&final.data.obligations[0].balance_minor===3000,'No second debt after HTTP replay');
const report={kind:'sgf_historical_payable_actual_http_kernel',assertions,passed:true,existing_html_reused:true,csrf_and_context_enforced:true,reader_write_refused:true,no_pending_payment_decision_route_added:true,one_obligation_same_balance_after_replay:true,visual_review:false,browser_interactions_tested:false};
const output=path.join(root,'http-proof.json');if(fs.existsSync(output)){const prior=fs.readFileSync(output),archive=output+'.'+crypto.createHash('sha256').update(prior).digest('hex')+'.archive';if(!fs.existsSync(archive))fs.writeFileSync(archive,prior,{flag:'wx'});}fs.writeFileSync(output,JSON.stringify(report,null,2)+'\n');console.log(JSON.stringify(report));
} finally {
if(child.exitCode===null){child.kill();await new Promise(resolve=>child.once('exit',resolve));}
fs.closeSync(log);
}