from __future__ import annotations import contextlib import io import json import threading from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer import pytest from scripts.verify_deployment import DeploymentVerificationError, verify_deployment @contextlib.contextmanager def deployment_server( *, expected_token: str = "correct horse battery staple", ready: int = 200, relative_manifest: bool = False, ): requests: list[tuple[str, str, str]] = [] class Handler(BaseHTTPRequestHandler): def do_GET(self): # noqa: N802 cookie = self.headers.get("Cookie", "") requests.append(("GET", self.path, cookie)) if self.path == "/dashboard/healthz": return self.respond(200, {"status": "ok"}) if self.path == "/dashboard/readyz": return self.respond(ready, {"status": "ready" if ready == 200 else "not ready"}) if self.path == "/dashboard/manifest.webmanifest": return self.respond(200, { "name": "StackChain Dashboard", "start_url": "./" if relative_manifest else "/dashboard/", "scope": "./" if relative_manifest else "/dashboard/", }) if self.path == "/dashboard/login": return self.respond(200, "
", "text/html") if self.path == "/dashboard/": if "stackchain_session=valid" in cookie: return self.respond(200, "", "text/html") self.send_response(303) self.send_header("Location", "/dashboard/login") self.end_headers() return None return self.respond(404, {"detail": "missing"}) def do_POST(self): # noqa: N802 length = int(self.headers.get("Content-Length", "0")) body = self.rfile.read(length).decode() requests.append(("POST", self.path, body)) supplied = json.loads(body).get("access_token", "") if self.path == "/dashboard/api/v1/session" and supplied == expected_token: payload = json.dumps({"authenticated": True}).encode() self.send_response(200) self.send_header("Content-Type", "application/json") self.send_header("Set-Cookie", "stackchain_session=valid; Path=/dashboard/; HttpOnly") self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) return None return self.respond(401, {"detail": "Invalid access token"}) def respond(self, status, body, content_type="application/json"): payload = body.encode() if isinstance(body, str) else json.dumps(body).encode() self.send_response(status) self.send_header("Content-Type", content_type) self.send_header("Content-Length", str(len(payload))) self.end_headers() self.wfile.write(payload) return None def log_message(self, *args): pass server = ThreadingHTTPServer(("127.0.0.1", 0), Handler) thread = threading.Thread(target=server.serve_forever, daemon=True) thread.start() try: yield f"http://127.0.0.1:{server.server_port}/dashboard/", requests finally: server.shutdown() thread.join() def test_verifier_proves_public_login_manifest_and_authenticated_mobile_home(): with deployment_server() as (url, requests): result = verify_deployment(url, "correct horse battery staple") assert result == { "health": "ok", "readiness": "ready", "public_entry": "login", "manifest_scope": "/dashboard/", "authenticated_home": "mobile", } assert any( method == "POST" and path == "/dashboard/api/v1/session" for method, path, _ in requests ) def test_verifier_resolves_relative_manifest_scope_inside_public_subpath(): with deployment_server(relative_manifest=True) as (url, _): result = verify_deployment(url, "correct horse battery staple") assert result["manifest_scope"] == "/dashboard/" def test_verifier_failure_redacts_operator_token(): secret = "never-print-this-secret" with deployment_server(expected_token="different") as (url, _): with pytest.raises(DeploymentVerificationError) as raised: verify_deployment(url, secret) assert secret not in str(raised.value) def test_verifier_rejects_a_live_but_unready_deployment_before_login(): with deployment_server(ready=503) as (url, requests): with pytest.raises(DeploymentVerificationError, match="readiness returned HTTP 503"): verify_deployment(url, "correct horse battery staple") assert not any(method == "POST" for method, _, _ in requests)