113 lines
8.5 KiB
Python
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']}))
|