Files
SGF/scripts/requisition-composition-navigation.php
2026-11-04 09:37:20 +02:00

25 lines
3.7 KiB
PHP

<?php
use Illuminate\Support\Facades\DB;
$s=json_decode(file_get_contents(getenv('IR_COMPOSITION_STATE')),true,64,JSON_THROW_ON_ERROR);
$name=$argv[1]??'navigation.json';if(!preg_match('/^navigation(?:-[a-z0-9]+)?\.json$/D',$name)||file_exists($s['root'].'/'.$name))throw new LogicException('Fresh navigation output required');
require $s['app'].'/vendor/autoload.php';$app=require $s['app'].'/bootstrap/app.php';$app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
$checks=[];$check=function($v,$name)use(&$checks){if(!$v)throw new RuntimeException($name);$checks[]=$name;};
$fingerprint=function(){ $h=hash_init('sha256');foreach(['ir_requests','ir_commands','ir_events','ir_steps','ir_policies','ir_policy_versions','cb_budgets','cb_budget_entries','cb_budget_consumptions','permission_grants']as$table){hash_update($h,$table);foreach(DB::table($table)->orderBy('id')->cursor()as$row)hash_update($h,json_encode($row,JSON_THROW_ON_ERROR));}return hash_final($h);};$before=$fingerprint();
foreach([true,false]as$enabled){
$socket=stream_socket_server('tcp://127.0.0.1:0');$port=(int)substr(strrchr(stream_socket_get_name($socket,false),':'),1);fclose($socket);
$server=new Symfony\Component\Process\Process([PHP_BINARY,'-S','127.0.0.1:'.$port,dirname(__DIR__).'/scripts/requisition-composition-http.php'],$s['app'],['SGF_REQUISITIONS_QA_ENABLED'=>$enabled?'true':'false'],null,60);$server->start();
try{for($i=0;$i<80;$i++){if($sock=@fsockopen('127.0.0.1',$port,$e,$m,.1)){fclose($sock);break;}usleep(50000);}
$jar=new GuzzleHttp\Cookie\CookieJar();$client=new GuzzleHttp\Client(['base_uri'=>'http://127.0.0.1:'.$port,'http_errors'=>false,'timeout'=>15,'cookies'=>$jar]);
$entry=$client->get('/qa/enter/validator');$check($entry->getStatusCode()===200,'navigation QA entry '.(int)$enabled);$context=json_decode((string)$entry->getBody(),true,32,JSON_THROW_ON_ERROR);
$snapshot=$client->get('/api/session');$body=json_decode((string)$snapshot->getBody(),true,32,JSON_THROW_ON_ERROR);
$check($snapshot->getStatusCode()===200&&$body['context']['purchasing']['internal_requisitions']===$enabled,'server capability matches mounted composition '.(int)$enabled);
$page=$client->get('/purchasing/internal-requisitions');$check($page->getStatusCode()===($enabled?200:404),'screen mounted only in enabled QA '.(int)$enabled);
if($enabled){$check(str_contains((string)$page->getBody(),'data-sgf-module-back'),'canonical screen returns to SGF navigation');$check($client->get('/api/internal-requisitions',['headers'=>['Accept'=>'application/json','X-SGF-Context'=>$context['context'],'X-Purchasing-Context'=>'procurement.internal']])->getStatusCode()===200,'current reviewer reads after UI integration');
$check($client->post('/api/internal-requisitions/save',['headers'=>['Accept'=>'application/json','X-SGF-Context'=>$context['context'],'X-Purchasing-Context'=>'procurement.internal'],'json'=>[]])->getStatusCode()===419,'mounted mutation keeps CSRF');
$anonymous=new GuzzleHttp\Client(['base_uri'=>'http://127.0.0.1:'.$port,'http_errors'=>false,'timeout'=>15]);$check($anonymous->get('/api/internal-requisitions',['headers'=>['Accept'=>'application/json']])->getStatusCode()===401,'mounted API refuses anonymous session');}
}finally{$server->stop(2);file_put_contents($s['root'].'/'.$name.'-http-'.(int)$enabled.'.txt',$server->getOutput()."\n".$server->getErrorOutput());}}
$check($fingerprint()===$before,'navigation and default-off probes preserve business and rights fingerprints');
file_put_contents($s['root'].'/'.$name,json_encode(['passed'=>true,'checks'=>$checks,'actual_http'=>true,'business_and_rights_fingerprint_unchanged'=>true,'visual_review'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo count($checks)." read-only navigation checks passed.\n";