Files
Investidor-V2/tools/portal-country-c110-prepare.py
T
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

113 lines
8.5 KiB
Python

"""Prepare an isolated current-source copy; do not change any live application file."""
from pathlib import Path
from datetime import datetime, timezone
import hashlib, json, shutil, subprocess
ROOT = Path(__file__).resolve().parent.parent
PRIVATE = ROOT / 'storage/app/private/investmoz-v2/portal-audit'
STAMP = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
WORK = PRIVATE / ('country-c110-source-' + STAMP)
OUT = ROOT / 'outputs/investmoz-country02-c110-20261011'
OUT.mkdir(exist_ok=True)
sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
baseline = {}
for directory in ['app','config','routes','resources','database','bootstrap','tests','tools']:
for p in (ROOT / directory).rglob('*'):
if not p.is_file() or 'cache' in p.relative_to(ROOT / directory).parts:
continue
rel = p.relative_to(ROOT)
target = WORK / rel
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(p, target)
baseline[rel.as_posix()] = sha(p)
for name in ['artisan','composer.json','composer.lock','package.json','package-lock.json','tsconfig.json','vite.config.ts','phpunit.xml','phpstan.neon']:
p = ROOT / name
if p.is_file():
shutil.copy2(p, WORK / name)
baseline[name] = sha(p)
for folder in ['bootstrap/cache','public','storage/framework/views','storage/framework/cache','storage/framework/sessions','storage/logs','storage/app/private/investmoz-v2/portal-audit']:
(WORK / folder).mkdir(parents=True, exist_ok=True)
for name in ['packages.php','services.php']:
p = ROOT / 'bootstrap/cache' / name
if p.is_file(): shutil.copy2(p, WORK / 'bootstrap/cache' / name)
for name in ['vendor','node_modules']:
subprocess.run(['powershell','-NoProfile','-Command',f"New-Item -ItemType Junction -Path '{WORK / name}' -Target '{ROOT / name}' | Out-Null"],check=True)
controller = WORK / 'app/Http/Controllers/OpportunityResourceController.php'
text = controller.read_text(encoding='utf-8')
text = text.replace('use Illuminate\\Support\\Facades\\URL;', 'use Illuminate\\Support\\Facades\\URL;\nuse Illuminate\\Support\\Str;')
old = " $plan = [...$service->preview($request->user()), 'actionUrl' => '/management/opportunity-resources-install/organization-country', 'extension' => true];"
new = """ $plan = $service->preview($request->user());
$scope = $this->countryOperationScope($request, $plan);
$operation = $request->session()->get('portal.country_installation_operation');
if (! is_array($operation) || ($operation['scope'] ?? null) !== $scope) {
$operation = ['scope' => $scope, 'key' => (string) Str::uuid()];
$request->session()->put('portal.country_installation_operation', $operation);
}
$plan = [...$plan, 'operationKey' => $operation['key'], 'actionUrl' => '/management/opportunity-resources-install/organization-country', 'extension' => true];"""
assert text.count(old) == 1
text = text.replace(old, new)
old = ' $result = $service->apply($request->user(), $request->all());'
new = """ // Preview authorizes the actor; the UUID remains stable until this scope changes or logout.
$plan = $service->preview($request->user());
$operation = $request->session()->get('portal.country_installation_operation');
$data = $request->validate(['idempotency_key' => 'required|uuid']);
abort_unless(is_array($operation) && ($operation['scope'] ?? null) === $this->countryOperationScope($request, $plan)
&& hash_equals($operation['key'], $data['idempotency_key']), 409, 'A operação não corresponde a esta prévia. Atualize a página antes de tentar novamente.');
$result = $service->apply($request->user(), $request->all());"""
assert text.count(old) == 1
text = text.replace(old, new)
anchor = ' public function index(int $id, OpportunityResources $service): Response'
method = """ /** @param array<string, mixed> $plan */
private function countryOperationScope(Request $request, array $plan): string
{
return hash('sha256', json_encode([$request->user()->getAuthIdentifier(), $plan['target'], $plan['migration'], $plan['code_hash']], JSON_THROW_ON_ERROR));
}
"""
assert text.count(anchor) == 1
controller.write_text(text.replace(anchor, method+anchor),encoding='utf-8')
page = WORK / 'resources/js/pages/management/opportunity-install.tsx'
text = page.read_text(encoding='utf-8')
text = text.replace(' extension = false,',' extension = false,\n operationKey,')
text = text.replace(' extension?: boolean;',' extension?: boolean;\n operationKey?: string;')
assert text.count('idempotency_key: crypto.randomUUID(),') == 1
text = text.replace('idempotency_key: crypto.randomUUID(),', "idempotency_key: operationKey ?? '',")
text = text.replace(' !form.data.confirm ||', ' !form.data.confirm ||\n (extension && !operationKey) ||')
text = text.replace(' {!ready && (', """ {extension && !operationKey && (
<p role="alert">Atualize a prévia para obter a chave desta operação.</p>
)}
{!ready && (""")
page.write_text(text,encoding='utf-8')
test = WORK / 'tests/Feature/OrganizationCountryInstallationTest.php'
text = test.read_text(encoding='utf-8')
text = text.replace(" return ['hash' => $this->getJson('/management/opportunity-resources-install/organization-country')->assertOk()->json('hash'),\n 'confirm' => true, 'idempotency_key' => (string) Str::uuid(),", " $preview = $this->getJson('/management/opportunity-resources-install/organization-country')->assertOk()->json();\n\n return ['hash' => $preview['hash'],\n 'confirm' => true, 'idempotency_key' => $preview['operationKey'],")
anchor = ' public function test_missing_mfa_other_user_and_partial_schema_are_denied(): void'
extra = """ public function test_operation_uuid_survives_errors_reload_and_cannot_be_replaced(): void
{
$this->login($this->actor());
$this->readiness(true);
$input = $this->input();
$this->assertTrue(Str::isUuid($input['idempotency_key']));
$this->postJson('/management/opportunity-resources-install/organization-country', [...$input, 'reason' => 'short'])->assertUnprocessable();
$this->assertSame($input, $this->input());
$this->postJson('/management/opportunity-resources-install/organization-country', [...$input, 'idempotency_key' => (string) Str::uuid()])->assertConflict();
$this->assertSame($input, $this->input());
$this->assertSame(0, DB::table('audit_events')->where('source_system', 'organization-country-installation')->count());
$this->postJson('/management/opportunity-resources-install/organization-country', $input)->assertOk();
$this->postJson('/management/opportunity-resources-install/organization-country', $input)->assertOk();
$this->assertSame(1, DB::table('audit_events')->where('source_system', 'organization-country-installation')->count());
}
"""
assert anchor in text
test.write_text(text.replace(anchor,extra+anchor),encoding='utf-8')
(WORK / 'qa-autoload.php').write_text("<?php\n$loader = require __DIR__.'/vendor/autoload.php';\n$loader->setPsr4('App\\\\', __DIR__.'/app');\n$loader->setPsr4('Tests\\\\', __DIR__.'/tests');\nreturn $loader;\n",encoding='utf-8')
phpunit = WORK / 'phpunit.xml'
phpunit.write_text(phpunit.read_text(encoding='utf-8').replace('bootstrap="vendor/autoload.php"','bootstrap="qa-autoload.php"'),encoding='utf-8')
changed = [name for name,expected in baseline.items() if sha(WORK / name) != expected]
assert set(changed) == {'app/Http/Controllers/OpportunityResourceController.php','resources/js/pages/management/opportunity-install.tsx','tests/Feature/OrganizationCountryInstallationTest.php','phpunit.xml'}
assert all(sha(ROOT / name) == h for name,h in baseline.items()), 'Live source changed'
plan = {'at':datetime.now(timezone.utc).isoformat(),'root':str(ROOT),'source':str(WORK),'output':str(OUT),'changed':changed,'baseline_live_sources':baseline,'live_application_modified':False,'active_database_writes':0,'key_source':'Server Str::uuid; actor+target+migration+revision-bound session, stable across failure/reload/retry','other_shared_consumers':'020000/030000 form does not consume idempotency_key; accepts absence and has no browser randomUUID call after patch'}
(PRIVATE / 'country-c110-source-latest.json').write_text(json.dumps(plan,indent=2),encoding='utf-8')
print(json.dumps({k:plan[k] for k in ['source','output','changed','live_application_modified','active_database_writes']}))