| 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() |
|
|