Files

81 lines
3.9 KiB
Python
Executable File

#!/usr/bin/env python3
"""Start an isolated native process and verify HTTP readiness and PWA resources."""
import argparse
import json
import os
from pathlib import Path
import socket
import subprocess
import tempfile
import time
import urllib.error
import urllib.parse
import urllib.request
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('binary', type=Path)
parser.add_argument('--timeout', type=int, default=90)
args = parser.parse_args()
binary = args.binary.resolve(strict=True)
with socket.socket() as sock:
sock.bind(('127.0.0.1', 0))
port = sock.getsockname()[1]
base = f'http://127.0.0.1:{port}'
# Never inherit application secrets, OIDC settings or a developer's JAVA_HOME.
env = {'PATH': '/usr/bin:/bin', 'LANG': 'C.UTF-8'}
with tempfile.TemporaryDirectory(prefix='werkjournal-smoke-') as directory:
env['HOME'] = directory
logfile = Path(directory) / 'server.log'
with logfile.open('wb') as log:
process = subprocess.Popen(
[str(binary), f'--server.port={port}', '--server.address=127.0.0.1'],
cwd=directory, env=env, stdout=log, stderr=subprocess.STDOUT,
)
try:
deadline = time.monotonic() + args.timeout
while True:
if process.poll() is not None:
raise RuntimeError(f'Native process exited with {process.returncode}')
try:
with urllib.request.urlopen(base + '/api/v1/info', timeout=2) as response:
info = json.load(response)
if info.get('application') != 'werkjournal' or not info.get('version'):
raise RuntimeError(f'Unexpected readiness response: {info}')
break
except (urllib.error.URLError, TimeoutError):
if time.monotonic() >= deadline:
raise RuntimeError('Native readiness timed out')
time.sleep(0.25)
for resource, expected in [('/', b'<html'), ('/offline.html', b'Werkjournal')]:
with urllib.request.urlopen(base + resource, timeout=5) as response:
if expected not in response.read():
raise RuntimeError(f'Missing content in {resource}')
with urllib.request.urlopen(base + '/manifest.webmanifest', timeout=5) as response:
manifest = json.load(response)
if manifest.get('name') != 'Werkjournal' or not manifest.get('icons'):
raise RuntimeError('Incomplete PWA manifest')
for icon in manifest['icons']:
icon_url = urllib.parse.urljoin(base + '/', icon['src'])
if urllib.parse.urlparse(icon_url).netloc != urllib.parse.urlparse(base).netloc:
raise RuntimeError('Unexpected external PWA icon')
with urllib.request.urlopen(icon_url, timeout=5) as response:
if not response.headers.get('Content-Type', '').startswith('image/') or not response.read():
raise RuntimeError('PWA icon is missing or is not an image')
with urllib.request.urlopen(base + '/sw.js', timeout=5) as response:
if 'javascript' not in response.headers.get('Content-Type', '') or not response.read():
raise RuntimeError('Service worker is missing or is not JavaScript')
if process.poll() is not None:
raise RuntimeError('Process stopped during smoke checks')
print(json.dumps({'result': 'PASS', 'info': info, 'binary': str(binary)}))
except Exception:
print(logfile.read_text(errors='replace'))
raise
finally:
if process.poll() is None:
process.terminate()
try:
process.wait(timeout=20)
except subprocess.TimeoutExpired:
process.kill()
process.wait()