fix: guard unconfigured oauth calls; add missing test coverage; clean imports
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+9
-3
@@ -71,6 +71,8 @@ def is_enabled(config: dict) -> bool:
|
|||||||
def authorization_url(config: dict, state: str, redirect_uri: str) -> str:
|
def authorization_url(config: dict, state: str, redirect_uri: str) -> str:
|
||||||
"""Return the Gitea OAuth2 authorization URL to redirect the browser to."""
|
"""Return the Gitea OAuth2 authorization URL to redirect the browser to."""
|
||||||
g = _gitea_cfg(config)
|
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({
|
params = urllib.parse.urlencode({
|
||||||
"client_id": g["client_id"],
|
"client_id": g["client_id"],
|
||||||
"redirect_uri": redirect_uri,
|
"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.
|
Returns the access token string. Raises OAuthError on any failure.
|
||||||
"""
|
"""
|
||||||
g = _gitea_cfg(config)
|
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"
|
url = f"{g['url'].rstrip('/')}/login/oauth/access_token"
|
||||||
payload = {
|
payload = {
|
||||||
"client_id": g["client_id"],
|
"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()
|
text = await resp.text()
|
||||||
raise OAuthError(f"Token exchange failed ({resp.status}): {text}")
|
raise OAuthError(f"Token exchange failed ({resp.status}): {text}")
|
||||||
data = await resp.json()
|
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:
|
except aiohttp.ClientError as exc:
|
||||||
raise OAuthError(f"Token exchange network error: {exc}") from 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
|
return token
|
||||||
|
|
||||||
|
|
||||||
@@ -118,6 +122,8 @@ async def fetch_user(config: dict, token: str) -> dict:
|
|||||||
Raises OAuthError on any failure.
|
Raises OAuthError on any failure.
|
||||||
"""
|
"""
|
||||||
g = _gitea_cfg(config)
|
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"
|
url = f"{g['url'].rstrip('/')}/api/v1/user"
|
||||||
timeout = aiohttp.ClientTimeout(total=10)
|
timeout = aiohttp.ClientTimeout(total=10)
|
||||||
try:
|
try:
|
||||||
|
|||||||
+43
-4
@@ -1,4 +1,6 @@
|
|||||||
import time as time_mod
|
import time as time_mod
|
||||||
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
from urllib.parse import urlparse, parse_qs
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
@@ -132,10 +134,6 @@ def test_provision_oauth_user_survives_config_reload():
|
|||||||
assert "oauthonly" in users_mod.users
|
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():
|
def test_authorization_url_shape():
|
||||||
state = "teststate"
|
state = "teststate"
|
||||||
redirect_uri = "https://hbd.example.com/login/oauth/gitea/callback"
|
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",
|
"full_name": "Alice Smith",
|
||||||
"avatar_url": "https://git.example.com/avatars/alice.png",
|
"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")
|
||||||
|
|||||||
Reference in New Issue
Block a user