#!/usr/bin/env python3 """Generate runnable logging/storage projects and verify their HTTP and shutdown paths.""" import argparse from datetime import datetime, timezone import hashlib import json import os from pathlib import Path import secrets import signal import socket import subprocess import tempfile import time import urllib.error import urllib.parse import urllib.request def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument('--cli', type=Path) parser.add_argument('--framework-dir', type=Path, default=Path(__file__).resolve().parent.parent) parser.add_argument('--output', type=Path, default=Path('performance/scaffoldModulesLocal.json')) args = parser.parse_args() root = args.framework_dir.resolve() output = args.output.resolve() output.parent.mkdir(parents=True, exist_ok=True) log_path = output.with_suffix('.txt') env = {k: v for k, v in os.environ.items() if not k.startswith('GO_OMNI_')} env.update(GOWORK='off', GOMAXPROCS='4') report = {'status': 'running', 'startedAt': datetime.now(timezone.utc).isoformat(), 'cases': [], 'sourceSha256': {}} sources = list((root / 'cmd/go-omni').rglob('*.go')) + list((root / 'cmd/go-omni/scaffold').rglob('*.tmpl')) sources += [root / 'config/env.go', Path(__file__).resolve()] for p in sorted(sources): report['sourceSha256'][str(p.relative_to(root))] = hashlib.sha256(p.read_bytes()).hexdigest() opener = urllib.request.build_opener(urllib.request.ProxyHandler({})) def request(method, url, body=None, headers=None): req = urllib.request.Request(url, data=body, headers=headers or {}, method=method) try: response = opener.open(req, timeout=3) except urllib.error.HTTPError as error: response = error with response: return response.status, response.read(), dict(response.headers) def call(base, token, path, value=None, expected=200, method='POST'): headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token, 'X-Request-ID': 'scaffold-module-test'} body = json.dumps(value).encode() if value is not None else None status, data, _ = request(method, base + path, body, headers) if status != expected: raise AssertionError(f'{method} {path}: {status}, expected {expected}; {data[:512]!r}') return json.loads(data) if data else None failure = None with log_path.open('w') as log, tempfile.TemporaryDirectory(prefix='goOmniModuleAcceptance') as directory: temp = Path(directory).resolve() def run(command, cwd): subprocess.run(command, cwd=cwd, env=env, stdout=log, stderr=subprocess.STDOUT, check=True, timeout=180) log.flush() try: cli = args.cli.resolve() if args.cli else temp / 'go-omni' if not args.cli: run(['go', 'build', '-o', str(cli), './cmd/go-omni'], root) profiles = [('stderr', 'json', 'nethttp', signal.SIGTERM), ('stdout', 'text', 'native', signal.SIGINT), ('file', 'json', 'native', signal.SIGTERM), ('file', 'text', 'nethttp', signal.SIGINT)] for index, (sink, fmt, transport, stop_signal) in enumerate(profiles): print(f'Checking {sink}/{fmt}, {transport}, {stop_signal.name}', flush=True) project = temp / ('case' + str(index)) run([str(cli), 'new', '--module', 'example.com/moduleAcceptance', '--framework-dir', str(root), '--storage', 'local', '--log-output', sink, '--log-format', fmt, str(project)], temp) run(['go', 'test', '-mod=mod', '-race', '-p=2', './...'], project) binary = project / 'server' run(['go', 'build', '-o', str(binary), './cmd/server'], project) with socket.socket() as reservation: reservation.bind(('127.0.0.1', 0)) port = reservation.getsockname()[1] base = f'http://127.0.0.1:{port}' token = secrets.token_hex(32) runtime_env = dict(env, GO_OMNI_FILES_TOKEN=token, GO_OMNI_STORAGE_SIGNING_KEY=secrets.token_hex(32), GO_OMNI_HTTP_ADDR=f'127.0.0.1:{port}', GO_OMNI_TRANSPORT=transport, GO_OMNI_STORAGE_PUBLIC_URL=base + '/storage/object', GO_OMNI_STORAGE_MAX_BYTES='64') process_log = project / 'process.log' with process_log.open('w') as process_output: process = subprocess.Popen([str(binary)], cwd=project, env=runtime_env, stdout=process_output, stderr=subprocess.STDOUT) try: deadline = time.monotonic() + 15 while True: if process.poll() is not None: raise RuntimeError('startup failed: ' + process_log.read_text()) try: status, _, _ = request('GET', base + '/health/ready') if status == 200: break except (OSError, urllib.error.URLError): pass if time.monotonic() > deadline: raise TimeoutError('service readiness timeout') time.sleep(0.05) call(base, token, '/users', {'name': 'Example', 'email': 'module@example.com'}, 201) data = b'hello go-omni' upload_headers = {'Content-Type': 'text/plain', 'Authorization': 'Bearer ' + token, 'X-Request-ID': 'scaffold-module-test'} status, raw, _ = request('POST', base + '/files', data, upload_headers) if status != 201: raise AssertionError(f'upload failed: {status}, {raw!r}') key = json.loads(raw)['data']['key'] download = call(base, token, '/files/downloadLink', {'key': key})['data']['url'] status, raw, _ = request('GET', download) if status != 200 or raw != data: raise AssertionError('download content mismatch') parts = urllib.parse.urlsplit(download) query = urllib.parse.parse_qs(parts.query) query['signature'] = ['tampered'] altered = urllib.parse.urlunsplit(parts._replace(query=urllib.parse.urlencode(query, doseq=True))) if request('GET', altered)[0] != 403: raise AssertionError('tampered signature accepted') direct = call(base, token, '/files/uploadLink', {'size': len(data), 'contentType': 'text/plain'})['data'] signed = direct['request'] headers = {k: ', '.join(v) for k, v in signed['headers'].items()} if request(signed['method'], signed['url'], data, headers)[0] != 201: raise AssertionError('signed upload failed') if request('POST', base + '/files', data, {'Content-Type': 'text/plain'})[0] != 401: raise AssertionError('missing authorization accepted') if request('POST', base + '/files', b'x' * 65, upload_headers)[0] != 413: raise AssertionError('oversized upload accepted') wrong_headers = dict(upload_headers, **{'Content-Type': 'image/png'}) if request('POST', base + '/files', data, wrong_headers)[0] != 415: raise AssertionError('forged type accepted') call(base, token, '/files/downloadLink', {'key': '../bad'}, 400) call(base, token, '/files/downloadLink', {'key': 'missing'}, 404) process.send_signal(stop_signal) exit_code = process.wait(timeout=12) if exit_code != 0: raise AssertionError(f'{stop_signal.name}: exit {exit_code}') finally: if process.poll() is None: process.kill() process.wait(timeout=5) sink_path = project / 'var/log/server.log' if sink == 'file' else process_log text = sink_path.read_text() if token in text or 'signature=' in text or '"url":' in text: raise AssertionError('secret or signed URL appeared in logs') for event in ('user.created', 'file.uploaded', 'file.downloadLink.issued', 'file.uploadLink.issued', 'http.request', 'scaffold-module-test'): if event not in text: raise AssertionError('missing log evidence: ' + event) if fmt == 'json': entries = [json.loads(line) for line in text.splitlines() if line.strip()] if not any(e.get('status') == 403 and e.get('path') == '/storage/object' for e in entries): raise AssertionError('signed endpoint status lost in access logs') with socket.socket() as check: check.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) check.bind(('127.0.0.1', port)) # Startup validation must fail instead of silently exposing control endpoints. invalid_env = dict(runtime_env, GO_OMNI_FILES_TOKEN='') invalid = subprocess.run([str(binary)], cwd=project, env=invalid_env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5) if invalid.returncode == 0 or b'GO_OMNI_FILES_TOKEN' not in invalid.stdout: raise AssertionError('missing token did not fail startup') report['cases'].append({'logOutput': sink, 'logFormat': fmt, 'transport': transport, 'signal': stop_signal.name, 'status': 'passed', 'exitCode': exit_code, 'portReleased': True, 'missingTokenRejected': True}) report['status'] = 'passed' except Exception as error: failure = str(error) report['status'], report['error'] = 'failed', failure finally: log.flush() report['finishedAt'] = datetime.now(timezone.utc).isoformat() report['logSha256'] = hashlib.sha256(log_path.read_bytes()).hexdigest() output.write_text(json.dumps(report, indent=2) + '\n') if failure: raise SystemExit('FAIL: ' + failure + '; see ' + str(log_path)) print('PASS generated project HTTP/logging/shutdown; report: ' + str(output), flush=True) if __name__ == '__main__': main()