|
| 1 | +from unittest.mock import AsyncMock, MagicMock, patch |
| 2 | + |
1 | 3 | import pytest |
2 | 4 | from fastapi import FastAPI |
3 | 5 | from fastapi.testclient import TestClient |
4 | 6 | from starlette.middleware.sessions import SessionMiddleware |
5 | 7 |
|
6 | 8 | from carbonserver.api.routers import authenticate |
| 9 | +from carbonserver.api.services.auth_providers.oidc_auth_provider import ( |
| 10 | + OIDCAuthProvider, |
| 11 | +) |
7 | 12 | from carbonserver.container import ServerContainer |
8 | 13 |
|
9 | 14 | SESSION_COOKIE_NAME = "user_session" |
@@ -55,3 +60,89 @@ class FakeRequest: |
55 | 60 | ) |
56 | 61 | # We cannot directly check session cleared, but can check that logout returns redirect |
57 | 62 | assert "window.location.href" in response.text |
| 63 | + |
| 64 | + |
| 65 | +# --- Token revocation tests --- |
| 66 | + |
| 67 | + |
| 68 | +@pytest.fixture |
| 69 | +def mock_oidc_client(): |
| 70 | + """Create a mock OIDC client with load_server_metadata and _get_oauth_client.""" |
| 71 | + client = MagicMock() |
| 72 | + client.load_server_metadata = AsyncMock() |
| 73 | + client._get_oauth_client = MagicMock() |
| 74 | + return client |
| 75 | + |
| 76 | + |
| 77 | +@pytest.fixture |
| 78 | +def oidc_provider(mock_oidc_client): |
| 79 | + """Create an OIDCAuthProvider with a mocked client.""" |
| 80 | + with patch.object(OIDCAuthProvider, "__init__", lambda self, **kw: None): |
| 81 | + provider = OIDCAuthProvider() |
| 82 | + provider.client = mock_oidc_client |
| 83 | + return provider |
| 84 | + |
| 85 | + |
| 86 | +@pytest.mark.asyncio |
| 87 | +async def test_revoke_token_success(oidc_provider, mock_oidc_client): |
| 88 | + """Token is revoked successfully when the provider exposes a revocation_endpoint.""" |
| 89 | + mock_oidc_client.load_server_metadata.return_value = { |
| 90 | + "revocation_endpoint": "https://auth.example.com/revoke", |
| 91 | + } |
| 92 | + |
| 93 | + mock_response = MagicMock(status_code=200) |
| 94 | + mock_http_client = AsyncMock() |
| 95 | + mock_http_client.request = AsyncMock(return_value=mock_response) |
| 96 | + mock_http_client.__aenter__ = AsyncMock(return_value=mock_http_client) |
| 97 | + mock_http_client.__aexit__ = AsyncMock(return_value=False) |
| 98 | + mock_oidc_client._get_oauth_client.return_value = mock_http_client |
| 99 | + |
| 100 | + await oidc_provider.revoke_token("test-access-token") |
| 101 | + |
| 102 | + mock_http_client.request.assert_called_once_with( |
| 103 | + "POST", |
| 104 | + "https://auth.example.com/revoke", |
| 105 | + withhold_token=True, |
| 106 | + data={"token": "test-access-token", "token_type_hint": "access_token"}, |
| 107 | + ) |
| 108 | + |
| 109 | + |
| 110 | +@pytest.mark.asyncio |
| 111 | +async def test_revoke_token_no_endpoint(oidc_provider, mock_oidc_client): |
| 112 | + """Revocation is silently skipped when the provider has no revocation_endpoint.""" |
| 113 | + mock_oidc_client.load_server_metadata.return_value = { |
| 114 | + "authorization_endpoint": "https://auth.example.com/authorize", |
| 115 | + } |
| 116 | + |
| 117 | + await oidc_provider.revoke_token("test-access-token") |
| 118 | + |
| 119 | + mock_oidc_client._get_oauth_client.assert_not_called() |
| 120 | + |
| 121 | + |
| 122 | +@pytest.mark.asyncio |
| 123 | +async def test_revoke_token_http_error(oidc_provider, mock_oidc_client): |
| 124 | + """Revocation failure does not raise — logout must always succeed.""" |
| 125 | + mock_oidc_client.load_server_metadata.return_value = { |
| 126 | + "revocation_endpoint": "https://auth.example.com/revoke", |
| 127 | + } |
| 128 | + |
| 129 | + mock_response = MagicMock(status_code=503, text="Service Unavailable") |
| 130 | + mock_http_client = AsyncMock() |
| 131 | + mock_http_client.request = AsyncMock(return_value=mock_response) |
| 132 | + mock_http_client.__aenter__ = AsyncMock(return_value=mock_http_client) |
| 133 | + mock_http_client.__aexit__ = AsyncMock(return_value=False) |
| 134 | + mock_oidc_client._get_oauth_client.return_value = mock_http_client |
| 135 | + |
| 136 | + # Should not raise |
| 137 | + await oidc_provider.revoke_token("test-access-token") |
| 138 | + |
| 139 | + |
| 140 | +@pytest.mark.asyncio |
| 141 | +async def test_revoke_token_exception(oidc_provider, mock_oidc_client): |
| 142 | + """Revocation is non-blocking even when load_server_metadata raises.""" |
| 143 | + mock_oidc_client.load_server_metadata.side_effect = ConnectionError( |
| 144 | + "Network unreachable" |
| 145 | + ) |
| 146 | + |
| 147 | + # Should not raise |
| 148 | + await oidc_provider.revoke_token("test-access-token") |
0 commit comments