Skip to content

Commit 8375a46

Browse files
committed
add test
1 parent 042a68d commit 8375a46

File tree

3 files changed

+195
-3
lines changed

3 files changed

+195
-3
lines changed

packages/apps/src/microsoft/teams/apps/token_manager.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ async def _get_token(
9090
return JsonWebToken(access_token)
9191
else:
9292
self._logger.debug(f"TokenRes: {token_res}")
93-
error = token_res.get("error", ValueError("Error retrieving token"))
93+
error = token_res.get("error", "Error retrieving token")
9494
if not isinstance(error, BaseException):
9595
error = ValueError(error)
9696
error_description = token_res.get("error_description", "Error retrieving token from MSAL")

packages/apps/tests/test_app.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
Account,
1414
ConversationAccount,
1515
InvokeActivity,
16+
ManagedIdentityCredentials,
1617
MessageActivity,
1718
TokenCredentials,
1819
TokenProtocol,
@@ -575,3 +576,102 @@ def test_user_agent_format(self, app_with_options: App):
575576
# Verify the http_client has the correct User-Agent header
576577
assert "User-Agent" in app_with_options.http_client._options.headers
577578
assert app_with_options.http_client._options.headers["User-Agent"] == expected_user_agent
579+
580+
@pytest.mark.parametrize(
581+
"options_dict,env_vars,expected_client_id,expected_tenant_id,description",
582+
[
583+
# Inferred from client_id only
584+
(
585+
{"client_id": "test-managed-identity-client-id"},
586+
{"CLIENT_SECRET": "", "TENANT_ID": "test-tenant-id"},
587+
"test-managed-identity-client-id",
588+
"test-tenant-id",
589+
"inferred from client_id only",
590+
),
591+
# managed_identity_client_id equals client_id (valid)
592+
(
593+
{"client_id": "test-client-id", "managed_identity_client_id": "test-client-id"},
594+
{"CLIENT_SECRET": "", "TENANT_ID": "test-tenant-id"},
595+
"test-client-id",
596+
"test-tenant-id",
597+
"managed_identity_client_id equals client_id",
598+
),
599+
# From environment variables
600+
(
601+
{},
602+
{"CLIENT_ID": "env-managed-identity-client-id", "CLIENT_SECRET": "", "TENANT_ID": "env-tenant-id"},
603+
"env-managed-identity-client-id",
604+
"env-tenant-id",
605+
"from environment variables",
606+
),
607+
# Explicit managed_identity_client_id
608+
(
609+
{
610+
"client_id": "test-app-id",
611+
"managed_identity_client_id": "test-app-id",
612+
"tenant_id": "test-tenant-id",
613+
},
614+
{"CLIENT_SECRET": ""},
615+
"test-app-id",
616+
"test-tenant-id",
617+
"explicit managed_identity_client_id",
618+
),
619+
],
620+
)
621+
def test_app_init_with_managed_identity(
622+
self,
623+
mock_logger,
624+
mock_storage,
625+
options_dict: dict,
626+
env_vars: dict,
627+
expected_client_id: str,
628+
expected_tenant_id: str,
629+
description: str,
630+
):
631+
"""Test app initialization with managed identity credentials."""
632+
options = AppOptions(logger=mock_logger, storage=mock_storage, **options_dict)
633+
634+
with patch.dict("os.environ", env_vars, clear=False):
635+
app = App(**options)
636+
637+
assert app.credentials is not None, f"Failed for: {description}"
638+
assert isinstance(app.credentials, ManagedIdentityCredentials), f"Failed for: {description}"
639+
assert app.credentials.client_id == expected_client_id, f"Failed for: {description}"
640+
assert app.credentials.tenant_id == expected_tenant_id, f"Failed for: {description}"
641+
642+
def test_app_init_with_managed_identity_client_id_mismatch(self, mock_logger, mock_storage):
643+
"""Test app init raises error when managed_identity_client_id != client_id (federated identity)."""
644+
# When managed_identity_client_id differs from client_id, should raise error
645+
# (Federated Identity Credentials not yet supported)
646+
options = AppOptions(
647+
logger=mock_logger,
648+
storage=mock_storage,
649+
client_id="app-client-id",
650+
managed_identity_client_id="different-managed-identity-id", # Different!
651+
)
652+
653+
with patch.dict("os.environ", {"CLIENT_SECRET": "", "TENANT_ID": "test-tenant-id"}, clear=False):
654+
with pytest.raises(ValueError) as exc_info:
655+
App(**options)
656+
657+
assert "Federated Identity Credentials is not yet supported" in str(exc_info.value)
658+
assert "managed_identity_client_id must equal client_id" in str(exc_info.value)
659+
660+
def test_app_init_with_client_secret_takes_precedence(self, mock_logger, mock_storage):
661+
"""Test that ClientCredentials is used when both client_secret and managed_identity_client_id are provided."""
662+
# When client_secret is provided, it should take precedence over managed identity
663+
options = AppOptions(
664+
logger=mock_logger,
665+
storage=mock_storage,
666+
client_id="test-client-id",
667+
client_secret="test-client-secret",
668+
managed_identity_client_id="test-managed-id", # This should be ignored
669+
tenant_id="test-tenant-id",
670+
)
671+
672+
app = App(**options)
673+
674+
assert app.credentials is not None
675+
# Should use ClientCredentials, not ManagedIdentityCredentials
676+
assert type(app.credentials).__name__ == "ClientCredentials"
677+
assert app.credentials.client_id == "test-client-id"

packages/apps/tests/test_token_manager.py

Lines changed: 94 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@
33
Licensed under the MIT License.
44
"""
55

6-
from unittest.mock import MagicMock, patch
6+
from unittest.mock import MagicMock, create_autospec, patch
77

88
import pytest
9-
from microsoft.teams.api import ClientCredentials, JsonWebToken
9+
from microsoft.teams.api import ClientCredentials, JsonWebToken, ManagedIdentityCredentials
1010
from microsoft.teams.apps.token_manager import TokenManager
11+
from msal import ManagedIdentityClient # pyright: ignore[reportMissingTypeStubs]
1112

1213
# Valid JWT-like token for testing (format: header.payload.signature)
1314
VALID_TEST_TOKEN = (
@@ -105,3 +106,94 @@ async def test_get_graph_token_with_tenant(self):
105106
calls = mock_msal_class.call_args_list
106107
# Should have been called with different-tenant-id
107108
assert any("different-tenant-id" in str(call) for call in calls)
109+
110+
@pytest.mark.asyncio
111+
@pytest.mark.parametrize(
112+
"get_token_method,expected_resource",
113+
[
114+
("get_bot_token", "https://api.botframework.com"),
115+
("get_graph_token", "https://graph.microsoft.com"),
116+
],
117+
)
118+
async def test_get_token_with_managed_identity(self, get_token_method: str, expected_resource: str):
119+
"""Test token retrieval using ManagedIdentityCredentials."""
120+
mock_credentials = ManagedIdentityCredentials(
121+
client_id="test-managed-identity-client-id",
122+
tenant_id="test-tenant-id",
123+
)
124+
125+
# Create a mock that will pass isinstance checks
126+
mock_msal_client = create_autospec(ManagedIdentityClient, instance=True)
127+
mock_msal_client.acquire_token_for_client.return_value = {"access_token": VALID_TEST_TOKEN}
128+
129+
manager = TokenManager(credentials=mock_credentials)
130+
131+
# Patch _get_msal_client to return our mock
132+
with patch.object(manager, "_get_msal_client", return_value=mock_msal_client):
133+
# Call the method dynamically
134+
token = await getattr(manager, get_token_method)()
135+
136+
assert token is not None
137+
assert isinstance(token, JsonWebToken)
138+
assert str(token) == VALID_TEST_TOKEN
139+
140+
# Verify MSAL was called with resource parameter (not scopes list)
141+
# and without /.default suffix
142+
mock_msal_client.acquire_token_for_client.assert_called_once_with(resource=expected_resource)
143+
144+
@pytest.mark.asyncio
145+
async def test_get_graph_token_with_managed_identity_and_tenant(self):
146+
"""Test getting tenant-specific graph token with ManagedIdentityCredentials."""
147+
mock_credentials = ManagedIdentityCredentials(
148+
client_id="test-managed-identity-client-id",
149+
tenant_id="original-tenant-id",
150+
)
151+
152+
# Create a mock that will pass isinstance checks
153+
mock_msal_client = create_autospec(ManagedIdentityClient, instance=True)
154+
mock_msal_client.acquire_token_for_client.return_value = {"access_token": VALID_TEST_TOKEN}
155+
156+
manager = TokenManager(credentials=mock_credentials)
157+
158+
# Track calls to _get_msal_client
159+
get_msal_client_calls: list[str] = []
160+
161+
def track_get_msal_client(tenant_id: str):
162+
get_msal_client_calls.append(tenant_id)
163+
return mock_msal_client
164+
165+
# Patch _get_msal_client to track calls
166+
with patch.object(manager, "_get_msal_client", side_effect=track_get_msal_client):
167+
# Request token for different tenant
168+
token = await manager.get_graph_token("different-tenant-id")
169+
170+
assert token is not None
171+
assert isinstance(token, JsonWebToken)
172+
173+
# Verify _get_msal_client was called with different-tenant-id
174+
assert "different-tenant-id" in get_msal_client_calls
175+
176+
@pytest.mark.asyncio
177+
async def test_get_token_error_handling_with_managed_identity(self):
178+
"""Test error handling when token acquisition fails with ManagedIdentityCredentials."""
179+
mock_credentials = ManagedIdentityCredentials(
180+
client_id="test-managed-identity-client-id",
181+
tenant_id="test-tenant-id",
182+
)
183+
184+
# Create a mock that returns an error
185+
mock_msal_client = create_autospec(ManagedIdentityClient, instance=True)
186+
mock_msal_client.acquire_token_for_client.return_value = {
187+
"error": "invalid_client",
188+
"error_description": "Invalid managed identity configuration",
189+
}
190+
191+
manager = TokenManager(credentials=mock_credentials)
192+
193+
# Patch _get_msal_client to return our mock
194+
with patch.object(manager, "_get_msal_client", return_value=mock_msal_client):
195+
# Should raise an error when token acquisition fails
196+
with pytest.raises(ValueError) as exc_info:
197+
await manager.get_bot_token()
198+
199+
assert "invalid_client" in str(exc_info.value)

0 commit comments

Comments
 (0)