""" End-to-end test for the initial password-setup flow on a scaffolded project (webapp-scaffold #7, Option A1). Reproduces the contract that broke twice in production: 1. oatpp-authkit AuthInterceptor rejected `/set-password?token=…` with 401 because the public-path check compared against the request-target (which includes the query string). v0.3.3 / commit 46971ac. 2. Newly-scaffolded projects shipped with `VITE_BASE='/'` so SPA assets 404'd behind the `/projects//` Apache prefix → blank page. webapp-scaffold v0.3.6 / commit b1a13b8. Both regressions slipped past the existing unit/component test layers because none of them exercises the *deployed* shape of a scaffolded project. The fixtures in `conftest.py` recreate that shape inline: clone webapp-template, build with VITE_BASE pinned, boot the binary, front it with the in-process PrefixStrippingProxy, follow the email link. """ import re import urllib.request import urllib.error import pytest def _http_get(url: str, *, headers=None) -> tuple[int, str, dict]: req = urllib.request.Request(url, headers=headers or {"Accept": "text/html"}) try: with urllib.request.urlopen(req, timeout=10) as r: return r.status, r.read().decode("utf-8", "replace"), dict(r.headers) except urllib.error.HTTPError as e: return e.code, e.read().decode("utf-8", "replace"), dict(e.headers) def test_set_password_link_returns_html(proxy, admin_token): """Regression for oatpp-authkit query-string 401: the link must serve the SPA HTML, not a JSON 401.""" url = f"{proxy['base_url']}/set-password?token={admin_token}" status, body, headers = _http_get(url) assert status == 200, f"got {status}: {body[:200]}" assert "text/html" in headers.get("Content-Type", "").lower(), headers # The SPA shell loads the bundle; the form itself is rendered by JS, # so we just assert the shell is well-formed. assert "]*\b(?:src|href)\s*=\s*["\']([^"\']+)["\']', body, re.IGNORECASE, ) # Only check absolute-path asset URLs (skip cross-origin, data:, etc.). local = [u for u in asset_urls if u.startswith("/")] assert local, f"no local asset URLs found in HTML: {body[:300]}" failures = [] for path in local: # The asset URL is browser-side — already includes the proxy prefix. # Fetch it through the proxy so we exercise the same path the # browser would. full = f"http://127.0.0.1:{proxy['port']}{path}" s, _, _ = _http_get(full) if s != 200: failures.append(f"{path} → {s}") assert not failures, "asset URLs failed to resolve through the prefix:\n " \ + "\n ".join(failures) def test_api_path_still_returns_json_401(proxy): """Sanity check the content-negotiation: an /api/ call without a token must still get JSON 401 (browser navigation gets HTML, but API stays JSON).""" full = f"http://127.0.0.1:{proxy['port']}{proxy['prefix']}/api/users" req = urllib.request.Request(full, headers={"Accept": "application/json"}) try: urllib.request.urlopen(req, timeout=5) pytest.fail("expected 401 from unauth /api/ call") except urllib.error.HTTPError as e: assert e.code == 401 assert "application/json" in e.headers.get("Content-Type", "").lower()