from __future__ import annotations import os from urllib.parse import parse_qs, urlparse def _set_google_env() -> None: os.environ["GOOGLE_CLIENT_ID"] = "test-google-client" os.environ["GOOGLE_CLIENT_SECRET"] = "test-google-secret" os.environ["FRONTEND_BASE_URL"] = "http://127.0.0.1:3000" os.environ["GOOGLE_OAUTH_REDIRECT_URI"] = "http://testserver/auth/google/callback" from app.core.config import get_settings get_settings.cache_clear() def _clear_google_env() -> None: for key in ( "GOOGLE_CLIENT_ID", "GOOGLE_CLIENT_SECRET", "FRONTEND_BASE_URL", "GOOGLE_OAUTH_REDIRECT_URI", ): os.environ.pop(key, None) from app.core.config import get_settings get_settings.cache_clear() def _fragment_params(location: str) -> dict[str, str]: fragment = urlparse(location).fragment parsed = parse_qs(fragment) return {key: values[0] for key, values in parsed.items()} def test_google_start_redirects_to_frontend_error_without_oauth_credentials(auth_client): os.environ["GOOGLE_CLIENT_ID"] = "" os.environ["GOOGLE_CLIENT_SECRET"] = "" from app.core.config import get_settings get_settings.cache_clear() try: response = auth_client.get("/auth/google/start", follow_redirects=False) finally: _clear_google_env() assert response.status_code == 302 params = _fragment_params(response.headers["location"]) assert "not ready" in params["error"].lower() def test_google_callback_creates_user_and_returns_frontend_session(auth_client, monkeypatch): _set_google_env() from app.routes import auth as auth_routes monkeypatch.setattr( auth_routes, "_post_google_token", lambda payload: {"access_token": "google-access-token"}, ) monkeypatch.setattr( auth_routes, "_fetch_google_profile", lambda access_token: { "email": "google.student@example.com", "email_verified": True, "name": "Google Student", }, ) try: nonce = "test-google-state-nonce" state = auth_routes._create_google_state( next_path="/dashboard", invite_code=None, nonce=nonce ) response = auth_client.get( f"/auth/google/callback?code=test-code&state={state}", cookies={auth_routes.GOOGLE_STATE_COOKIE: nonce}, follow_redirects=False, ) assert response.status_code == 302 location = response.headers["location"] assert location.startswith("http://127.0.0.1:3000/auth/google/callback#") params = _fragment_params(location) assert params["access_token"] assert params["token_type"] == "bearer" assert params["next"] == "/dashboard" assert "google.student@example.com" in params["user"] finally: _clear_google_env() def test_google_callback_respects_beta_invite_gate(auth_client, monkeypatch): _set_google_env() os.environ["BETA_ACCESS_ENABLED"] = "true" os.environ["BETA_INVITE_CODE"] = "DOCDOE-BETA-2026" from app.core.config import get_settings from app.routes import auth as auth_routes get_settings.cache_clear() monkeypatch.setattr( auth_routes, "_post_google_token", lambda payload: {"access_token": "google-access-token"}, ) monkeypatch.setattr( auth_routes, "_fetch_google_profile", lambda access_token: { "email": "invite-needed@example.com", "email_verified": True, "name": "Invite Needed", }, ) try: nonce = "test-google-state-nonce" state = auth_routes._create_google_state( next_path="/onboarding", invite_code=None, nonce=nonce ) response = auth_client.get( f"/auth/google/callback?code=test-code&state={state}", cookies={auth_routes.GOOGLE_STATE_COOKIE: nonce}, follow_redirects=False, ) assert response.status_code == 302 params = _fragment_params(response.headers["location"]) assert "invite" in params["error"].lower() assert params["next"] == "/onboarding" finally: os.environ["BETA_ACCESS_ENABLED"] = "false" os.environ.pop("BETA_INVITE_CODE", None) _clear_google_env() def test_google_callback_rejects_state_from_another_browser(auth_client, monkeypatch): _set_google_env() from app.routes import auth as auth_routes monkeypatch.setattr( auth_routes, "_post_google_token", lambda payload: (_ for _ in ()).throw(AssertionError("token exchange must not run")), ) try: state = auth_routes._create_google_state( next_path="/dashboard", invite_code=None, nonce="browser-a" ) response = auth_client.get( f"/auth/google/callback?code=test-code&state={state}", cookies={auth_routes.GOOGLE_STATE_COOKIE: "browser-b"}, follow_redirects=False, ) assert response.status_code == 302 params = _fragment_params(response.headers["location"]) assert "expired" in params["error"].lower() finally: _clear_google_env()