"""Tests for TokenManager (sync) and AsyncTokenManager.""" import time import pytest import responses as resp_lib import respx import httpx from sentryagent_idp.token_manager import TokenManager, REFRESH_BUFFER_SECONDS from sentryagent_idp.async_token_manager import AsyncTokenManager from sentryagent_idp.errors import AgentIdPError BASE_URL = "http://localhost:3000" TOKEN_URL = f"{BASE_URL}/api/v1/token" TOKEN_RESP = { "access_token": "eyJ.abc.def", "token_type": "Bearer", "expires_in": 3600, "scope": "agents:read", } # ─── Sync TokenManager ──────────────────────────────────────────────────────── @resp_lib.activate def test_token_manager_issues_token() -> None: resp_lib.add(resp_lib.POST, TOKEN_URL, json=TOKEN_RESP, status=200) tm = TokenManager(BASE_URL, "client-id", "secret", "agents:read") token = tm.get_token() assert token == "eyJ.abc.def" assert len(resp_lib.calls) == 1 @resp_lib.activate def test_token_manager_caches_token() -> None: resp_lib.add(resp_lib.POST, TOKEN_URL, json=TOKEN_RESP, status=200) tm = TokenManager(BASE_URL, "client-id", "secret", "agents:read") tm.get_token() tm.get_token() # Only one HTTP call because second call uses cache assert len(resp_lib.calls) == 1 @resp_lib.activate def test_token_manager_refreshes_near_expiry() -> None: resp_lib.add(resp_lib.POST, TOKEN_URL, json={**TOKEN_RESP, "expires_in": 30}, status=200) resp_lib.add(resp_lib.POST, TOKEN_URL, json=TOKEN_RESP, status=200) tm = TokenManager(BASE_URL, "client-id", "secret", "agents:read") tm.get_token() # Simulate cached token being nearly expired assert tm._cached is not None tm._cached.expires_at = time.time() + (REFRESH_BUFFER_SECONDS - 1) tm.get_token() assert len(resp_lib.calls) == 2 @resp_lib.activate def test_token_manager_raises_on_auth_failure() -> None: resp_lib.add( resp_lib.POST, TOKEN_URL, json={"error": "invalid_client", "error_description": "Bad creds."}, status=401, ) tm = TokenManager(BASE_URL, "client-id", "bad-secret", "agents:read") with pytest.raises(AgentIdPError) as exc_info: tm.get_token() assert exc_info.value.code == "invalid_client" assert exc_info.value.http_status == 401 @resp_lib.activate def test_token_manager_clear_cache() -> None: resp_lib.add(resp_lib.POST, TOKEN_URL, json=TOKEN_RESP, status=200) resp_lib.add(resp_lib.POST, TOKEN_URL, json=TOKEN_RESP, status=200) tm = TokenManager(BASE_URL, "client-id", "secret", "agents:read") tm.get_token() tm.clear_cache() tm.get_token() assert len(resp_lib.calls) == 2 # ─── Async TokenManager ─────────────────────────────────────────────────────── @pytest.mark.asyncio async def test_async_token_manager_issues_token() -> None: with respx.mock: respx.post(TOKEN_URL).mock(return_value=httpx.Response(200, json=TOKEN_RESP)) tm = AsyncTokenManager(BASE_URL, "client-id", "secret", "agents:read") token = await tm.get_token() assert token == "eyJ.abc.def" @pytest.mark.asyncio async def test_async_token_manager_caches_token() -> None: with respx.mock: route = respx.post(TOKEN_URL).mock(return_value=httpx.Response(200, json=TOKEN_RESP)) tm = AsyncTokenManager(BASE_URL, "client-id", "secret", "agents:read") await tm.get_token() await tm.get_token() assert route.call_count == 1 @pytest.mark.asyncio async def test_async_token_manager_raises_on_auth_failure() -> None: with respx.mock: respx.post(TOKEN_URL).mock(return_value=httpx.Response( 401, json={"error": "invalid_client", "error_description": "Bad creds."} )) tm = AsyncTokenManager(BASE_URL, "client-id", "bad-secret", "agents:read") with pytest.raises(AgentIdPError) as exc_info: await tm.get_token() assert exc_info.value.code == "invalid_client"