From 167fa8d84af01a6c14c0e6bac1f3701114819756 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Tue, 17 Mar 2026 20:01:42 -0700 Subject: [PATCH] fix(e2e): cloud auth via cookie, local port, Playwright WebSocket gotcha MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit E2E harness fixes to get all three modes (demo/cloud/local) passing: - conftest.py: use ctx.add_cookies() for cloud auth instead of ctx.route() or set_extra_http_headers(). Playwright's route() only intercepts HTTP; set_extra_http_headers() explicitly excludes WebSocket handshakes. Streamlit reads st.context.headers from the WebSocket upgrade, so cookies are the only vehicle that reaches it without a reverse proxy. - cloud_session.py: fall back to Cookie header when X-CF-Session is absent — supports direct access (E2E tests, dev without Caddy). In production Caddy sets X-CF-Session; in tests the cf_session cookie is set on the browser context and arrives in the Cookie header. - modes/cloud.py: add /peregrine base URL path (STREAMLIT_SERVER_BASE_URL_PATH=peregrine) - modes/local.py: correct port from 8502 → 8501 and add /peregrine path All three modes now pass smoke + interaction tests clean. --- app/cloud_session.py | 7 ++++++- tests/e2e/conftest.py | 16 +++++++++++----- tests/e2e/modes/cloud.py | 2 +- tests/e2e/modes/local.py | 2 +- 4 files changed, 19 insertions(+), 8 deletions(-) diff --git a/app/cloud_session.py b/app/cloud_session.py index e5a3ed8..5e1b7b3 100644 --- a/app/cloud_session.py +++ b/app/cloud_session.py @@ -151,7 +151,12 @@ def resolve_session(app: str = "peregrine") -> None: if st.session_state.get("user_id"): return - cookie_header = st.context.headers.get("x-cf-session", "") + # Primary: Caddy injects X-CF-Session header in production. + # Fallback: direct access (E2E tests, dev without Caddy) reads the cookie header. + cookie_header = ( + st.context.headers.get("x-cf-session", "") + or st.context.headers.get("cookie", "") + ) session_jwt = _extract_session_token(cookie_header) if not session_jwt: _render_auth_wall("Please sign in to access Peregrine.") diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index ff0f7a7..a567d1a 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -93,11 +93,17 @@ def mode_contexts(active_modes, playwright) -> dict[str, BrowserContext]: for mode in active_modes: ctx = browser.new_context(viewport={"width": 1280, "height": 900}) if mode.name == "cloud": - def _inject_jwt(route, request): - jwt = _get_jwt() - headers = {**request.headers, "x-cf-session": f"cf_session={jwt}"} - route.continue_(headers=headers) - ctx.route(f"{mode.base_url}/**", _inject_jwt) + # Cookies are sent on WebSocket upgrade requests; set_extra_http_headers + # and ctx.route() are both HTTP-only and miss st.context.headers. + # cloud_session.py falls back to the Cookie header when X-CF-Session + # is absent (direct access without Caddy). + jwt = _get_jwt() + ctx.add_cookies([{ + "name": "cf_session", + "value": jwt, + "domain": "localhost", + "path": "/", + }]) else: mode.auth_setup(ctx) contexts[mode.name] = ctx diff --git a/tests/e2e/modes/cloud.py b/tests/e2e/modes/cloud.py index 112257a..19a4fb5 100644 --- a/tests/e2e/modes/cloud.py +++ b/tests/e2e/modes/cloud.py @@ -68,7 +68,7 @@ def _cloud_auth_setup(context: Any) -> None: CLOUD = ModeConfig( name="cloud", - base_url="http://localhost:8505", + base_url="http://localhost:8505/peregrine", auth_setup=_cloud_auth_setup, expected_failures=[], results_dir=Path("tests/e2e/results/cloud"), diff --git a/tests/e2e/modes/local.py b/tests/e2e/modes/local.py index 4cc993b..8e5047a 100644 --- a/tests/e2e/modes/local.py +++ b/tests/e2e/modes/local.py @@ -9,7 +9,7 @@ _BASE_SETTINGS_TABS = [ LOCAL = ModeConfig( name="local", - base_url="http://localhost:8502", + base_url="http://localhost:8501/peregrine", auth_setup=lambda ctx: None, expected_failures=[], results_dir=Path("tests/e2e/results/local"),