test: add TestCallbackEndpoint tests to cover /callback auth endpoint
Agent-Logs-Url: https://github.com/christianlouis/dmarq/sessions/0683a057-d74b-4b1a-aeab-47f5bfbe4190 Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -16,7 +16,7 @@ Logto SDK calls are mocked so no live Logto instance is needed.
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import AsyncMock, MagicMock, patch
|
||||||
|
|
||||||
from fastapi.testclient import TestClient
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
@@ -207,6 +207,91 @@ class TestAuthMeEndpoint:
|
|||||||
assert res.status_code == 401
|
assert res.status_code == 401
|
||||||
|
|
||||||
|
|
||||||
|
# ── /api/v1/auth/callback ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
class TestCallbackEndpoint:
|
||||||
|
"""Tests for the Logto OIDC authorization-code callback handler."""
|
||||||
|
|
||||||
|
def _make_mock_claims(self):
|
||||||
|
claims = MagicMock()
|
||||||
|
claims.sub = "logto-sub-callback"
|
||||||
|
claims.email = "callback@example.com"
|
||||||
|
claims.name = "Callback User"
|
||||||
|
claims.username = None
|
||||||
|
claims.picture = None
|
||||||
|
claims.email_verified = True
|
||||||
|
return claims
|
||||||
|
|
||||||
|
def _mock_client(self, handle_error=None, claims_error=None, claims=None):
|
||||||
|
"""Build a mock LogtoClient with configurable side effects."""
|
||||||
|
mock_client = MagicMock()
|
||||||
|
if handle_error:
|
||||||
|
mock_client.handleSignInCallback = AsyncMock(side_effect=handle_error)
|
||||||
|
else:
|
||||||
|
mock_client.handleSignInCallback = AsyncMock()
|
||||||
|
if claims_error:
|
||||||
|
mock_client.getIdTokenClaims.side_effect = claims_error
|
||||||
|
elif claims is not None:
|
||||||
|
mock_client.getIdTokenClaims.return_value = claims
|
||||||
|
return mock_client
|
||||||
|
|
||||||
|
def test_callback_without_logto_config_returns_503(self, client: TestClient):
|
||||||
|
"""When Logto is not configured the callback must return 503."""
|
||||||
|
with patch("app.api.api_v1.endpoints.auth.settings") as mock_settings:
|
||||||
|
mock_settings.logto_configured = False
|
||||||
|
res = client.get("/api/v1/auth/callback", follow_redirects=False)
|
||||||
|
assert res.status_code == 503
|
||||||
|
|
||||||
|
def test_callback_handle_signin_error_redirects_to_callback_failed(self, client: TestClient):
|
||||||
|
"""If handleSignInCallback raises, redirect to /login?error=callback_failed."""
|
||||||
|
mock_client = self._mock_client(handle_error=Exception("bad state"))
|
||||||
|
with patch("app.api.api_v1.endpoints.auth.settings") as mock_settings:
|
||||||
|
mock_settings.logto_configured = True
|
||||||
|
with patch("app.api.api_v1.endpoints.auth.make_logto_client", return_value=mock_client):
|
||||||
|
res = client.get("/api/v1/auth/callback?code=bad", follow_redirects=False)
|
||||||
|
assert res.status_code == 302
|
||||||
|
assert "callback_failed" in res.headers["location"]
|
||||||
|
|
||||||
|
def test_callback_get_claims_error_redirects_to_token_error(self, client: TestClient):
|
||||||
|
"""If getIdTokenClaims raises, redirect to /login?error=token_error."""
|
||||||
|
mock_client = self._mock_client(claims_error=Exception("claims unavailable"))
|
||||||
|
with patch("app.api.api_v1.endpoints.auth.settings") as mock_settings:
|
||||||
|
mock_settings.logto_configured = True
|
||||||
|
with patch("app.api.api_v1.endpoints.auth.make_logto_client", return_value=mock_client):
|
||||||
|
res = client.get("/api/v1/auth/callback?code=x", follow_redirects=False)
|
||||||
|
assert res.status_code == 302
|
||||||
|
assert "token_error" in res.headers["location"]
|
||||||
|
|
||||||
|
def test_callback_success_issues_session_cookie_and_redirects_to_root(self, client: TestClient):
|
||||||
|
"""Successful callback must issue the dmarq_session cookie and redirect to /."""
|
||||||
|
claims = self._make_mock_claims()
|
||||||
|
mock_client = self._mock_client(claims=claims)
|
||||||
|
with patch("app.api.api_v1.endpoints.auth.settings") as mock_settings:
|
||||||
|
mock_settings.logto_configured = True
|
||||||
|
with patch("app.api.api_v1.endpoints.auth.make_logto_client", return_value=mock_client):
|
||||||
|
res = client.get("/api/v1/auth/callback?code=good", follow_redirects=False)
|
||||||
|
assert res.status_code == 302
|
||||||
|
assert res.headers["location"] == "/"
|
||||||
|
set_cookie = res.headers.get("set-cookie", "")
|
||||||
|
assert SESSION_COOKIE in set_cookie
|
||||||
|
|
||||||
|
def test_callback_success_respects_logto_next_cookie(self, client: TestClient):
|
||||||
|
"""After a successful callback the user is redirected to the stored next URL."""
|
||||||
|
claims = self._make_mock_claims()
|
||||||
|
mock_client = self._mock_client(claims=claims)
|
||||||
|
with patch("app.api.api_v1.endpoints.auth.settings") as mock_settings:
|
||||||
|
mock_settings.logto_configured = True
|
||||||
|
with patch("app.api.api_v1.endpoints.auth.make_logto_client", return_value=mock_client):
|
||||||
|
res = client.get(
|
||||||
|
"/api/v1/auth/callback?code=good",
|
||||||
|
cookies={"logto_next": "/dashboard"},
|
||||||
|
follow_redirects=False,
|
||||||
|
)
|
||||||
|
assert res.status_code == 302
|
||||||
|
assert res.headers["location"] == "/dashboard"
|
||||||
|
|
||||||
|
|
||||||
# ── /api/v1/auth/sign-in ─────────────────────────────────────────────────────
|
# ── /api/v1/auth/sign-in ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user