Files
SGF/scripts/payable-page-http.mjs
T
2026-11-04 09:37:20 +02:00

31 lines
6.0 KiB
JavaScript

import fs from 'node:fs';
import path from 'node:path';
import net from 'node:net';
import {spawn} from 'node:child_process';
import assert from 'node:assert/strict';
import crypto from 'node:crypto';
const state=JSON.parse(fs.readFileSync('C:/laragon/data/sgf-tempo/run/modular-regression-current.json','utf8')),tag=process.argv[2];
if(state.port!==55439||!/^[a-f0-9]{12}$/.test(tag||''))throw Error('Owned QA tag required');
const root=path.join(state.root,'private/payable-history-'+tag),settings=path.join(root,'settings.json'),config=JSON.parse(fs.readFileSync(settings,'utf8'));
if(config.tag!==tag||config.cluster!=='sgf_modular_regression_'+state.id)throw Error('Wrong owned host');
await new Promise((resolve,reject)=>{const probe=net.createServer();probe.once('error',()=>reject(Error('Loopback proof port occupied')));probe.listen(8047,'127.0.0.1',()=>probe.close(resolve));});
const log=fs.openSync(path.join(root,'page-http-server.log'),'a'),child=spawn('C:/laragon/tools/sgf-tempo/php-8.4.27/php.exe',['-S','127.0.0.1:8047',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 checks=0;const check=(v,why)=>{checks++;assert(v,why);};
const client=()=>{const jar=new Map();let auth;const request=async(url,{body,headers:extra={}}={})=>{const headers={Accept:'application/json','X-SGF-Context':auth?.context??'',Cookie:[...jar].map(([k,v])=>k+'='+v).join('; '),...extra};if(body){headers['Content-Type']='application/json';headers['X-CSRF-TOKEN']=auth?.csrf??'';headers['Idempotency-Key']=crypto.randomUUID();}const r=await fetch('http://127.0.0.1:8047'+url,{method:body?'POST':'GET',headers,body:body?JSON.stringify(body):undefined,redirect:'manual',signal:AbortSignal.timeout(8000)});for(const c of r.headers.getSetCookie()){const pair=c.split(';')[0],at=pair.indexOf('=');jar.set(pair.slice(0,at),pair.slice(at+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,enter:async role=>{const r=await request('/qa/enter/'+role);if(r.status!==200)throw Error('QA entry failed');auth=r.data;return auth;}};};
try {
const reader=client();let ready=false;for(let n=0;n<50&&!ready;n++){try{await reader.enter('read');ready=true;}catch{await new Promise(r=>setTimeout(r,100));}}check(ready,'Owned host ready');
const first=await reader.request('/api/supplier-payables');check(first.status===200,'Page endpoint available');const a=first.data,volume=a.pagination.documents.total;check(volume>=1000&&a.documents.length===25&&a.obligations.length===25,'Default source and position window bounded');check(a.proposals.length===25&&a.events.length===25,'Evidence windows bounded');
const second=await reader.request('/api/supplier-payables?page=2');check(second.status===200&&second.data.documents.length===25,'Second source page');const keys=new Set(a.documents.map(d=>d.local_id));check(!second.data.documents.some(d=>keys.has(d.local_id)),'Disjoint pages');check(a.documents.at(-1).local_id>second.data.documents[0].local_id,'Stable descending order');
const large=await reader.request('/api/supplier-payables?per_page=100');check(large.status===200&&large.data.documents.length===100&&large.data.events.length===100,'100 row server page');
const events=await reader.request('/api/supplier-payables?event_page=2');check(events.status===200&&!events.data.events.some(e=>a.events.some(old=>old.id===e.id)),'Independent disjoint event window');check(events.data.documents[0].local_id===a.documents[0].local_id,'Event pager preserves source window');
const one=await reader.request('/api/supplier-payables?q=BENCH-00002');check(one.status===200&&one.data.pagination.documents.total===1&&one.data.documents.length===1,'Number filter before pagination');
const missing=await reader.request('/api/supplier-payables?recognition=unrecognised');check(missing.status===200&&missing.data.pagination.documents.total===0&&missing.data.events.length===0,'Recognition filter does not expose other records');
for(const query of ['page=0','per_page=1000','event_page=0','recognition=paid','company_id=999','q%5Ba%5D=x'])check((await reader.request('/api/supplier-payables?'+query)).status===422,'Unsupported/invalid filters refused');
check((await reader.request('/api/supplier-payables',{headers:{'X-SGF-Context':'obsolete'}})).status===409,'Stale context refused on reads');
check((await reader.request('/api/supplier-payables/prepare_history',{body:{id:a.documents[0].local_id,reason:'Reader must not acquire write rights from a page.'}})).status===403,'Read page grants no write access');
const html=await reader.request('/supplier-payables?q=BENCH&recognition=recognised&per_page=25');check(html.status===200&&html.text.includes('Filtrar documentos')&&html.text.includes('Paginação documentos')&&html.text.includes('Paginação histórico'),'Existing HTML has server filters and pagers');check(html.text.includes('q=BENCH&amp;recognition=recognised'),'Pager preserves search/recognition');
const injection=await reader.request('/supplier-payables?q='+encodeURIComponent('<script>page_injection</script>'));check(injection.status===200&&!injection.text.includes('<script>page_injection</script>')&&injection.text.includes('&lt;script&gt;page_injection&lt;/script&gt;'),'Search text escaped in HTML');
check(first.headers.get('cache-control').includes('no-store'),'Private response');
const report={kind:'financial_pagination_actual_http',checks,passed:true,volume,default_page:25,larger_page:100,filter_before_count:true,source_and_evidence_pages_disjoint:true,context_and_rights_enforced:true,unknown_parameters_refused:true,html_escaping:true,visual_review:false,browser_interactions_tested:false};fs.writeFileSync(path.join(root,'page-http.json'),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);}