122 lines
8.0 KiB
Python
122 lines
8.0 KiB
Python
"""Rehearse the exact fixed ZIP on a private QA source tree, then restore its base."""
|
|
from pathlib import Path
|
|
from datetime import datetime, timezone
|
|
import hashlib, json, shutil, subprocess, zipfile
|
|
|
|
R = Path(__file__).resolve().parent.parent
|
|
P = R / 'storage/app/private/investmoz-v2/portal-audit'
|
|
O = R / 'outputs/investmoz-internal-designation-20261011'
|
|
load = lambda p: json.loads(p.read_text(encoding='utf-8-sig'))
|
|
sha = lambda p: hashlib.sha256(p.read_bytes()).hexdigest()
|
|
m = load(O / 'manifesto.json')
|
|
qa = load(P / 'internal-designation-plan.json')
|
|
source = Path(qa['source'])
|
|
assert qa['target']['port'] == 55441 and source.parent == P
|
|
stamp = datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S')
|
|
stage = P / ('internal-designation-rehearsal-' + stamp)
|
|
assert not stage.exists() and stage.parent == P
|
|
stage.mkdir()
|
|
for folder in ['app', 'config', 'routes', 'resources', 'bootstrap']:
|
|
shutil.copytree(R / folder, stage / folder)
|
|
shutil.copytree(R / 'public/build', stage / 'public/build')
|
|
for file in ['.env', 'qa-autoload.php', 'artisan', 'public/index.php']:
|
|
(stage / file).parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copyfile(source / file, stage / file)
|
|
for folder in ['storage/app/private', 'storage/logs', 'storage/framework/cache/data', 'storage/framework/sessions', 'storage/framework/views', 'bootstrap/cache']:
|
|
(stage / folder).mkdir(parents=True, exist_ok=True)
|
|
assert not list((stage / 'bootstrap/cache').glob('config.php'))
|
|
shutil.copytree(R / 'database', stage / 'database')
|
|
for file in ['composer.json', 'composer.lock']:
|
|
shutil.copyfile(R / file, stage / file)
|
|
stage_vendor = str(stage / 'vendor').replace("'", "''")
|
|
root_vendor = str(R / 'vendor').replace("'", "''")
|
|
subprocess.run(['powershell.exe', '-NoProfile', '-Command', f"New-Item -ItemType Junction -Path '{stage_vendor}' -Target '{root_vendor}' | Out-Null"], capture_output=True, check=True)
|
|
env = stage / '.env'
|
|
text = env.read_text(encoding='utf-8')
|
|
assert text.count('PORTAL_PROFILE_ADMINISTRATION_ENABLED=true') == 1
|
|
env.write_text(text.replace('PORTAL_PROFILE_ADMINISTRATION_ENABLED=true','PORTAL_PROFILE_ADMINISTRATION_ENABLED=false'), encoding='utf-8')
|
|
old_env_hash = sha(env)
|
|
before = {x['path']: sha(stage / x['path']) if (stage / x['path']).is_file() else None for x in m['files']}
|
|
assert before == {x['path']: x['before_sha256'] for x in m['files']}
|
|
original_bytes = {p: (stage / p).read_bytes() for p, v in before.items() if v is not None}
|
|
old_manifest = (stage / 'public/build/manifest.json').read_bytes()
|
|
php = 'C:/laragon/bin/php/php-8.5.11-Win32-vs17-x64/php.exe'
|
|
probe = stage / 'release-probe.php'
|
|
probe.write_text('''<?php
|
|
ini_set('zend.exception_ignore_args','1');
|
|
require __DIR__.'/qa-autoload.php';
|
|
$app=require __DIR__.'/bootstrap/app.php';
|
|
$app->make(Illuminate\\Contracts\\Console\\Kernel::class)->bootstrap();
|
|
$c=config('database.connections.'.config('database.default'));
|
|
throw_unless((int)$c['port']===55441 && str_starts_with($c['database'],'onboarding_') && str_ends_with($c['database'],'_test'),RuntimeException::class,'QA required.');
|
|
DB::connection()->getPdo()->exec('BEGIN READ ONLY');
|
|
$a=app(App\\Services\\PortalProfileAccess::class);
|
|
$new=method_exists($a,'governanceAvailable');
|
|
$flags=['administration'=>(bool)config('portal.profile_administration_enabled'),'operations'=>(bool)config('portal.profile_access_enabled'),'country'=>(bool)config('portal.organization_country_enabled')];
|
|
$proof=['stage'=>$argv[1],'flags'=>$flags,'new_code'=>$new,'installed'=>$new?$a->installed():null,'operations_available'=>$a->available(),
|
|
'governance_available'=>$new?$a->governanceAvailable():null,'config_cached'=>$app->configurationIsCached(),
|
|
'middleware_loadable'=>$new?class_exists(App\\Http\\Middleware\\EnsurePortalProfileAdministration::class):false,
|
|
'source_file'=>(new ReflectionClass($a))->getFileName(), 'port'=>(int)$c['port'], 'database'=>$c['database']];
|
|
$proof['operations_middleware']=array_values(array_filter(app('router')->getRoutes()->getByAction(App\\Http\\Controllers\\PortalProfileController::class.'@opportunities')->gatherMiddleware(),fn($v)=>str_contains($v,'EnsurePortal')));
|
|
if($new)$proof['administration_middleware']=array_values(array_filter(app('router')->getRoutes()->getByAction(App\\Http\\Controllers\\PortalProfileController::class.'@administration')->gatherMiddleware(),fn($v)=>str_contains($v,'EnsurePortal')));
|
|
DB::connection()->getPdo()->exec('ROLLBACK');
|
|
echo json_encode($proof,JSON_THROW_ON_ERROR).PHP_EOL;
|
|
''', encoding='utf-8')
|
|
def inspect(label):
|
|
r = subprocess.run([php,str(probe),label],cwd=stage,capture_output=True,text=True,encoding='utf-8',timeout=30)
|
|
if r.returncode:
|
|
(P / ('internal-designation-rehearsal-' + label + '-error.log')).write_text(r.stderr,encoding='utf-8')
|
|
raise RuntimeError('QA rehearsal failed; private diagnostic retained')
|
|
v=json.loads(r.stdout)
|
|
assert str(stage) in v['source_file'] and v['port']==55441 and not v['operations_available'] and v['flags']['country']
|
|
return v
|
|
proofs=[inspect('before')]
|
|
archive=O/m['package']
|
|
assert sha(archive)==m['package_sha256']
|
|
expected={x['path']:x['after_sha256'] for x in m['files']}|{x['path']:x['sha256'] for x in m['assets']}
|
|
def apply():
|
|
with zipfile.ZipFile(archive) as z:
|
|
assert set(z.namelist())==set(expected)
|
|
# Hashed resources first, switch the manifest last; retain previous hashed assets.
|
|
for relative in sorted(expected,key=lambda p:(p=='public/build/manifest.json',p)):
|
|
assert '..' not in Path(relative).parts and not Path(relative).is_absolute()
|
|
dest=stage/relative
|
|
assert dest.resolve().is_relative_to(stage.resolve())
|
|
data=z.read(relative)
|
|
assert hashlib.sha256(data).hexdigest()==expected[relative]
|
|
dest.parent.mkdir(parents=True,exist_ok=True)
|
|
dest.write_bytes(data)
|
|
assert all(sha(stage/p)==h for p,h in expected.items()) and sha(env)==old_env_hash
|
|
apply()
|
|
proofs.append(inspect('code_with_flag_off'))
|
|
apply()
|
|
proofs.append(inspect('same_zip_reapplied_without_data_actions'))
|
|
text=env.read_text(encoding='utf-8')
|
|
env.write_text(text.replace('PORTAL_PROFILE_ADMINISTRATION_ENABLED=false','PORTAL_PROFILE_ADMINISTRATION_ENABLED=true'),encoding='utf-8')
|
|
proofs.append(inspect('administration_only_on'))
|
|
assert proofs[-1]['governance_available'] and not proofs[-1]['flags']['operations'] and proofs[-1]['middleware_loadable']
|
|
assert any('EnsurePortalProfileAdministration' in v for v in proofs[-1]['administration_middleware'])
|
|
assert any(v.endswith('EnsurePortalProfiles') for v in proofs[-1]['operations_middleware'])
|
|
# Do not add configuration caching to the destination: its current state is uncached.
|
|
text=env.read_text(encoding='utf-8')
|
|
env.write_text(text.replace('PORTAL_PROFILE_ADMINISTRATION_ENABLED=true','PORTAL_PROFILE_ADMINISTRATION_ENABLED=false'),encoding='utf-8')
|
|
proofs.append(inspect('containment_flag_off'))
|
|
assert not proofs[-1]['governance_available']
|
|
for relative in before:
|
|
dest=stage/relative
|
|
if before[relative] is None:
|
|
assert dest.resolve().is_relative_to(stage.resolve())
|
|
dest.unlink()
|
|
else:
|
|
dest.write_bytes(original_bytes[relative])
|
|
(stage/'public/build/manifest.json').write_bytes(old_manifest)
|
|
proofs.append(inspect('base_code_and_manifest_restored'))
|
|
assert sha(env)==old_env_hash and before=={p:sha(stage/p) if (stage/p).is_file() else None for p in before}
|
|
result={'result':'passed','at':datetime.now(timezone.utc).isoformat(),'delta':m['delta_sha256'],
|
|
'package_sha256':sha(archive),'QA_only':True,'active_application_changes':0,'active_database_writes':0,
|
|
'rehearsal':proofs,'base_restored':True,'configuration_state_preserved':'uncached',
|
|
'hashed_assets_retained_for_existing_tabs':True,'credentials_or_secrets_in_evidence':False,
|
|
'HTTP_visual_criteria_reused':'atribuicoes-qa-navegador.json; no A/B or C37 rerun'}
|
|
(O/'ensaio-disponibilizacao-reversao.json').write_text(json.dumps(result,indent=2,ensure_ascii=False),encoding='utf-8')
|
|
print(json.dumps({k:result[k] for k in ['result','delta','QA_only','active_application_changes','active_database_writes','base_restored']}))
|