21 lines
5.6 KiB
PHP
21 lines
5.6 KiB
PHP
<?php
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
$checks=[];$ok=function($v,$name)use(&$checks){if(!$v)throw new RuntimeException($name);$checks[]=$name;};
|
|
$source=DB::connection('stock_valuation_source');$fingerprint=function()use($source){$h=hash_init('sha256');foreach(['stock_movements','gl_journals','gl_lines','gl_stock_books','gl_stock_rates','gl_stock_rate_reviews']as$t){hash_update($h,$t);foreach($source->table($t)->orderBy('id')->cursor()as$r)hash_update($h,json_encode($r,JSON_THROW_ON_ERROR));}return hash_final($h);};$before=$fingerprint();$grants=DB::table('permission_grants')->orderBy('id')->get()->toJson();
|
|
$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,__DIR__.'/stock-ledger-observer-router.php'],dirname(__DIR__),['STOCK_OBSERVER_SETTINGS'=>$s['root'].'/settings.json'],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);}
|
|
$client=new GuzzleHttp\Client(['base_uri'=>'http://127.0.0.1:'.$port,'http_errors'=>false,'timeout'=>15]);$sessions=[];foreach(['reader','limited','foreign-unit','foreign-company']as$role){$jar=new GuzzleHttp\Cookie\CookieJar();$r=$client->get('/qa/stock-observer/'.$role,['cookies'=>$jar]);$ok($r->getStatusCode()===200,'SGF QA session '.$role);$sessions[$role]=['jar'=>$jar]+json_decode((string)$r->getBody(),true,32,JSON_THROW_ON_ERROR);}
|
|
$call=function($role,$url,$extra=[])use($client,&$sessions){$v=$sessions[$role];$r=$client->get($url,['cookies'=>$v['jar'],'headers'=>array_replace(['Accept'=>'application/json','X-SGF-Context'=>$v['context']],$extra)]);$text=(string)$r->getBody();return ['status'=>$r->getStatusCode(),'data'=>json_decode($text,true),'body'=>$text,'cache'=>$r->getHeaderLine('Cache-Control')];};
|
|
$r=$call('reader','/api/stock-valuation-read?per_page=100');$ok($r['status']===200,'SGF reads canonical source with explicit dual rights');$d=$r['data'];$ok($d['physical_quantity_milli']===1500&&$d['posted_stock_value_minor']===7500&&$d['pagination']['total']===3&&$d['status']==='reconciled','SGF sees actual1.5UN and posted75 from same native book');$ok($d['operation_executed']===false&&$d['source_identity']['role']===$s['source']['username']&&count($d['lines'])===6,'read-only canonical source identity and six lines');$ok(str_contains($r['cache'],'no-store'),'SGF response no-store');
|
|
$ok($call('reader','/api/stock-valuation-read?page=2')['data']['journals']===[],'SGF server pagination boundary');$ok($call('reader','/api/stock-valuation-read?per_page=26')['status']===422,'SGF invalid page size refused');$ok($call('reader','/api/stock-valuation-read?tenant_id=999')['status']===422,'SGF protected scope parameter refused');$ok($call('reader','/api/stock-valuation-read', ['X-SGF-Context'=>'stale'])['status']===409,'SGF stale tab refused');
|
|
$ok($call('limited','/api/stock-valuation-read')['status']===403,'general ledger view alone cannot read stock evidence');$ok($call('foreign-unit','/api/stock-valuation-read')['status']===403,'foreign SGF unit refused');$ok($call('foreign-company','/api/stock-valuation-read')['status']===403,'foreign SGF company refused');
|
|
$ok($call('reader','/api/stock-valuation-read',['X-QA-Source'=>'missing'])['status']===503,'missing binding is unavailable not zero');$ok($call('reader','/api/stock-valuation-read',['X-QA-Source'=>'wrong-scope'])['status']===503,'native scope mismatch refused');$ok($call('reader','/api/stock-valuation-read',['X-QA-Source'=>'wrong-role'])['status']===403,'unapproved native role refused');
|
|
$r=$call('reader','/general-ledger/stock-valuation');$ok($r['status']===200&&str_contains($r['body'],'75,00')&&str_contains($r['body'],'1500')&&!str_contains($r['body'],'<form'),'SGF HTML has source facts and no financial command forms');file_put_contents($s['root'].'/observer.html',$r['body']);
|
|
$r=$call('reader','/api/session');$ok($r['status']===200&&$r['data']['context']['accounting']['stock_valuation_read']===true&&$r['data']['context']['purchasing']['internal_requisitions']===false,'SGF snapshot advertises only configured stock observer feature');
|
|
$grant=(array)DB::table('permission_grants')->where($f['scope'])->where(['user_id'=>$f['actor'],'permission'=>'stock.view'])->first();DB::table('permission_grants')->where('id',$grant['id'])->delete();try{$ok($call('reader','/api/stock-valuation-read')['status']===403,'revoked stock view refused on next request');}finally{DB::table('permission_grants')->insert($grant);}
|
|
$error=null;try{$source->delete('DELETE FROM gl_journals WHERE false');}catch(Illuminate\Database\QueryException $e){$error=$e;}$ok($error&&in_array($error->errorInfo[0]??null,['25006','42501'],true),'source SQL role cannot write');
|
|
$ok($before===$fingerprint()&&DB::table('permission_grants')->orderBy('id')->get()->toJson()===$grants&&!DB::getSchemaBuilder()->hasTable('gl_journals')&&!DB::getSchemaBuilder()->hasTable('stock_movements'),'source and SGF grants unchanged; no copied physical or ledger book');
|
|
file_put_contents($s['root'].'/proof.json',json_encode(['passed'=>true,'checks'=>$checks,'actual_sgf_http'=>true,'physical_quantity_milli'=>1500,'posted_stock_minor'=>7500,'source_journals'=>3,'source_lines'=>6,'source_and_grants_unchanged'=>true,'select_only_source'=>true,'no_local_book_copy'=>true,'visual_review'=>false],JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));echo count($checks)." SGF canonical stock valuation observer checks passed.\n";
|
|
}finally{$server->stop(2);file_put_contents($s['root'].'/http.log',$server->getOutput()."\n".$server->getErrorOutput());}
|