49 lines
5.8 KiB
JavaScript
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);
|
|
}
|