make(Kernel::class)->bootstrap(); $config = DB::connection('pgsql')->getConfig(); unset($config['name']); if ($config['host'] !== '127.0.0.1') { throw new LogicException('Provas apenas no PostgreSQL local isolado.'); } config(['database.connections.ir_proof_admin' => [...$config, 'url' => null, 'database' => 'postgres']]); $admin = DB::connection('ir_proof_admin'); $suffix = bin2hex(random_bytes(6)); $database = 'budget_core_sgf_'.$suffix; $role = 'ir_proof_'.$suffix; $password = bin2hex(random_bytes(32)); $work = sys_get_temp_dir().'/sgc-ir-proof-'.$suffix; mkdir($work, 0700); mkdir($work.'/sessions', 0700); mkdir($work.'/cache', 0700); $host = realpath(dirname(__DIR__).'/../sgf-tempo'); $candidate = getenv('IR_CANDIDATE_ROOT') ?: dirname(__DIR__); $before = json_decode(file_get_contents($host.'/packages/purchasing/current.json'), true, 32, JSON_THROW_ON_ERROR); $created = false; $roleCreated = false; $restore = null; $restoreCreated = false; $sourceDatabase = null; $sourceCreated = false; $exitCode = 0; try { if (! preg_match('/^budget_core_sgf_[a-f0-9]{12}$/D', $database) || ! preg_match('/^ir_proof_[a-f0-9]{12}$/D', $role)) { throw new LogicException('Identidade descartável inválida.'); } $admin->statement('CREATE ROLE "'.$role.'" LOGIN NOSUPERUSER NOCREATEDB NOCREATEROLE PASSWORD '.$admin->getPdo()->quote($password)); $roleCreated = true; $admin->statement('CREATE DATABASE "'.$database.'" OWNER "'.$role.'"'); $created = true; $env = ['APP_ENV' => 'budget-testing', 'APP_DEBUG' => 'false', 'APP_KEY' => 'base64:'.base64_encode(random_bytes(32)), 'APP_URL' => 'http://127.0.0.1', 'DB_CONNECTION' => 'pgsql', 'DB_URL' => '', 'DB_HOST' => $config['host'], 'DB_PORT' => (string) $config['port'], 'DB_DATABASE' => $database, 'DB_USERNAME' => $role, 'DB_PASSWORD' => $password, 'SESSION_DRIVER' => 'file', 'SESSION_FILES' => $work.'/sessions', 'SESSION_COOKIE' => 'sgf_ir_'.$suffix, 'CACHE_STORE' => 'array', 'QUEUE_CONNECTION' => 'sync', 'MAIL_MAILER' => 'array', 'BCRYPT_ROUNDS' => '4', 'IR_SGF_HOST' => $host, 'IR_CANDIDATE_ROOT' => $candidate, 'IR_FAULT_FILE' => $work.'/fault.flag', 'IR_WORK_DIR' => $work]; // The project wrapper is the authorised entry to the pinned host PHP. $process = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $host.'/scripts/sgf.ps1', 'php', $candidate.'/scripts/internal-requisitions-sgf-proof.php'], $host, $env, null, 120); $process->run(); if (! $process->isSuccessful()) { echo $process->getOutput(); echo $process->getErrorOutput(); throw new RuntimeException('Prova SGF da candidata falhou.'); } try { $proof = json_decode($process->getOutput(), true, 64, JSON_THROW_ON_ERROR); } catch (JsonException $e) { file_put_contents($work.'/failure.txt', $process->getOutput().$process->getErrorOutput()); if (preg_match('/Candidate proof failed: [^\r\n]+/', $process->getOutput(), $diagnostic)) { echo $diagnostic[0]."\n"; } else { echo 'Saída da prova não é JSON; diagnóstico privado em '.$work.'/failure.txt'."\n"; } throw new RuntimeException('A prova filha não concluiu as verificações.'); } $after = json_decode(file_get_contents($host.'/packages/purchasing/current.json'), true, 32, JSON_THROW_ON_ERROR); if ($before !== $after || $after['version'] !== '0.1.1') { throw new RuntimeException('A instalação activa não devia mudar.'); } $qa = [...$config, 'url' => null, 'database' => $database, 'username' => $role, 'password' => $password]; config(['database.connections.ir_proof_read' => $qa]); $db = DB::connection('ir_proof_read'); $tables = array_map(fn ($r) => $r->tablename, $db->select("select tablename from pg_tables where schemaname='public' order by tablename")); $fingerprint = function ($connection) use ($tables) { $out = []; foreach ($tables as $table) { $quoted = '"'.str_replace('"', '""', $table).'"'; $rows = $connection->select('select to_jsonb(t)::text as value from '.$quoted.' t order by to_jsonb(t)::text'); $out[$table] = hash('sha256', json_encode(array_column($rows, 'value'), JSON_THROW_ON_ERROR)); } return $out; }; $baseline = $fingerprint($db); $dump = $work.'/recovery.dump'; $bin = 'C:/Program Files/PostgreSQL/17/bin/'; $pgEnv = ['PGHOST' => $config['host'], 'PGPORT' => (string) $config['port'], 'PGUSER' => $role, 'PGPASSWORD' => $password]; $backup = new Process([$bin.'pg_dump.exe', '--format=custom', '--file='.$dump, '--dbname='.$database], null, $pgEnv, null, 60); $backup->mustRun(); $restore = 'budget_core_sgf_'.bin2hex(random_bytes(6)); $admin->statement('CREATE DATABASE "'.$restore.'" OWNER "'.$role.'"'); $restoreCreated = true; $restoreProcess = new Process([$bin.'pg_restore.exe', '--no-owner', '--no-privileges', '--exit-on-error', '--dbname='.$restore, $dump], null, $pgEnv, null, 60); $restoreProcess->mustRun(); config(['database.connections.ir_proof_restore' => [...$qa, 'database' => $restore]]); $restored = DB::connection('ir_proof_restore'); if ($fingerprint($restored) !== $baseline) { throw new RuntimeException('Dados restaurados diferentes da prova.'); } $readRestore = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $host.'/scripts/sgf.ps1', 'php', $candidate.'/tests/Support/internal-requisition-sgf-restored.php'], $host, [...$env, 'DB_DATABASE' => $restore], null, 30); $readRestore->mustRun(); $restoredResult = json_decode($readRestore->getOutput(), true, 32, JSON_THROW_ON_ERROR); if ($fingerprint($restored) !== $baseline) { throw new RuntimeException('Consulta alterou os dados restaurados.'); } $schemaSignature = function ($connection) { return json_encode([ $connection->select("select table_name,column_name,data_type,character_maximum_length,is_nullable,ordinal_position from information_schema.columns where table_schema='public' order by table_name,ordinal_position"), $connection->select("select c.relname,t.tgname,pg_get_triggerdef(t.oid) as definition from pg_trigger t join pg_class c on c.oid=t.tgrelid join pg_namespace n on n.oid=c.relnamespace where n.nspname='public' and not t.tgisinternal order by c.relname,t.tgname"), $connection->select("select c.relname,k.conname,pg_get_constraintdef(k.oid) as definition from pg_constraint k join pg_class c on c.oid=k.conrelid join pg_namespace n on n.oid=c.relnamespace where n.nspname='public' order by c.relname,k.conname"), ], JSON_THROW_ON_ERROR); }; if ($schemaSignature($db) !== $schemaSignature($restored)) { file_put_contents($work.'/schema-before.json', $schemaSignature($db)); file_put_contents($work.'/schema-after.json', $schemaSignature($restored)); echo 'Schema diagnostic: '.$work."\n"; throw new RuntimeException('Schema restaurado diferente da prova.'); } $proof['data_schema_recovery'] = ['backup_sha256' => hash_file('sha256', $dump), 'tables_reconciled' => count($tables), 'restore_verified' => true, 'schema_constraints_and_triggers_verified' => true, 'restored_consultation' => $restoredResult, 'restores_new_operations_automatically' => false, 'separate_from_code_rollback' => true]; $sourceDatabase = 'compras_receipts_test_'.bin2hex(random_bytes(6)); $admin->statement('CREATE DATABASE "'.$sourceDatabase.'" OWNER "'.$role.'"'); $sourceCreated = true; $sourceEnv = ['APP_ENV' => 'testing', 'DB_CONNECTION' => 'pgsql', 'DB_URL' => '', 'DB_HOST' => $config['host'], 'DB_PORT' => (string) $config['port'], 'DB_DATABASE' => $sourceDatabase, 'DB_USERNAME' => $role, 'DB_PASSWORD' => $password]; $sourceTest = new Process([PHP_BINARY, 'vendor/bin/pest', '--compact', 'tests/Feature/InternalRequisitionCandidateTest.php', 'tests/Feature/ProcurementTest.php', 'tests/Feature/BudgetTest.php'], dirname(__DIR__), $sourceEnv, null, 120); $sourceTest->mustRun(); $proof['source_postgres_tests'] = ['passed' => true, 'output' => trim($sourceTest->getOutput())]; $proof['checked_at'] = gmdate('c'); $proof['isolated_database_removed_in_finally'] = true; $proof['active_destination_version'] = $after['version']; $proof['candidate_root'] = $candidate; file_put_contents(dirname(__DIR__).'/docs/proofs/internal-requisitions-sgf.json', json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)); echo json_encode($proof, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR)."\n"; } catch (Throwable $error) { fwrite(STDERR, $error->getMessage()."\n"); $exitCode = 1; } finally { if ($sourceCreated) { $admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname=? and pid<>pg_backend_pid()', [$sourceDatabase]); $admin->statement('DROP DATABASE "'.$sourceDatabase.'"'); } if ($restoreCreated) { DB::purge('ir_proof_restore'); $admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname=? and pid<>pg_backend_pid()', [$restore]); $admin->statement('DROP DATABASE "'.$restore.'"'); } DB::purge('ir_proof_read'); if ($created) { $admin->select('select pg_terminate_backend(pid) from pg_stat_activity where datname=? and pid<>pg_backend_pid()', [$database]); $admin->statement('DROP DATABASE "'.$database.'"'); } if ($roleCreated) { $admin->statement('DROP ROLE "'.$role.'"'); } // Only the generated workspace is eligible for cleanup; retained on proof failure for diagnosis. } exit($exitCode);