Spaces:
Paused
Paused
File size: 5,616 Bytes
17b22d9 e14044c 17b22d9 e14044c 17b22d9 e14044c 17b22d9 e14044c 17b22d9 e14044c 17b22d9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 | """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()
|