273 lines
21 KiB
PHP
273 lines
21 KiB
PHP
<?php
|
|
|
|
use App\Models\User;
|
|
use App\Providers\AppServiceProvider;
|
|
use GuzzleHttp\Client;
|
|
use GuzzleHttp\Cookie\CookieJar;
|
|
use Illuminate\Contracts\Console\Kernel;
|
|
use Illuminate\Database\QueryException;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\Process\Process;
|
|
use Tempo\Purchasing\Delivery\CodeDeployment;
|
|
use Tempo\Purchasing\Delivery\Installation;
|
|
use Tempo\Purchasing\Delivery\SqlPrivileges;
|
|
|
|
$original = getenv('FIX_SOURCE_HOST');
|
|
$clone = getenv('FIX_CLONE');
|
|
$root = getenv('FIX_ROOT');
|
|
require $clone.'/vendor/autoload.php';
|
|
$app = require $clone.'/bootstrap/app.php';
|
|
$app->make(Kernel::class)->bootstrap();
|
|
$bundle = getenv('FIX_BUNDLE');
|
|
$work = getenv('FIX_WORK');
|
|
$runtime = getenv('FIX_RUNTIME');
|
|
$runtimePassword = getenv('FIX_RUNTIME_PASSWORD');
|
|
require $bundle.'/integrations/sgf/operational/bootstrap.php';
|
|
require $bundle.'/integrations/sgf/operational/CodeDeployment.php';
|
|
$inventory = json_decode(file_get_contents($work.'/baseline.json'), true, 64, JSON_THROW_ON_ERROR);
|
|
$db = DB::connection();
|
|
if (! preg_match('/^sgf_reproduction_([a-f0-9]{12})$/D', $db->getDatabaseName(), $identity) || $runtime !== 'fix_app_'.$identity[1] || $db->selectOne('select current_user as name')->name !== 'fix_owner_'.$identity[1]) {
|
|
throw new RuntimeException('Disposable reproduction identity required before any test grant or write.');
|
|
}
|
|
$checks = [];
|
|
$check = function ($ok, $label) use (&$checks) {
|
|
if (! $ok) {
|
|
throw new RuntimeException($label);
|
|
}$checks[] = $label;
|
|
};
|
|
$tables = array_column($db->select("select tablename from pg_tables where schemaname='public' order by tablename"), 'tablename');
|
|
$check($tables === $inventory['tables'], 'reproduced current 87-table schema without source rows');
|
|
$check(DB::table('users')->count() === 0 && DB::table('permission_grants')->count() === 0, 'no accounts or grants imported from destination');
|
|
$q = fn ($name) => '"'.str_replace('"', '""', $name).'"';
|
|
$db->statement('GRANT USAGE ON SCHEMA public TO '.$q($runtime));
|
|
foreach ($inventory['runtime_table_acl'] as $table => $rights) {
|
|
if ($rights) {
|
|
$db->statement('GRANT '.implode(', ', $rights).' ON TABLE public.'.$q($table).' TO '.$q($runtime));
|
|
}
|
|
if (in_array('INSERT', $rights, true) && $db->getSchemaBuilder()->hasColumn($table, 'id')) {
|
|
$seq = $db->selectOne('select pg_get_serial_sequence(?,?) as name', ['public.'.$table, 'id'])->name;
|
|
if ($seq) {
|
|
$db->statement('GRANT USAGE ON SEQUENCE '.$seq.' TO '.$q($runtime));
|
|
}
|
|
}
|
|
}
|
|
$db->statement('REVOKE ALL ON ALL FUNCTIONS IN SCHEMA public FROM PUBLIC');
|
|
foreach ($inventory['runtime_functions'] as $function) {
|
|
$db->statement('GRANT EXECUTE ON FUNCTION '.$function.' TO '.$q($runtime));
|
|
}
|
|
config(['database.connections.fixed_runtime' => [...$db->getConfig(), 'url' => null, 'username' => $runtime, 'password' => $runtimePassword]]);
|
|
$rt = DB::connection('fixed_runtime');
|
|
$check(! $rt->selectOne("select has_schema_privilege(current_user,'public','CREATE') as allowed")->allowed, 'runtime cannot create schema objects');
|
|
$now = now();
|
|
$tenant = DB::table('tenants')->insertGetId(['name' => 'Empresa sintética da reprodução', 'created_at' => $now, 'updated_at' => $now]);
|
|
$company = DB::table('companies')->insertGetId(['tenant_id' => $tenant, 'name' => 'Horizonte sintético', 'created_at' => $now, 'updated_at' => $now]);
|
|
$unit = DB::table('units')->insertGetId(['tenant_id' => $tenant, 'company_id' => $company, 'name' => 'Unidade sintética', 'created_at' => $now, 'updated_at' => $now]);
|
|
$password = bin2hex(random_bytes(24));
|
|
$actor = User::create(['name' => 'Preparador sintético', 'email' => 'prepare-'.$unit.'@example.test', 'password' => $password])->id;
|
|
$head = User::create(['name' => 'Aprovador sintético', 'email' => 'approve-'.$unit.'@example.test', 'password' => $password])->id;
|
|
foreach ([$actor, $head] as $id) {
|
|
DB::table('company_memberships')->insert(['user_id' => $id, 'tenant_id' => $tenant, 'company_id' => $company]);
|
|
foreach (['company.view' => null, 'unit.view' => $unit] as $right => $u) {
|
|
DB::table('permission_grants')->insert(['user_id' => $id, 'tenant_id' => $tenant, 'company_id' => $company, 'unit_id' => $u, 'permission' => $right, 'created_at' => $now, 'updated_at' => $now]);
|
|
}
|
|
}
|
|
DB::table('permission_grants')->insert(['user_id' => $actor, 'tenant_id' => $tenant, 'company_id' => $company, 'unit_id' => null, 'permission' => 'access.manage', 'created_at' => $now, 'updated_at' => $now]);
|
|
$security = function () use ($db) {
|
|
$out = [];
|
|
foreach (['users', 'company_memberships', 'permission_grants', 'identity_audit'] as $table) {
|
|
$out[$table] = hash('sha256', json_encode($db->select('select to_jsonb(t)::text as data from "'.$table.'" t order by to_jsonb(t)::text'), JSON_THROW_ON_ERROR));
|
|
}
|
|
|
|
return $out;
|
|
};
|
|
$before = $security();
|
|
$target = Installation::target($db);
|
|
$fault = true;
|
|
DB::listen(function ($query) use (&$fault) {
|
|
if ($fault && str_contains($query->sql, 'create table "cb_ui_commands"')) {
|
|
$fault = false;
|
|
throw new RuntimeException('Injected corrected migration failure');
|
|
}
|
|
});
|
|
try {
|
|
Installation::migrate($db, $target, str_repeat('a', 64), 'Isolated migrator');
|
|
throw new LogicException('Expected migration failure');
|
|
} catch (RuntimeException$error) {
|
|
$check($error->getMessage() === 'Injected corrected migration failure', 'injected dependency migration failure');
|
|
}
|
|
$check(! $db->getSchemaBuilder()->hasTable('ir_requests') && ! $db->getSchemaBuilder()->hasTable('cb_ui_commands') && $security() === $before, 'failed migration restores original schema and identity data');
|
|
Installation::migrate($db, $target, str_repeat('a', 64), 'Isolated migrator');
|
|
$check($security() === $before, 'successful migration preserves existing identity and grants');
|
|
$check(DB::table('cb_centres')->count() === 0 && DB::table('cb_budgets')->count() === 0, 'installation creates empty centres and allocations');
|
|
try {
|
|
$rt->table('sgc_installations')->count();
|
|
throw new LogicException('Unexpected module access');
|
|
} catch (QueryException$error) {
|
|
$check($error->errorInfo[0] === '42501', 'runtime module SQL access denied before test-only privilege plan');
|
|
}
|
|
$privileges = SqlPrivileges::plan($runtime);
|
|
foreach ($privileges['statements'] as $sql) {
|
|
$db->statement($sql);
|
|
}
|
|
$check(! $rt->selectOne("select has_table_privilege(current_user,'ir_events','UPDATE') as allowed")->allowed && ! $rt->selectOne("select has_table_privilege(current_user,'ir_requests','DELETE') as allowed")->allowed, 'minimal runtime excludes historical update and requisition deletion');
|
|
$check($rt->table('sgc_installations')->value('revision') === 2, 'runtime reads corrected revision without ownership');
|
|
$deploy = new CodeDeployment($clone);
|
|
$mh = hash_file('sha256', $bundle.'/manifest.json');
|
|
$plan = $deploy->plan($bundle, $mh);
|
|
$applied = $deploy->apply($bundle, $mh, $plan['target_sha256'], 'Isolated code preparation', true);
|
|
unlink($clone.'/storage/framework/down');
|
|
file_put_contents($clone.'/config/purchasing-operational.php', '<?php return ["enabled"=>true,"target"=>getenv("IR_HTTP_TARGET"),"scopes"=>[getenv("IR_HTTP_SCOPE")]];');
|
|
file_put_contents($clone.'/config/purchasing-access.php', '<?php return ["initialisation_scopes"=>[getenv("IR_HTTP_SCOPE")]];');
|
|
$f = ['actor' => $actor, 'validator' => $head, 'scope' => ['tenant_id' => $tenant, 'company_id' => $company, 'unit_id' => $unit]];
|
|
$scope = $f['scope'];
|
|
$target = Installation::target(DB::connection());
|
|
$scopeKey = implode(':', $scope);
|
|
config(['purchasing-operational.enabled' => true, 'purchasing-operational.target' => $target, 'purchasing-operational.scopes' => [$scopeKey]]);
|
|
$actor = $f['actor'];
|
|
$head = $f['validator'];
|
|
DB::table('permission_grants')->where($scope)->where('user_id', $actor)->where('permission', 'like', 'purchasing_%')->delete();
|
|
$password = bin2hex(random_bytes(24));
|
|
foreach ([$actor, $head] as $id) {
|
|
$u = User::findOrFail($id);
|
|
$u->password = $password;
|
|
$u->save();
|
|
}
|
|
$socket = stream_socket_server('tcp://127.0.0.1:0');
|
|
$address = stream_socket_get_name($socket, false);
|
|
fclose($socket);
|
|
$port = (int) substr(strrchr($address, ':'), 1);
|
|
$server = new Process([PHP_BINARY, '-S', '127.0.0.1:'.$port, '-t', $clone.'/public', $root.'/tests/Support/sgf-composition-router.php'], $clone, ['IR_CLONE' => $clone, 'DB_USERNAME' => $runtime, 'DB_PASSWORD' => $runtimePassword, 'IR_HTTP_ENABLED' => '1', 'IR_HTTP_TARGET' => $target, 'IR_HTTP_SCOPE' => $scopeKey], null, 180);
|
|
$server->start();
|
|
$earlyChecks = $checks;
|
|
$check = function ($ok, $label) use (&$checks) {
|
|
if (! $ok) {
|
|
throw new RuntimeException($label);
|
|
}$checks[] = $label;
|
|
};
|
|
$check(str_starts_with(str_replace('\\', '/', (new ReflectionClass(AppServiceProvider::class))->getFileName()), str_replace('\\', '/', $clone).'/app/'), 'provider really loaded from isolated copy');
|
|
$plan = new Process(['powershell.exe', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $original.'/scripts/sgf.ps1', 'php', $clone.'/packages/internal-requisitions/scripts/sgf-internal-install.php', 'plan', $clone], $clone, null, null, 60);
|
|
$plan->mustRun();
|
|
$check(json_decode($plan->getOutput(), true, 16, JSON_THROW_ON_ERROR)['schema_prepared'], 'new bundle bootstrap accepted by installer integrity check');
|
|
try {
|
|
for ($i = 0; $i < 100; $i++) {
|
|
if (@fsockopen('127.0.0.1', $port)) {
|
|
break;
|
|
}usleep(50000);
|
|
}
|
|
$clients = [];
|
|
$anonymous = new Client(['http_errors' => false]);
|
|
$check($anonymous->get('http://127.0.0.1:'.$port.'/purchasing/internal-requisitions/access-initialisation', ['headers' => ['Accept' => 'application/json']])->getStatusCode() === 401, 'anonymous direct initialisation URL denied');
|
|
$call = function ($id, $path, $method = 'GET', $data = null, $headers = []) use (&$clients, $port) {
|
|
$c = &$clients[$id];
|
|
$opts = ['http_errors' => false, 'headers' => array_merge(['Accept' => 'application/json', 'X-CSRF-TOKEN' => $c['csrf'], 'X-SGF-Context' => $c['token'] ?? '', 'X-Purchasing-Context' => 'procurement.internal'], $headers)];
|
|
if ($data !== null) {
|
|
$opts['json'] = $data;
|
|
}
|
|
$r = $c['http']->request($method, 'http://127.0.0.1:'.$port.$path, $opts);
|
|
$body = (string) $r->getBody();
|
|
|
|
return ['status' => $r->getStatusCode(), 'data' => json_decode($body, true), 'body' => $body];
|
|
};
|
|
foreach ([$actor, $head] as $id) {
|
|
$http = new Client(['cookies' => new CookieJar, 'timeout' => 15]);
|
|
$s = json_decode((string) $http->get('http://127.0.0.1:'.$port.'/api/session', ['headers' => ['Accept' => 'application/json']])->getBody(), true);
|
|
$clients[$id] = ['http' => $http, 'csrf' => $s['csrf']];
|
|
$login = $call($id, '/api/login', 'POST', ['email' => User::findOrFail($id)->email, 'password' => $password]);
|
|
$check($login['status'] === 200, 'native clone authentication '.$id);
|
|
$clients[$id]['csrf'] = $login['data']['csrf'];
|
|
$selected = $call($id, '/api/context', 'POST', $scope);
|
|
$check($selected['status'] === 200, 'native clone context '.$id);
|
|
$clients[$id]['token'] = $selected['data']['context']['token'];
|
|
}
|
|
$page = $call($actor, '/purchasing/internal-requisitions/access-initialisation');
|
|
$check($page['status'] === 200 && str_contains($page['body'], 'Administrador nominativo'), 'provider registers initial authority page without prior functional rights');
|
|
$initial = ['operation_key' => (string) Str::uuid(), 'reason' => 'Entrega sintética através da composição HTTP.', 'administrators' => [['user_id' => $actor, 'rights' => ['purchasing_requisitions.view', 'purchasing_requisitions.prepare', 'purchasing_requisitions.configure', 'purchasing_requisitions.approve.department_head', 'purchasing_budget.record', 'purchasing_budget.view', 'purchasing_budget.create', 'purchasing_budget.approve']]]];
|
|
$check($call($actor, '/api/internal-requisitions/access-initialisation', 'POST', $initial, ['X-CSRF-TOKEN' => 'bad'])['status'] === 419, 'initial authority CSRF enforced');
|
|
$check($call($actor, '/api/internal-requisitions/access-initialisation', 'POST', $initial)['status'] === 200, 'initial authority through normal middleware');
|
|
$check($call($actor, '/purchasing/internal-requisitions')['status'] === 409, 'work page requires explicit module selection');
|
|
$oldToken = $clients[$actor]['token'];
|
|
$selected = $call($actor, '/api/context', 'POST', $scope + ['module_id' => 'purchasing']);
|
|
$check($selected['status'] === 200, 'new requisition view right makes purchasing selectable');
|
|
$clients[$actor]['token'] = $selected['data']['context']['token'];
|
|
$check($call($actor, '/api/internal-requisitions', 'GET', null, ['X-SGF-Context' => $oldToken])['status'] === 409, 'stale module generation refused');
|
|
$check($call($actor, '/purchasing/internal-requisitions')['status'] === 200, 'provider registers operational work page');
|
|
$check($call($actor, '/api/internal-requisitions', 'GET', null, ['X-Purchasing-Context' => 'procurement.third_party'])['status'] === 403, 'different enterprise option refused by operational composition');
|
|
foreach (['purchasing_requisitions.view', 'purchasing_requisitions.approve.department_head', 'purchasing_budget.view', 'purchasing_budget.approve'] as $right) {
|
|
$r = $call($actor, '/api/accesses', 'POST', ['email' => User::findOrFail($head)->email, 'unit_id' => $scope['unit_id'], 'permission' => $right, 'reason' => 'Atribuição sintética explícita.']);
|
|
$check($r['status'] === 200, 'native administrative delegation '.$right);
|
|
}
|
|
$selected = $call($head, '/api/context', 'POST', $scope + ['module_id' => 'purchasing']);
|
|
$clients[$head]['token'] = $selected['data']['context']['token'];
|
|
$check($call($actor, '/purchasing/internal-requisitions/budget-setup')['status'] === 200, 'empty installation exposes authorised centre and budget preparation');
|
|
$centreCommand = ['operation_key' => (string) Str::uuid(), 'code' => 'SINTETICO-REPRO', 'reason' => 'Centro exclusivamente sintético da reprodução isolada.'];
|
|
$centre = $call($actor, '/api/internal-requisitions/centres', 'POST', $centreCommand);
|
|
$check($centre['status'] === 200, 'centre created through authorised UI API with runtime role');
|
|
$f['centre'] = $centre['data']['centre_id'];
|
|
$repeat = $call($actor, '/api/internal-requisitions/centres', 'POST', $centreCommand);
|
|
$check($repeat['status'] === 200 && $repeat['data']['replayed'] && DB::table('cb_centres')->count() === 1, 'centre retries preserve one source');
|
|
$budgetData = ['cost_center_id' => $f['centre'], 'year' => (int) date('Y'), 'kind' => 'operating', 'includes_tax' => true, 'amount' => '10000.00', 'reason' => 'Dotação exclusivamente sintética, sem valores de destino.'];
|
|
$prepared = $call($actor, '/api/purchasing-budget/create', 'POST', $budgetData, ['Idempotency-Key' => (string) Str::uuid()]);
|
|
$check($prepared['status'] === 200, 'existing budget API prepares synthetic allocation');
|
|
$budgetId = DB::table('cb_budgets')->value('id');
|
|
$check($call($actor, '/api/purchasing-budget/approve', 'POST', ['budget_id' => $budgetId], ['Idempotency-Key' => (string) Str::uuid()])['status'] !== 200, 'budget approval requires independent identity');
|
|
$approvedBudget = $call($head, '/api/purchasing-budget/approve', 'POST', ['budget_id' => $budgetId], ['Idempotency-Key' => (string) Str::uuid()]);
|
|
$check($approvedBudget['status'] === 200, 'independent allocation approval uses existing budget kernel');
|
|
$list = $call($actor, '/api/purchasing-budget');
|
|
$check($list['status'] === 200 && count($list['data']['centres']) === 1, 'workspace dependencies available on reproduced schema');
|
|
$check($call($actor, '/api/purchasing-budget/movement', 'POST', [], ['Idempotency-Key' => (string) Str::uuid()])['status'] === 403, 'direct budget movements cannot bypass requisition lifecycle');
|
|
$policy = ['version' => 0, 'reason' => 'Política sintética na composição integrada.', 'enabled' => true, 'budget_required' => true, 'first_limit_minor' => 1000000, 'second_limit_minor' => 2000000, 'approval_includes_tax' => true, 'centres' => [['id' => $f['centre'], 'department_ref' => 'OPER']], 'members' => [['user_id' => $actor, 'department_ref' => 'OPER'], ['user_id' => $head, 'department_ref' => 'OPER']]];
|
|
$check($call($actor, '/api/internal-requisitions/policy', 'POST', $policy)['status'] === 200, 'operational policy configured via HTTP');
|
|
$draft = $call($actor, '/api/internal-requisitions/save', 'POST', ['operation_key' => (string) Str::uuid(), 'cost_center_id' => $f['centre'], 'priority' => 'normal', 'needed_by' => date('Y-m-d', time() + 864000), 'justification' => 'Requisição sintética da composição.', 'items' => [['description' => 'Material', 'category' => 'Consumíveis', 'purchase_type' => 'consumable', 'quantity' => '2.000', 'unit' => 'UN', 'unit_price' => '50.00', 'tax_rate' => '16.00'], ['description'=>'Servi?o sint?tico','category'=>'Servi?os','purchase_type'=>'service','quantity'=>'1.000','unit'=>'UN','unit_price'=>'25.00','tax_rate'=>'0.00'], ['description'=>'Outro sint?tico','category'=>'Outros','purchase_type'=>'other','quantity'=>'1.000','unit'=>'UN','unit_price'=>'10.00','tax_rate'=>'0.00']]]);
|
|
$check($draft['status'] === 200, 'integrated operational draft');
|
|
$id = $draft['data']['id'];
|
|
$submitted = $call($actor, "/api/internal-requisitions/$id/submit", 'POST', ['operation_key' => (string) Str::uuid(), 'version' => 1]);
|
|
$check($submitted['status'] === 200 && DB::table('cb_budget_entries')->where('kind', 'reserve')->count() === 1, 'integrated submit uses existing budget adapter');
|
|
$decision = ['operation_key' => (string) Str::uuid(), 'version' => $submitted['data']['version'], 'decision' => 'approved'];
|
|
$check($call($actor, "/api/internal-requisitions/$id/decide", 'POST', $decision)['status'] === 403, 'integrated independent identity enforced');
|
|
$approved = $call($head, "/api/internal-requisitions/$id/decide", 'POST', $decision);
|
|
$check($approved['status'] === 200 && $approved['data']['status'] === 'approved', 'integrated approval by another identity');
|
|
$view = $call($actor, "/api/internal-requisitions/$id");
|
|
$check($view['status'] === 200 && count($view['data']['history']) === 3, 'integrated authorised consultation and history');
|
|
require __DIR__.'/sgf-admin-candidate-checks.php';
|
|
$revoke = $call($actor, '/api/accesses/revoke', 'POST', ['email' => User::findOrFail($head)->email, 'unit_id' => $scope['unit_id'], 'permission' => 'purchasing_requisitions.view', 'reason' => 'Revogação sintética explícita.']);
|
|
$check($revoke['status'] === 200 && $call($head, "/api/internal-requisitions/$id")['status'] === 403, 'native revocation applies to direct module URL');
|
|
file_put_contents($clone.'/config/modules.php', '<?php return ["contracted"=>[]];');
|
|
// Deployment configuration changes require restarting persistent PHP services.
|
|
$server->stop(1);
|
|
$server->start();
|
|
for ($i = 0; $i < 100; $i++) {
|
|
if (@fsockopen('127.0.0.1', $port)) {
|
|
break;
|
|
}
|
|
usleep(50000);
|
|
}
|
|
$check($call($actor, "/api/internal-requisitions/$id")['status'] === 403, 'uncontracted module refused at server');
|
|
file_put_contents($clone.'/config/modules.php', '<?php return ["contracted"=>["purchasing"]];');
|
|
file_put_contents($clone.'/config/purchasing-operational.php', '<?php return ["enabled"=>false];');
|
|
$server->stop(1);
|
|
$server->start();
|
|
for ($i = 0; $i < 100; $i++) {
|
|
if (@fsockopen('127.0.0.1', $port)) {
|
|
break;
|
|
}usleep(50000);
|
|
}
|
|
$check($call($actor, '/purchasing/internal-requisitions')['status'] === 404, 'disabled composition exposes no executable work page');
|
|
$fingerprints = [];
|
|
foreach (array_column($db->select("select tablename from pg_tables where schemaname='public' order by tablename"), 'tablename') as $table) {
|
|
$rows = $db->select('select to_jsonb(t)::text as data from "'.$table.'" t order by to_jsonb(t)::text');
|
|
$fingerprints[$table] = hash('sha256', json_encode(array_column($rows, 'data'), JSON_THROW_ON_ERROR));
|
|
}
|
|
$backup = new Process(['C:/Program Files/PostgreSQL/17/bin/pg_dump.exe', '--format=custom', '--file='.$work.'/installed.dump', '--dbname='.$db->getDatabaseName()], null, ['PGHOST' => $db->getConfig('host'), 'PGPORT' => (string) $db->getConfig('port'), 'PGUSER' => $db->getConfig('username'), 'PGPASSWORD' => $db->getConfig('password')], null, 60);
|
|
$backup->mustRun();
|
|
file_put_contents($clone.'/storage/framework/down', '{"status":503}');
|
|
$server->stop(1);
|
|
require __DIR__.'/sgf-corrected-update.php';
|
|
$deploy->rollback($applied['deployment_id'], 'Isolated corrected code recovery', true);
|
|
$check(DB::table('ir_requests')->where('id', $id)->value('status') === 'approved' && DB::table('cb_budget_entries')->where('kind', 'reserve')->count() === 1, 'code recovery preserves submitted origin decision and reservation');
|
|
$check(! is_dir($clone.'/packages/internal-requisitions') && is_file($clone.'/storage/framework/down'), 'code recovery leaves baseline and maintenance intact');
|
|
echo json_encode(['passed' => true, 'checks' => $checks, 'clone' => $clone, 'data_fingerprints' => $fingerprints, 'destination_activated' => false, 'normal_provider_routes' => true, 'browser_used' => false], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
|
|
} finally {
|
|
$server->stop(1);
|
|
}
|