| """Tests for authenticated HF token propagation through backend dependencies.""" |
|
|
| import sys |
| from pathlib import Path |
| from types import SimpleNamespace |
| from urllib.parse import parse_qs, urlparse |
|
|
| import pytest |
| from fastapi import HTTPException |
|
|
| _BACKEND_DIR = Path(__file__).resolve().parent.parent.parent / "backend" |
| if str(_BACKEND_DIR) not in sys.path: |
| sys.path.insert(0, str(_BACKEND_DIR)) |
|
|
| import dependencies |
| from routes import auth |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_current_user_carries_internal_hf_token(monkeypatch): |
| monkeypatch.setattr(dependencies, "AUTH_ENABLED", True) |
| dependencies._token_cache.clear() |
|
|
| async def fake_validate_token(token): |
| assert token == "hf-user-token" |
| return {"sub": "user-id", "preferred_username": "alice"} |
|
|
| async def fake_fetch_user_plan(token): |
| assert token == "hf-user-token" |
| return "pro" |
|
|
| monkeypatch.setattr(dependencies, "_validate_token", fake_validate_token) |
| monkeypatch.setattr(dependencies, "_fetch_user_plan", fake_fetch_user_plan) |
|
|
| request = SimpleNamespace( |
| headers={"Authorization": "Bearer hf-user-token"}, |
| cookies={}, |
| ) |
|
|
| user = await dependencies.get_current_user(request) |
|
|
| assert user["user_id"] == "user-id" |
| assert user["username"] == "alice" |
| assert user["plan"] == "pro" |
| assert user[dependencies.INTERNAL_HF_TOKEN_KEY] == "hf-user-token" |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_cookie_auth_requires_current_oauth_scope_marker(monkeypatch): |
| monkeypatch.setattr(dependencies, "AUTH_ENABLED", True) |
|
|
| request = SimpleNamespace( |
| headers={}, |
| cookies={"hf_access_token": "hf-user-token"}, |
| ) |
|
|
| with pytest.raises(HTTPException) as exc_info: |
| await dependencies.get_current_user(request) |
|
|
| assert exc_info.value.status_code == 401 |
| assert "scopes changed" in exc_info.value.detail |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_cookie_auth_accepts_current_oauth_scope_marker(monkeypatch): |
| monkeypatch.setattr(dependencies, "AUTH_ENABLED", True) |
| dependencies._token_cache.clear() |
|
|
| async def fake_validate_token(token): |
| assert token == "hf-user-token" |
| return {"sub": "user-id", "preferred_username": "alice"} |
|
|
| async def fake_fetch_user_plan(token): |
| assert token == "hf-user-token" |
| return "pro" |
|
|
| monkeypatch.setattr(dependencies, "_validate_token", fake_validate_token) |
| monkeypatch.setattr(dependencies, "_fetch_user_plan", fake_fetch_user_plan) |
|
|
| request = SimpleNamespace( |
| headers={}, |
| cookies={ |
| "hf_access_token": "hf-user-token", |
| dependencies.OAUTH_SCOPE_COOKIE: dependencies.oauth_scope_fingerprint(), |
| }, |
| ) |
|
|
| user = await dependencies.get_current_user(request) |
|
|
| assert user["user_id"] == "user-id" |
| assert user[dependencies.INTERNAL_HF_TOKEN_KEY] == "hf-user-token" |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_auth_me_does_not_expose_internal_hf_token(): |
| user = { |
| "user_id": "user-id", |
| "username": "alice", |
| "authenticated": True, |
| dependencies.INTERNAL_HF_TOKEN_KEY: "hf-user-token", |
| } |
|
|
| response = await auth.get_me(user) |
|
|
| assert response == { |
| "user_id": "user-id", |
| "username": "alice", |
| "authenticated": True, |
| } |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_oauth_login_requests_collection_write_scope(monkeypatch): |
| monkeypatch.setattr(auth, "OAUTH_CLIENT_ID", "oauth-client") |
| monkeypatch.setenv("SPACE_HOST", "example.hf.space") |
| auth.oauth_states.clear() |
|
|
| response = await auth.oauth_login(SimpleNamespace()) |
| params = parse_qs(urlparse(response.headers["location"]).query) |
| scopes = set(params["scope"][0].split()) |
|
|
| assert "write-collections" in scopes |
|
|
|
|
| def test_oauth_callback_detects_missing_required_collection_scope(): |
| granted = [scope for scope in auth.OAUTH_SCOPES if scope != "write-collections"] |
|
|
| assert auth._missing_required_scopes({"scope": " ".join(granted)}) == { |
| "write-collections" |
| } |
|
|
|
|
| def test_oauth_callback_treats_absent_scope_as_full_grant(): |
| assert auth._missing_required_scopes({}) == set() |
|
|
|
|
| @pytest.mark.asyncio |
| async def test_oauth_callback_sets_scope_marker_cookie(monkeypatch): |
| monkeypatch.setenv("SPACE_HOST", "example.hf.space") |
| auth.oauth_states.clear() |
| auth.oauth_states["state"] = { |
| "redirect_uri": "https://example.hf.space/auth/callback", |
| "expires_at": 9999999999, |
| } |
|
|
| class FakeResponse: |
| def __init__(self, payload): |
| self._payload = payload |
|
|
| def raise_for_status(self): |
| return None |
|
|
| def json(self): |
| return self._payload |
|
|
| class FakeAsyncClient: |
| def __init__(self, *args, **kwargs): |
| pass |
|
|
| async def __aenter__(self): |
| return self |
|
|
| async def __aexit__(self, *args): |
| return None |
|
|
| async def post(self, *args, **kwargs): |
| return FakeResponse( |
| { |
| "access_token": "hf-user-token", |
| "scope": " ".join(auth.OAUTH_SCOPES), |
| } |
| ) |
|
|
| async def get(self, *args, **kwargs): |
| return FakeResponse({}) |
|
|
| monkeypatch.setattr(auth.httpx, "AsyncClient", FakeAsyncClient) |
|
|
| response = await auth.oauth_callback(SimpleNamespace(), code="code", state="state") |
| set_cookies = [ |
| value.decode("latin-1") |
| for key, value in response.raw_headers |
| if key == b"set-cookie" |
| ] |
|
|
| expected = ( |
| f"{dependencies.OAUTH_SCOPE_COOKIE}=" |
| f"{dependencies.oauth_scope_fingerprint(auth.OAUTH_SCOPES)}" |
| ) |
| assert any(cookie.startswith(expected) for cookie in set_cookies) |
|
|