77import pytest
88from pydantic import SecretStr
99
10+ from ccproxy .auth .managers .token_snapshot import TokenSnapshot
1011from ccproxy .auth .oauth .protocol import StandardProfileFields
1112from ccproxy .plugins .copilot .config import CopilotOAuthConfig
1213from ccproxy .plugins .copilot .oauth .models import (
@@ -87,6 +88,7 @@ def mock_oauth_token(self) -> CopilotOAuthToken:
8788 return CopilotOAuthToken (
8889 access_token = SecretStr ("gho_test_token" ),
8990 token_type = "bearer" ,
91+ refresh_token = SecretStr ("gho_refresh_token" ),
9092 expires_in = 28800 , # 8 hours
9193 created_at = now ,
9294 scope = "read:user" ,
@@ -285,6 +287,7 @@ async def test_get_user_profile_success(
285287 provider_type = "copilot" ,
286288 email = "test@example.com" ,
287289 display_name = "Test User" ,
290+ features = {"copilot_access" : True },
288291 )
289292
290293 with patch .object (
@@ -367,6 +370,25 @@ async def test_get_token_info_success(
367370 assert result .account_type == "individual"
368371 assert result .oauth_expires_at is not None
369372 assert result .copilot_expires_at is not None
373+ assert result .copilot_access is True
374+
375+ async def test_get_token_info_falls_back_to_credentials (
376+ self ,
377+ oauth_provider : CopilotOAuthProvider ,
378+ mock_credentials : CopilotCredentials ,
379+ ) -> None :
380+ """copilot_access derived from credentials when profile unavailable."""
381+ oauth_provider .storage .load_credentials .return_value = mock_credentials # type: ignore[attr-defined]
382+
383+ with patch .object (
384+ oauth_provider , "get_user_profile" , new_callable = AsyncMock
385+ ) as mock_get_profile :
386+ mock_get_profile .side_effect = RuntimeError ("profile not available" )
387+
388+ result = await oauth_provider .get_token_info ()
389+
390+ assert isinstance (result , CopilotTokenInfo )
391+ assert result .copilot_access is True
370392
371393 async def test_get_token_info_no_credentials (
372394 self , oauth_provider : CopilotOAuthProvider
@@ -378,6 +400,40 @@ async def test_get_token_info_no_credentials(
378400
379401 assert result is None
380402
403+ async def test_get_token_snapshot_uses_manager (
404+ self , oauth_provider : CopilotOAuthProvider
405+ ) -> None :
406+ manager = AsyncMock ()
407+ manager .get_token_snapshot .return_value = TokenSnapshot (provider = "copilot" ) # type: ignore[attr-defined]
408+
409+ with patch .object (
410+ oauth_provider , "create_token_manager" , AsyncMock (return_value = manager )
411+ ) as mock_create :
412+ snapshot = await oauth_provider .get_token_snapshot ()
413+
414+ assert isinstance (snapshot , TokenSnapshot )
415+ assert snapshot .provider == "copilot"
416+ mock_create .assert_awaited_once ()
417+ manager .get_token_snapshot .assert_awaited_once ()
418+
419+ async def test_get_token_snapshot_fallback_to_credentials (
420+ self ,
421+ oauth_provider : CopilotOAuthProvider ,
422+ mock_credentials : CopilotCredentials ,
423+ ) -> None :
424+ oauth_provider .storage .load_credentials .return_value = mock_credentials # type: ignore[attr-defined]
425+
426+ with patch .object (
427+ oauth_provider ,
428+ "create_token_manager" ,
429+ AsyncMock (side_effect = RuntimeError ("boom" )),
430+ ):
431+ snapshot = await oauth_provider .get_token_snapshot ()
432+
433+ assert isinstance (snapshot , TokenSnapshot )
434+ assert snapshot .provider == "copilot"
435+ assert snapshot .has_refresh_token () is True
436+
381437 async def test_is_authenticated_with_valid_tokens (
382438 self ,
383439 oauth_provider : CopilotOAuthProvider ,
0 commit comments