33Licensed under the MIT License.
44"""
55
6- from unittest .mock import AsyncMock , MagicMock , patch
6+ from unittest .mock import MagicMock , patch
77
88import pytest
99from microsoft .teams .api import ClientCredentials , JsonWebToken
1010from microsoft .teams .apps .token_manager import TokenManager
11- from microsoft .teams .common import Client
1211
1312# Valid JWT-like token for testing (format: header.payload.signature)
1413VALID_TEST_TOKEN = (
@@ -23,204 +22,86 @@ class TestTokenManager:
2322
2423 @pytest .mark .asyncio
2524 async def test_get_bot_token_success (self ):
26- """Test successful bot token refresh, caching, and expiration refresh."""
27- # First token response
28- mock_token_response1 = MagicMock ()
29- mock_token_response1 .access_token = VALID_TEST_TOKEN
30-
31- # Second token response for expired token
32- mock_token_response2 = MagicMock ()
33- mock_token_response2 .access_token = (
34- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
35- "eyJzdWIiOiI5ODc2NTQzMjEwIiwibmFtZSI6IkphbmUgRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
36- "Twzj7LKlhYUUe2GFRME4WOZdWq2TdayZhWjhBr1r5X4"
37- )
38-
39- # Third token response for force refresh
40- mock_token_response3 = MagicMock ()
41- mock_token_response3 .access_token = (
42- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
43- "eyJzdWIiOiIxMTExMTExMTExIiwibmFtZSI6IkZvcmNlIFJlZnJlc2giLCJpYXQiOjE1MTYyMzkwMjJ9."
44- "dQw4w9WgXcQ"
45- )
46-
25+ """Test successful bot token retrieval using MSAL."""
4726 mock_credentials = ClientCredentials (
4827 client_id = "test-client-id" ,
4928 client_secret = "test-client-secret" ,
5029 tenant_id = "test-tenant-id" ,
5130 )
5231
53- # Mock the BotTokenClient
54- mock_bot_token_client = MagicMock ()
55- mock_bot_token_client .get = AsyncMock (
56- side_effect = [mock_token_response1 , mock_token_response2 , mock_token_response3 ]
57- )
32+ # Mock MSAL ConfidentialClientApplication
33+ mock_msal_app = MagicMock ()
34+ mock_msal_app .acquire_token_for_client = MagicMock (return_value = {"access_token" : VALID_TEST_TOKEN })
5835
59- mock_http_client = MagicMock ( spec = Client )
60- mock_http_client . clone = MagicMock ( return_value = mock_http_client )
36+ with patch ( "microsoft.teams.apps.token_manager.ConfidentialClientApplication" , return_value = mock_msal_app ):
37+ manager = TokenManager ( credentials = mock_credentials )
6138
62- with patch ("microsoft.teams.apps.token_manager.BotTokenClient" , return_value = mock_bot_token_client ):
63- manager = TokenManager (
64- credentials = mock_credentials ,
65- )
66-
67- # First call
68- token1 = await manager .get_bot_token ()
69- assert token1 is not None
70- assert isinstance (token1 , JsonWebToken )
71- mock_bot_token_client .get .assert_called_once ()
72-
73- # Second call should use cache (mock should still only be called once)
74- token2 = await manager .get_bot_token ()
75- assert token2 == token1
76- mock_bot_token_client .get .assert_called_once () # Still only called once due to caching
39+ token = await manager .get_bot_token ()
7740
78- # Mock the token as expired
79- token1 .is_expired = MagicMock (return_value = True )
41+ assert token is not None
42+ assert isinstance (token , JsonWebToken )
43+ assert str (token ) == VALID_TEST_TOKEN
8044
81- # Third call should refresh because token is expired
82- token3 = await manager .get_bot_token ()
83- assert token3 is not None
84- assert token3 != token1 # New token
85- assert mock_bot_token_client .get .call_count == 2
45+ # Verify MSAL was called with correct scope
46+ mock_msal_app .acquire_token_for_client .assert_called_once_with (["https://api.botframework.com/.default" ])
8647
8748 @pytest .mark .asyncio
8849 async def test_get_bot_token_no_credentials (self ):
89- """Test refreshing bot token with no credentials returns None."""
90- mock_http_client = MagicMock (spec = Client )
91- mock_http_client .clone = MagicMock (return_value = mock_http_client )
92-
93- with patch ("microsoft.teams.apps.token_manager.BotTokenClient" ):
94- manager = TokenManager (
95- credentials = None ,
96- )
97-
98- token = await manager .get_bot_token ()
99- assert token is None
50+ """Test getting bot token with no credentials returns None."""
51+ manager = TokenManager (credentials = None )
52+ token = await manager .get_bot_token ()
53+ assert token is None
10054
10155 @pytest .mark .asyncio
10256 async def test_get_graph_token_default (self ):
103- """Test getting default graph token with caching and expiration refresh."""
104- # First token response
105- mock_token_response1 = MagicMock ()
106- mock_token_response1 .access_token = VALID_TEST_TOKEN
107-
108- # Second token response for expired token
109- mock_token_response2 = MagicMock ()
110- mock_token_response2 .access_token = (
111- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
112- "eyJzdWIiOiI5ODc2NTQzMjEwIiwibmFtZSI6IkphbmUgRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ."
113- "Twzj7LKlhYUUe2GFRME4WOZdWq2TdayZhWjhBr1r5X4"
114- )
115-
57+ """Test getting default graph token using MSAL."""
11658 mock_credentials = ClientCredentials (
11759 client_id = "test-client-id" ,
11860 client_secret = "test-client-secret" ,
11961 tenant_id = "default-tenant-id" ,
12062 )
12163
122- # Mock the BotTokenClient
123- mock_bot_token_client = MagicMock ()
124- mock_bot_token_client .get_graph = AsyncMock (side_effect = [mock_token_response1 , mock_token_response2 ])
125-
126- mock_http_client = MagicMock (spec = Client )
127- mock_http_client .clone = MagicMock (return_value = mock_http_client )
128-
129- with patch ("microsoft.teams.apps.token_manager.BotTokenClient" , return_value = mock_bot_token_client ):
130- manager = TokenManager (
131- credentials = mock_credentials ,
132- )
133-
134- token1 = await manager .get_graph_token ()
64+ # Mock MSAL ConfidentialClientApplication
65+ mock_msal_app = MagicMock ()
66+ mock_msal_app .acquire_token_for_client = MagicMock (return_value = {"access_token" : VALID_TEST_TOKEN })
13567
136- assert token1 is not None
137- assert isinstance ( token1 , JsonWebToken )
68+ with patch ( "microsoft.teams.apps.token_manager.ConfidentialClientApplication" , return_value = mock_msal_app ):
69+ manager = TokenManager ( credentials = mock_credentials )
13870
139- # Verify it's cached
140- token2 = await manager .get_graph_token ()
141- assert token2 == token1
142- mock_bot_token_client .get_graph .assert_called_once ()
71+ token = await manager .get_graph_token ()
14372
144- # Mock the token as expired
145- token1 .is_expired = MagicMock (return_value = True )
73+ assert token is not None
74+ assert isinstance (token , JsonWebToken )
75+ assert str (token ) == VALID_TEST_TOKEN
14676
147- # Third call should refresh because token is expired
148- token3 = await manager .get_graph_token ()
149- assert token3 is not None
150- assert token3 != token1 # New token
151- assert mock_bot_token_client .get_graph .call_count == 2
77+ # Verify MSAL was called with correct scope
78+ mock_msal_app .acquire_token_for_client .assert_called_once_with (["https://graph.microsoft.com/.default" ])
15279
15380 @pytest .mark .asyncio
15481 async def test_get_graph_token_with_tenant (self ):
155- """Test getting tenant-specific graph token."""
156- mock_token_response = MagicMock ()
157- mock_token_response .access_token = VALID_TEST_TOKEN
158-
82+ """Test getting tenant-specific graph token using MSAL."""
15983 original_credentials = ClientCredentials (
16084 client_id = "test-client-id" ,
16185 client_secret = "test-client-secret" ,
16286 tenant_id = "original-tenant-id" ,
16387 )
16488
165- # Mock the BotTokenClient
166- mock_bot_token_client = MagicMock ()
167- mock_bot_token_client .get_graph = AsyncMock (return_value = mock_token_response )
168-
169- mock_http_client = MagicMock (spec = Client )
170- mock_http_client .clone = MagicMock (return_value = mock_http_client )
89+ # Mock MSAL ConfidentialClientApplication
90+ mock_msal_app = MagicMock ()
91+ mock_msal_app .acquire_token_for_client = MagicMock (return_value = {"access_token" : VALID_TEST_TOKEN })
17192
172- with patch ("microsoft.teams.apps.token_manager.BotTokenClient" , return_value = mock_bot_token_client ):
173- manager = TokenManager (
174- credentials = original_credentials ,
175- )
93+ with patch (
94+ "microsoft.teams.apps.token_manager.ConfidentialClientApplication" , return_value = mock_msal_app
95+ ) as mock_msal_class :
96+ manager = TokenManager ( credentials = original_credentials )
17697
17798 token = await manager .get_graph_token ("different-tenant-id" )
17899
179100 assert token is not None
180- mock_bot_token_client .get_graph .assert_called_once ()
181-
182- # Verify tenant-specific credentials were created
183- call_args = mock_bot_token_client .get_graph .call_args
184- passed_credentials = call_args [0 ][0 ]
185- assert isinstance (passed_credentials , ClientCredentials )
186- assert passed_credentials .tenant_id == "different-tenant-id"
187-
188- @pytest .mark .asyncio
189- async def test_graph_token_force_refresh (self ):
190- """Test force refreshing graph token even when not expired."""
191- mock_token_response1 = MagicMock ()
192- mock_token_response1 .access_token = VALID_TEST_TOKEN
193-
194- mock_token_response2 = MagicMock ()
195- mock_token_response2 .access_token = (
196- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9."
197- "eyJzdWIiOiIxMTExMTExMTExIiwibmFtZSI6IkZvcmNlIFJlZnJlc2giLCJpYXQiOjE1MTYyMzkwMjJ9."
198- "dQw4w9WgXcQ"
199- )
200-
201- mock_credentials = ClientCredentials (
202- client_id = "test-client-id" ,
203- client_secret = "test-client-secret" ,
204- tenant_id = "test-tenant-id" ,
205- )
206-
207- mock_bot_token_client = MagicMock ()
208- mock_bot_token_client .get_graph = AsyncMock (side_effect = [mock_token_response1 , mock_token_response2 ])
209-
210- mock_http_client = MagicMock (spec = Client )
211- mock_http_client .clone = MagicMock (return_value = mock_http_client )
212-
213- with patch ("microsoft.teams.apps.token_manager.BotTokenClient" , return_value = mock_bot_token_client ):
214- manager = TokenManager (
215- credentials = mock_credentials ,
216- )
217-
218- # First call
219- token1 = await manager .get_graph_token ()
220- assert token1 is not None
221- mock_bot_token_client .get_graph .assert_called_once ()
101+ assert isinstance (token , JsonWebToken )
222102
223- # Second call should use cache
224- token2 = await manager .get_graph_token ()
225- assert token2 == token1
226- mock_bot_token_client .get_graph .assert_called_once () # Still only called once
103+ # Verify MSAL app was created with different tenant ID
104+ # The manager caches MSAL clients, so we check the call to the class constructor
105+ calls = mock_msal_class .call_args_list
106+ # Should have been called with different-tenant-id
107+ assert any ("different-tenant-id" in str (call ) for call in calls )
0 commit comments