"""The sign-in flow, exercised without ever reaching Hugging Face. Everything here is about refusing bad input. The happy path needs a real provider and a real Space, and is verified against the deployment rather than faked here — a mocked token exchange would only assert that the mock works. """ from __future__ import annotations import pytest from fastapi.testclient import TestClient from server import oauth from server.main import app @pytest.fixture(autouse=True) def clean_env(monkeypatch): for var in ("OAUTH_CLIENT_ID", "OAUTH_CLIENT_SECRET", "OAUTH_SCOPES", "OPENID_PROVIDER_URL", "SPACE_ID", "SPACE_HOST", "HF_TOKEN", "HUGGING_FACE_HUB_TOKEN", "ASE_ALLOW_SPACE_CREDITS"): monkeypatch.delenv(var, raising=False) monkeypatch.setenv("ASE_KEY_FILE", "/nonexistent/ase-glm-test-key") @pytest.fixture def client(): return TestClient(app) def test_not_configured_without_a_space(client): assert oauth.configured() is False r = client.get("/api/auth/login", follow_redirects=False) assert r.status_code == 503 assert "Space" in r.json()["error"] def test_config_says_sign_in_is_unavailable(client): auth = client.get("/api/config").json()["auth"] assert auth["canSignIn"] is False assert auth["profile"] is None def test_login_redirects_to_the_provider_with_the_billing_scope(client, monkeypatch): monkeypatch.setenv("OAUTH_CLIENT_ID", "cid") monkeypatch.setenv("OAUTH_CLIENT_SECRET", "shh") monkeypatch.setenv("SPACE_HOST", "example.hf.space") r = client.get("/api/auth/login", follow_redirects=False) assert r.status_code == 302 url = r.headers["location"] assert url.startswith("https://huggingface.co/oauth/authorize?") # inference-api is the whole point: without it we learn who someone is and # still cannot bill them. assert "inference-api" in url assert "example.hf.space%2Fauth%2Fcallback" in url assert "state=" in url assert "shh" not in url def start_login(client) -> str: """Begin a sign-in and return the state we were issued.""" r = client.get("/api/auth/login", follow_redirects=False) query = r.headers["location"].split("?", 1)[1] return dict(p.split("=", 1) for p in query.split("&"))["state"] def test_the_callback_refuses_a_state_it_did_not_issue(client, monkeypatch): monkeypatch.setenv("OAUTH_CLIENT_ID", "cid") monkeypatch.setenv("OAUTH_CLIENT_SECRET", "shh") # A real session, so the rejection is about the state and not the cookie. start_login(client) r = client.get("/auth/callback", params={"code": "abc", "state": "forged"}, follow_redirects=False) assert r.status_code == 200 assert "state did not match" in r.text # And nothing was stored. assert client.get("/api/config").json()["auth"]["mode"] == "none" def test_the_callback_refuses_a_missing_state(client): r = client.get("/auth/callback", params={"code": "abc"}, follow_redirects=False) # No session came back at all: reported as a cookie problem, because that # is what it almost always is and it has a different fix from a forgery. assert "not returned by your browser" in r.text def test_the_session_cookie_crosses_sites_on_a_space(monkeypatch): # A Space is viewed inside an iframe on huggingface.co, so this cookie is # third-party. Under SameSite=Lax the browser withholds it on the way back # from the provider and every sign-in fails as a state mismatch. monkeypatch.setenv("SPACE_ID", "someone/ase-glm") monkeypatch.setenv("OAUTH_CLIENT_ID", "cid") monkeypatch.setenv("OAUTH_CLIENT_SECRET", "shh") import importlib from server import main as main_mod importlib.reload(main_mod) r = TestClient(main_mod.app).get("/api/auth/login", follow_redirects=False) cookie = r.headers.get("set-cookie", "").lower() assert "samesite=none" in cookie # SameSite=None is only honoured on a Secure cookie. assert "secure" in cookie importlib.reload(main_mod) def test_the_callback_reports_a_provider_error(client): r = client.get("/auth/callback", params={"error": "access_denied"}) assert "access_denied" in r.text def test_a_state_cannot_be_replayed(client, monkeypatch): monkeypatch.setenv("OAUTH_CLIENT_ID", "cid") monkeypatch.setenv("OAUTH_CLIENT_SECRET", "shh") monkeypatch.setenv("SPACE_HOST", "example.hf.space") state = start_login(client) # The first use consumes it (and then fails at the token exchange, which is # fine — what matters is that a second attempt cannot reuse the same state). client.get("/auth/callback", params={"code": "abc", "state": state}) again = client.get("/auth/callback", params={"code": "abc", "state": state}) assert "Signed in" not in again.text assert client.get("/api/config").json()["auth"]["mode"] == "none" def test_logout_is_a_post_and_clears_the_session(client): assert client.get("/auth/callback", params={"error": "x"}).status_code == 200 assert client.post("/api/auth/logout").json() == {"ok": True} # GET must not log anyone out: a prefetched link would sign them out. assert client.get("/api/auth/logout").status_code in (404, 405) def test_the_session_cookie_is_not_readable_by_script(client, monkeypatch): monkeypatch.setenv("OAUTH_CLIENT_ID", "cid") monkeypatch.setenv("OAUTH_CLIENT_SECRET", "shh") r = client.get("/api/auth/login", follow_redirects=False) cookie = r.headers.get("set-cookie", "") assert "httponly" in cookie.lower() assert "samesite=lax" in cookie.lower()