80 lines
3.8 KiB
Python
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.")
|
|
|