From 32351a21349f296bb0a44da3aa3688c68ca78ab2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 31 Mar 2026 08:57:05 +0000 Subject: [PATCH] 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> --- backend/app/tests/test_auth.py | 87 +++++++++++++++++++++++++++++++++- 1 file changed, 86 insertions(+), 1 deletion(-) diff --git a/backend/app/tests/test_auth.py b/backend/app/tests/test_auth.py index 5fb2921..6e4cf5c 100644 --- a/backend/app/tests/test_auth.py +++ b/backend/app/tests/test_auth.py @@ -16,7 +16,7 @@ Logto SDK calls are mocked so no live Logto instance is needed. from __future__ import annotations -from unittest.mock import MagicMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from fastapi.testclient import TestClient @@ -207,6 +207,91 @@ class TestAuthMeEndpoint: 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 ─────────────────────────────────────────────────────