Files
SGF/scripts/requisition-admin-ui-check.mjs
T
2026-11-02 20:29:34 +02:00

49 lines
5.6 KiB
JavaScript

import fs from 'node:fs';import path from 'node:path';import crypto from 'node:crypto';
import assert from 'node:assert/strict';import React from 'react';import {renderToStaticMarkup} from 'react-dom/server';import {createServer} from 'vite';
const candidate=fs.realpathSync(process.argv[2]||'');
const app=fs.readFileSync(path.join(candidate,'resources/js/app.jsx'),'utf8');
const component=app.slice(app.indexOf('function AccessPanel('),app.indexOf("createRoot(document.getElementById('app'))"));
assert(component.startsWith('function AccessPanel('));
const work=path.join('C:/laragon/data/sgf-tempo/run','admin-render-'+crypto.randomBytes(6).toString('hex'));
fs.mkdirSync(work,{recursive:true});fs.symlinkSync(path.join(process.cwd(),'node_modules'),path.join(work,'node_modules'),'junction');
// Fixture-inject only the server response into the real candidate component.
// Event handlers, permission filtering, labels and form markup are unchanged.
const fixture=component.replace('function AccessPanel({ context, can, api, perform, busy })','export default function AccessPanel({ context, can, api, perform, busy, initialList, initialScope })')
.replace('const [list, setList] = useState(null)','const [list, setList] = useState(initialList)')
.replace("const [scope, setScope] = useState('company')","const [scope, setScope] = useState(initialScope)");
fs.writeFileSync(path.join(work,'AccessPanel.jsx'),"import React,{useState,useEffect} from 'react';\nconst labels={'unit.view':'Consultar unidade','company.view':'Consultar empresa'};\n"+fixture);
const server=await createServer({root:work,configFile:false,server:{middlewareMode:true,fs:{allow:[work,process.cwd()]}},appType:'custom'});
const checks=[];const check=(value,name)=>{assert(value,name);checks.push(name);};
try{
const {default:Access}=await server.ssrLoadModule(path.join(work,'AccessPanel.jsx'));
const unit=7,context={token:'synthetic',company:{id:3},unit:{id:unit},units:[{id:unit,name:'Unidade sintética'}]};
const rights=['unit.view','unit.edit','purchasing_requisitions.view','purchasing_requisitions.prepare','purchasing_requisitions.approve.department_head','purchasing_budget.create','purchasing_budget.approve'];
const list={members:[{id:1,name:'Sintético',email:'example@example.test'}],grants:[{id:1,user_id:1,permission:'purchasing_requisitions.prepare',unit_id:unit}],catalogue:{company:['company.view','access.manage'],unit:rights},catalogue_labels:{'purchasing_requisitions.prepare':'Preparar e submeter requisições','purchasing_requisitions.approve.department_head':'Aprovar como responsável de departamento','purchasing_budget.create':'Preparar dotações','purchasing_budget.approve':'Aprovar dotações'}};
const render=(granted,initialList=list,initialScope=String(unit))=>renderToStaticMarkup(React.createElement(Access,{context,initialList,initialScope,can:(p,u=null)=>granted.some(g=>g[0]===p&&g[1]===u),api:async()=>{},perform(){},busy:false}));
const preparer=render([['unit.view',unit],['purchasing_requisitions.prepare',unit],['purchasing_budget.create',unit]]);
check(preparer.includes('value="purchasing_requisitions.prepare"'),'native selector offers server catalogue requisition right');
check(preparer.includes('Preparar e submeter requisições'),'native selector uses server labels');
check(preparer.includes('Preparar dotações'),'budget creation right has server label');
check(!preparer.includes('value="purchasing_budget.approve"'),'preparer cannot delegate budget approval');
check(!preparer.includes('value="purchasing_requisitions.approve.department_head"'),'preparer cannot delegate absent decision authority');
const reviewer=render([['unit.view',unit],['purchasing_budget.approve',unit]]);
check(reviewer.includes('value="purchasing_budget.approve"'),'reviewer sees only own approval grant');
check(!reviewer.includes('value="purchasing_budget.create"'),'reviewer cannot delegate preparation');
const foreign=render([['purchasing_requisitions.prepare',99]]);
check(!foreign.includes('value="purchasing_requisitions.prepare"'),'foreign unit grant never becomes a local option');
const reduced=render([['purchasing_requisitions.prepare',unit]],{...list,catalogue:{company:[],unit:['unit.view']}});
check(!reduced.includes('value="purchasing_requisitions.prepare"'),'client cannot invent a catalogue entry from a grant');
const waiting=render([['purchasing_requisitions.prepare',unit]],null);
check(!waiting.includes('value="purchasing_requisitions.prepare"'),'no hardcoded new rights before server catalogue loads');
check(preparer.includes('<td>Preparar e submeter requisições</td>'),'existing grants render their server labels');
check(app.includes("allGroups.filter(g=>['start','admin'].includes(g.id))"),'general navigation remains separate');
const home=fs.readFileSync(path.join(candidate,'resources/js/PurchasingHome.jsx'),'utf8');
check(home.includes("can('purchasing_requisitions.view',unit)")&&home.includes('/purchasing/internal-requisitions'),'module entry links to requisition only under own right');
const manifest=JSON.parse(fs.readFileSync(path.join(candidate,'public/build/manifest.json'),'utf8'));
for(const chunk of Object.values(manifest))for(const file of [chunk.file,...(chunk.css||[])]){
check(file.startsWith('modules/internal-requisitions/assets/'),'frontend asset bound to installed bundle');
check(fs.existsSync(path.join(candidate,'integrations/sgf/frontend/assets',path.basename(file))),'referenced compiled dependency included');
}
console.log(JSON.stringify({passed:true,checks,browser_used:false,source_host_modified:false},null,2));
}finally{await server.close();}