114 lines
6.9 KiB
Python
114 lines
6.9 KiB
Python
"""Authenticated requests to the guarded QA server; no UI acceptance or active writes."""
|
|
import base64
|
|
import hashlib
|
|
import hmac
|
|
import http.cookiejar
|
|
import json
|
|
import re
|
|
import struct
|
|
import time
|
|
import urllib.error
|
|
import urllib.parse
|
|
import urllib.request
|
|
import uuid
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
from pathlib import Path
|
|
|
|
root = Path(__file__).resolve().parent.parent
|
|
private = root / 'storage/app/private/investmoz-v2/portal-audit'
|
|
backup = json.loads((private.parent / 'opportunity-resources-backup-proof.json').read_text(encoding='utf-8-sig'))
|
|
target = backup['restored_target']
|
|
assert backup['restore_verified'] and re.fullmatch(r'desk_restore_\d{8}_\d{6}_test', target['database']) and int(target['port']) == 55441
|
|
expected = target['database'] + ':55441'
|
|
events = []
|
|
jar = http.cookiejar.CookieJar()
|
|
opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar))
|
|
|
|
|
|
def call(method, path, data=None, status=200, html=False):
|
|
assert path.startswith('/') and not path.startswith('//')
|
|
headers = {'Accept': 'text/html' if html else 'application/json', 'Content-Type': 'application/json'}
|
|
for cookie in jar:
|
|
if cookie.name == 'XSRF-TOKEN':
|
|
headers['X-XSRF-TOKEN'] = urllib.parse.unquote(cookie.value)
|
|
request = urllib.request.Request('http://127.0.0.1:18097' + path, headers=headers, data=None if data is None else json.dumps(data).encode(), method=method)
|
|
try:
|
|
response = opener.open(request)
|
|
except urllib.error.HTTPError as e:
|
|
response = e
|
|
assert response.headers.get('X-Investmoz-QA-Target') == expected
|
|
content = response.read()
|
|
assert response.status == status, f'{method} {path}: expected {status}, observed {response.status}; response body withheld'
|
|
events.append({'method': method, 'url': path, 'status': response.status, 'qa_target_verified': True})
|
|
return None if html or not content else json.loads(content)
|
|
|
|
|
|
call('GET', '/login', html=True)
|
|
credentials = json.loads((private / 'i0-qa-actor-private.json').read_text(encoding='utf-8-sig'))
|
|
login = call('POST', '/login', {k: credentials[k] for k in ['email', 'password']})
|
|
if login.get('two_factor'):
|
|
secret = base64.b32decode(credentials['totp_secret'])
|
|
digest = hmac.new(secret, struct.pack('>Q', int(time.time()) // 30), hashlib.sha1).digest()
|
|
offset = digest[-1] & 15
|
|
code = f'{(struct.unpack(">I", digest[offset:offset + 4])[0] & 0x7fffffff) % 1000000:06d}'
|
|
call('POST', '/two-factor-challenge', {'code': code}, status=204)
|
|
|
|
plans = []
|
|
results = []
|
|
apply = '--apply' in sys.argv
|
|
verify_new = '--verify-new' in sys.argv
|
|
assert not (apply and verify_new), 'Verification must not apply another batch.'
|
|
for level in [1, 2, 3]:
|
|
plan = call('GET', f'/management/territory-geometry-import?level={level}')
|
|
plans.append(plan)
|
|
assert plan['expected'] == {1: 11, 2: 159, 3: 411}[level]
|
|
if not apply:
|
|
continue
|
|
assert plan['scan_ready'] and not any('conflict' in r['status'] or r['status'] == 'invalid_geometry' for r in plan['rows'])
|
|
data = {'level': level, 'hash': plan['hash'], 'idempotency_key': str(uuid.uuid4()), 'reason': 'Correspondências explícitas Maputo e descendentes sob os pais aceites, exclusivamente em QA', 'confirm': True}
|
|
result = call('POST', '/management/territory-geometry-import', data)
|
|
assert not result['published'] and result['resolved'] == plan['resolved']
|
|
assert call('POST', '/management/territory-geometry-import', data) == result
|
|
updated = call('GET', f'/management/territory-geometry-import?level={level}')
|
|
assert all(r['status'] == 'reuse' for r in updated['rows'] if r['location_id'])
|
|
results.append(result)
|
|
# Recovery through the actual endpoints used by the hierarchy fields and map component.
|
|
consumers = []
|
|
locations_to_verify = []
|
|
for plan in plans:
|
|
for row in plan['rows']:
|
|
if row['status'] != 'insert' or not row['location_id'] or not apply:
|
|
continue
|
|
locations_to_verify.append(row['location_id'])
|
|
if verify_new:
|
|
preservation = json.loads((private / 'geometry-crosswalk-preservation-qa.json').read_text(encoding='utf-8-sig'))
|
|
assert preservation['target'] == target and preservation['catalogue_preserved'] and preservation['original_geometries_preserved'] == 361
|
|
locations_to_verify = [g['location_id'] for g in preservation['new_geometries']]
|
|
assert len(locations_to_verify) == len(set(locations_to_verify)) == 32
|
|
for location_id in locations_to_verify:
|
|
context = call('GET', '/management/territory-geometries?' + urllib.parse.urlencode({'territory_id': location_id}))
|
|
assert context['selected_geometry_available'] and any(a['selected_territory'] and a['territory_id'] == location_id for a in context['areas'])
|
|
consumers.append({'territory_id': location_id, 'selected_geometry_available_manager': True, 'areas': len(context['areas'])})
|
|
province_name_recovery = []
|
|
if verify_new:
|
|
for name in ['Maputo Província', 'Maputo Cidade']:
|
|
options = call('GET', '/api/v1/investment/territory-options?' + urllib.parse.urlencode({'type': 'province', 'page': 1, 'q': name}))
|
|
matches = [o for o in options['data'] if o['name'] == name]
|
|
assert len(matches) == 1 and not options.get('next_page_url')
|
|
assert matches[0]['id'] in locations_to_verify
|
|
province_name_recovery.append({'name': name, 'territory_id': matches[0]['id'], 'unique_exact_match': True})
|
|
call('POST', '/logout', status=204)
|
|
public_recovery = []
|
|
if verify_new:
|
|
for location_id in locations_to_verify:
|
|
context = call('GET', '/api/v1/investment/territory-geometries?' + urllib.parse.urlencode({'territory_id': location_id}))
|
|
assert not context['selected_geometry_available'] and context['areas'] == []
|
|
public_recovery.append({'territory_id': location_id, 'private_geometry_not_disclosed': True})
|
|
proof = {'captured_at': datetime.now(timezone.utc).isoformat(), 'target': target, 'applied': apply, 'mfa_login_completed': bool(login.get('two_factor')), 'plans': plans, 'results': results, 'consumer_recovery': consumers, 'province_name_recovery': province_name_recovery, 'public_recovery': public_recovery, 'requests': events, 'interface_observed': False, 'active_writes': 0, 'published': False}
|
|
destination = private / ('geometry-crosswalk-http-qa.json' if apply else 'geometry-crosswalk-consumers-http-qa.json' if verify_new else 'geometry-crosswalk-preview-qa.json')
|
|
if destination.exists():
|
|
destination.rename(private / (destination.stem + '-' + datetime.now(timezone.utc).strftime('%Y%m%d_%H%M%S') + '.json'))
|
|
destination.write_text(json.dumps(proof, ensure_ascii=False, indent=2), encoding='utf-8')
|
|
print(json.dumps({'target': target, 'applied': apply, 'levels': [{'level': p['level'], 'expected': p['expected'], 'resolved': p['resolved'], 'pending': p['pending']} for p in plans], 'inserted': sum(r['inserted'] for r in results), 'reused': sum(r['reused'] for r in results), 'consumers': len(consumers), 'published': False, 'interface_observed': False, 'active_writes': 0}))
|