DUNGEONS-34: add one-command app launcher
Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
92
scripts/test-launcher.py
Normal file
92
scripts/test-launcher.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""POSIX integration test for the actual npm launcher, including teardown."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import signal
|
||||
import socket
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
origin = 'http://127.0.0.1:3002'
|
||||
|
||||
def stop(proc):
|
||||
if proc.poll() is None:
|
||||
os.killpg(proc.pid, signal.SIGINT)
|
||||
try:
|
||||
proc.wait(timeout=12)
|
||||
except subprocess.TimeoutExpired:
|
||||
os.killpg(proc.pid, signal.SIGKILL)
|
||||
proc.wait()
|
||||
raise AssertionError('Launcher did not stop on Ctrl+C')
|
||||
|
||||
with tempfile.TemporaryDirectory(prefix='webka-launcher-') as temporary:
|
||||
data = Path(temporary, 'data')
|
||||
env = dict(os.environ, WEBKA_DATA=str(data), WEBKA_MDNS='0')
|
||||
for key in ['WEBKA_ORIGIN', 'WEBKA_ALLOWED_ORIGINS']:
|
||||
env.pop(key, None)
|
||||
for iteration in range(2):
|
||||
logfile = Path(temporary, f'launch-{iteration}.log')
|
||||
with logfile.open('w') as output:
|
||||
proc = subprocess.Popen(['npm', 'run', 'dev:all', '--', '--host', '0.0.0.0', '--port', '3002'], cwd=root, env=env, stdout=output, stderr=subprocess.STDOUT, start_new_session=True)
|
||||
try:
|
||||
for _ in range(1200):
|
||||
if proc.poll() is not None:
|
||||
raise AssertionError(logfile.read_text())
|
||||
if 'Webka is ready!' in logfile.read_text():
|
||||
break
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
raise AssertionError('Startup timed out: ' + logfile.read_text()[-2000:])
|
||||
if iteration == 1:
|
||||
assert 'Frontend is up to date.' in logfile.read_text()
|
||||
assert 'Installing frontend dependencies' not in logfile.read_text()
|
||||
for route in ['/login', '/host', '/watch', '/settings/accounts', '/settings/general']:
|
||||
with urllib.request.urlopen(origin + route, timeout=3) as response:
|
||||
assert response.status == 200
|
||||
with urllib.request.urlopen(origin + '/api/bootstrap') as response:
|
||||
assert json.load(response)['needs_setup'] is (iteration == 0)
|
||||
body = json.dumps({'username': 'owner', 'password': 'launch-test-password-123', 'setup_token': (data / 'setup-token').read_text()}).encode()
|
||||
if iteration == 0:
|
||||
req = urllib.request.Request(origin + '/api/bootstrap', data=body, headers={'Origin': origin, 'Content-Type': 'application/json'})
|
||||
with urllib.request.urlopen(req) as response:
|
||||
assert response.status == 201
|
||||
req = urllib.request.Request(origin + '/api/login', data=body, headers={'Origin': origin, 'Content-Type': 'application/json'})
|
||||
with urllib.request.urlopen(req) as response:
|
||||
cookie = response.headers['Set-Cookie'].split(';')[0]
|
||||
req = urllib.request.Request(origin + '/api/status', headers={'Cookie': cookie})
|
||||
with urllib.request.urlopen(req) as response:
|
||||
status = json.load(response)
|
||||
assert ':3002/watch' in status['local_url']
|
||||
lan_origin = status['local_url'].removesuffix('/watch')
|
||||
req = urllib.request.Request(origin + '/api/login', data=body, headers={'Origin': lan_origin, 'Content-Type': 'application/json'})
|
||||
with urllib.request.urlopen(req) as response:
|
||||
assert response.status == 200
|
||||
req = urllib.request.Request(origin + '/api/logout', data=b'{}', headers={'Origin': 'http://evil.example', 'Content-Type': 'application/json', 'Cookie': cookie})
|
||||
try:
|
||||
urllib.request.urlopen(req)
|
||||
raise AssertionError('Untrusted origin accepted')
|
||||
except urllib.error.HTTPError as error:
|
||||
assert error.code == 403
|
||||
finally:
|
||||
stop(proc)
|
||||
# Assert that the actual server, not just npm, released the listener.
|
||||
with socket.socket() as server:
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind(('0.0.0.0', 3002))
|
||||
print(f'PASS: launch {iteration + 1}, routes, local/LAN auth, QR port, rejected foreign origin, Ctrl+C cleanup.', flush=True)
|
||||
with socket.socket() as occupied:
|
||||
occupied.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
occupied.bind(('127.0.0.1', 3002))
|
||||
occupied.listen()
|
||||
proc = subprocess.Popen(['npm', 'run', 'dev:all', '--', '--port', '3002'], cwd=root, env=env, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, start_new_session=True)
|
||||
try:
|
||||
stdout, stderr = proc.communicate(timeout=20)
|
||||
assert proc.returncode != 0 and 'already in use' in stderr, stdout + stderr
|
||||
finally:
|
||||
stop(proc)
|
||||
print('PASS: occupied port fails before building or starting a server.', flush=True)
|
||||
Reference in New Issue
Block a user