GuestOps/tests/production_smoke.py

80 lines
3.8 KiB
Python

"""Exercise disposable CI containers through the host's trusted proxy address.
The forwarded HTTPS header simulates Nginx TLS termination; this is never run
against an existing hotel database. Cookie values and credentials are not logged.
"""
import json
import os
import sys
import time
from http.cookies import SimpleCookie
from urllib.request import Request, urlopen
from urllib.error import HTTPError
cookies = SimpleCookie()
csrf = ""
def request(path, method="GET", data=None, expected=200):
headers = {"X-Forwarded-Proto": "https", "Content-Type": "application/json",
"Cookie": "; ".join(f"{k}={v.value}" for k, v in cookies.items()),
"X-CSRF-TOKEN": csrf}
req = Request("http://127.0.0.1:8080" + path, method=method, headers=headers,
data=json.dumps(data).encode() if data is not None else None)
try:
response = urlopen(req, timeout=15)
except HTTPError as error:
response = error
assert response.status == expected, f"{method} {path}: {response.status}, expected {expected}"
for value in response.headers.get_all("Set-Cookie", []):
parsed = SimpleCookie(value)
for cookie in parsed.values():
assert cookie["secure"] and cookie["httponly"], "Authentication cookies must be secure and HttpOnly"
cookies.load(value)
body = response.read().decode()
return json.loads(body) if "application/json" in response.headers.get("Content-Type", "") else body
assert "root" in request("/"), "Container must serve the built React interface"
request("/api/hotel", expected=401)
session = request("/api/session")
assert session["preview"] is False and session["user"] is None
csrf = session["csrfToken"]
request("/api/auth/login", "POST", {"email": os.environ["BOOTSTRAP_EMAIL"], "password": os.environ["BOOTSTRAP_PASSWORD"]})
session = request("/api/session")
assert session["user"]["role"] == "Owner"
csrf = session["csrfToken"]
assert request("/health/ready") == {"status": "ready"}
for attempt in range(10):
health = request("/api/operations")
if health["worker"]["state"] == "Reporting":
break
time.sleep(1)
assert health["worker"]["state"] == "Reporting" and health["database"] == "Reachable"
auto_status = request("/api/auto-replies/status")
assert auto_status["liveConfigured"] is False
request("/api/auto-replies/mode", "PUT", {"mode": "Live", "version": 0, "acceptanceConfirmed": True}, expected=400)
payment_status = request("/api/payments/status")
assert payment_status["configured"] is False and payment_status["createsConfigured"] is False
assert "securityKey" not in payment_status
request("/api/payments/controls", "PUT", {"version": 0, "enabled": True}, expected=400)
assert request("/api/payments/requests") == []
hotel = request("/api/hotel")
assert "root" in request("/account"), "Container must serve account link page"
assert len(request("/api/onboarding")["steps"]) == 5
team = request("/api/team")
assert all("passwordHash" not in member and "securityStamp" not in member and "accountLinkHash" not in member for member in team)
if "--read" in sys.argv:
assert hotel["signature"] == "Persisted across container restart"
invited = next(member for member in team if member["email"] == "ci-staff@example.invalid")
assert invited["pending"] and not invited["active"] and invited["linkPurpose"] == "Invite"
else:
hotel["signature"] = "Persisted across container restart"
request("/api/hotel", "PUT", hotel)
invite = request("/api/team/invite", "POST", {"name": "CI Staff", "email": "ci-staff@example.invalid"})
assert invite["link"].startswith("https://sandbox-guestops.futuresens.co.uk/account#token=")
request("/api/auth/logout", "POST")
request("/api/hotel", expected=401)
print("Production smoke checks passed: built UI, secure cookies, owner login, persisted settings and staff invitation, onboarding and logout.")