From d190029728e284c29be2c7854f9e87a99a7c6810 Mon Sep 17 00:00:00 2001 From: Andreas Wrede Date: Fri, 8 May 2026 13:42:21 -0400 Subject: [PATCH] fix: guard unconfigured oauth calls; add missing test coverage; clean imports Co-Authored-By: Claude Sonnet 4.6 --- hbd/server/oauth.py | 12 +++++++++--- tests/test_oauth.py | 47 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 52 insertions(+), 7 deletions(-) diff --git a/hbd/server/oauth.py b/hbd/server/oauth.py index aabb0a9..9435dcd 100644 --- a/hbd/server/oauth.py +++ b/hbd/server/oauth.py @@ -71,6 +71,8 @@ def is_enabled(config: dict) -> bool: def authorization_url(config: dict, state: str, redirect_uri: str) -> str: """Return the Gitea OAuth2 authorization URL to redirect the browser to.""" g = _gitea_cfg(config) + if not (g.get("url") and g.get("client_id") and g.get("client_secret")): + raise OAuthError("Gitea OAuth2 is not configured") params = urllib.parse.urlencode({ "client_id": g["client_id"], "redirect_uri": redirect_uri, @@ -87,6 +89,8 @@ async def exchange_code(config: dict, code: str, redirect_uri: str) -> str: Returns the access token string. Raises OAuthError on any failure. """ g = _gitea_cfg(config) + if not (g.get("url") and g.get("client_id") and g.get("client_secret")): + raise OAuthError("Gitea OAuth2 is not configured") url = f"{g['url'].rstrip('/')}/login/oauth/access_token" payload = { "client_id": g["client_id"], @@ -103,11 +107,11 @@ async def exchange_code(config: dict, code: str, redirect_uri: str) -> str: text = await resp.text() raise OAuthError(f"Token exchange failed ({resp.status}): {text}") data = await resp.json() + token = data.get("access_token") + if not token: + raise OAuthError(f"No access_token in response: {data}") except aiohttp.ClientError as exc: raise OAuthError(f"Token exchange network error: {exc}") from exc - token = data.get("access_token") - if not token: - raise OAuthError(f"No access_token in response: {data}") return token @@ -118,6 +122,8 @@ async def fetch_user(config: dict, token: str) -> dict: Raises OAuthError on any failure. """ g = _gitea_cfg(config) + if not (g.get("url") and g.get("client_id") and g.get("client_secret")): + raise OAuthError("Gitea OAuth2 is not configured") url = f"{g['url'].rstrip('/')}/api/v1/user" timeout = aiohttp.ClientTimeout(total=10) try: diff --git a/tests/test_oauth.py b/tests/test_oauth.py index b4e4e6e..13ac90d 100644 --- a/tests/test_oauth.py +++ b/tests/test_oauth.py @@ -1,4 +1,6 @@ import time as time_mod +from unittest.mock import AsyncMock, MagicMock, patch +from urllib.parse import urlparse, parse_qs import pytest @@ -132,10 +134,6 @@ def test_provision_oauth_user_survives_config_reload(): assert "oauthonly" in users_mod.users -from unittest.mock import AsyncMock, MagicMock, patch -from urllib.parse import urlparse, parse_qs - - def test_authorization_url_shape(): state = "teststate" redirect_uri = "https://hbd.example.com/login/oauth/gitea/callback" @@ -220,3 +218,44 @@ async def test_fetch_user_returns_profile(): "full_name": "Alice Smith", "avatar_url": "https://git.example.com/avatars/alice.png", } + + +@pytest.mark.asyncio +async def test_exchange_code_raises_when_no_access_token(): + redirect_uri = "https://hbd.example.com/login/oauth/gitea/callback" + mock_response = AsyncMock() + mock_response.status = 200 + mock_response.json = AsyncMock(return_value={"error": "bad_request"}) + + mock_session = MagicMock() + mock_session.post = MagicMock(return_value=AsyncMock( + __aenter__=AsyncMock(return_value=mock_response), + __aexit__=AsyncMock(return_value=False), + )) + + with patch("hbd.server.oauth.aiohttp.ClientSession", return_value=AsyncMock( + __aenter__=AsyncMock(return_value=mock_session), + __aexit__=AsyncMock(return_value=False), + )): + with pytest.raises(oauth.OAuthError): + await oauth.exchange_code(CFG_ON, "mycode", redirect_uri) + + +@pytest.mark.asyncio +async def test_fetch_user_raises_on_error_status(): + mock_response = AsyncMock() + mock_response.status = 401 + mock_response.text = AsyncMock(return_value="unauthorized") + + mock_session = MagicMock() + mock_session.get = MagicMock(return_value=AsyncMock( + __aenter__=AsyncMock(return_value=mock_response), + __aexit__=AsyncMock(return_value=False), + )) + + with patch("hbd.server.oauth.aiohttp.ClientSession", return_value=AsyncMock( + __aenter__=AsyncMock(return_value=mock_session), + __aexit__=AsyncMock(return_value=False), + )): + with pytest.raises(oauth.OAuthError): + await oauth.fetch_user(CFG_ON, "tok123")