From 2b4201e059848a3d3745eb6c8c21dd607f310b14 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 22 Jul 2025 13:46:03 -0700 Subject: [PATCH 01/14] MsalAuth test cases --- .../tests/test_msal_auth.py | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py diff --git a/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py b/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py new file mode 100644 index 00000000..b3b75406 --- /dev/null +++ b/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py @@ -0,0 +1,56 @@ +import unittest +from unittest.mock import Mock +import pytest +from msal import ManagedIdentityClient, ConfidentialClientApplication +from microsoft.agents.authentication.msal import MsalAuth +from microsoft.agents.hosting.core.authorization import AgentAuthConfiguration + +class TestMsalAuth: + """ + Test suite for testing MsalAuth functionality + """ + def setup_method(self): + """Set up test fixtures for each test method.""" + self.mock_client = Mock() + self.mock_client.acquire_token_for_client.return_value = {"access_token": "token"} + self.mock_client.acquire_token_on_behalf_of.return_value = {"access_token": "token"} + self.auth = MsalAuth(AgentAuthConfiguration()) + self.auth._create_client_application = Mock(return_value=self.mock_client) + + @pytest.mark.asyncio + async def test_get_access_token_managed_identity(self): + self.mock_client.mock_add_spec(ManagedIdentityClient) + token = await self.auth.get_access_token("https://test.api.botframework.com", scopes=["test-scope"]) + + assert token == "token" + self.mock_client.acquire_token_for_client.assert_called_with(resource="https://test.api.botframework.com") + + @pytest.mark.asyncio + async def test_get_access_token_confidential(self): + self.mock_client.mock_add_spec(ConfidentialClientApplication) + token = await self.auth.get_access_token("https://test.api.botframework.com", scopes=["test-scope"]) + + assert token == "token" + self.mock_client.acquire_token_for_client.assert_called_with(scopes=["test-scope"]) + + @pytest.mark.asyncio + async def test_aquire_token_on_behalf_of_managed_identity(self): + self.mock_client.mock_add_spec(ManagedIdentityClient) + + try: + await self.auth.aquire_token_on_behalf_of(scopes=["test-scope"], user_assertion="test-assertion") + except NotImplementedError: + assert True + else: + assert False + + + @pytest.mark.asyncio + async def test_aquire_token_on_behalf_of_confidential(self): + self.mock_client.mock_add_spec(ConfidentialClientApplication) + self.auth._create_client_application = Mock(return_value=self.mock_client) + + token = await self.auth.aquire_token_on_behalf_of(scopes=["test-scope"], user_assertion="test-assertion") + + assert token == "token" + self.mock_client.acquire_token_on_behalf_of.assert_called_with(scopes=["test-scope"], user_assertion="test-assertion") \ No newline at end of file From 6e5fedce669c255917c51989a65fac4ba600d7c8 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 22 Jul 2025 13:53:43 -0700 Subject: [PATCH 02/14] add a couple AgentAuthConfiguration tests --- .../tests/test_auth_configuration.py | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 libraries/microsoft-agents-hosting-core/tests/test_auth_configuration.py diff --git a/libraries/microsoft-agents-hosting-core/tests/test_auth_configuration.py b/libraries/microsoft-agents-hosting-core/tests/test_auth_configuration.py new file mode 100644 index 00000000..138fe670 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/tests/test_auth_configuration.py @@ -0,0 +1,78 @@ +from os import environ +from typing import Dict +from microsoft.agents.activity import load_configuration_from_env +from microsoft.agents.hosting.core import AgentAuthConfiguration, AuthTypes + +class TestAuthorizationConfiguration: + """ + Unit tests to validate Authorization Configuration cases + """ + + def test_auth_configuration_basic(self): + # test AgentAuthConfiguration with manual insertion of fields + auth_config = AgentAuthConfiguration( + auth_type=AuthTypes.client_secret, + tenant_id="test-tenant-id", + client_id="test-client-id", + client_secret="test-client-secret", + cert_pem_file="test-cert.pem", + cert_key_file="test-cert.key", + connection_name="test-connection", + authority="https://login.microsoftonline.com", + scopes=["test-scope-1", "test-scope-2"], + ) + + assert auth_config.AUTH_TYPE == AuthTypes.client_secret + assert auth_config.TENANT_ID == "test-tenant-id" + assert auth_config.CLIENT_ID == "test-client-id" + assert auth_config.CLIENT_SECRET == "test-client-secret" + assert auth_config.CERT_PEM_FILE == "test-cert.pem" + assert auth_config.CERT_KEY_FILE == "test-cert.key" + assert auth_config.CONNECTION_NAME == "test-connection" + assert auth_config.AUTHORITY == "https://login.microsoftonline.com" + assert auth_config.SCOPES == ["test-scope-1", "test-scope-2"] + assert auth_config.ISSUERS == [ + "https://api.botframework.com", + f"https://sts.windows.net/test-tenant-id/", + f"https://login.microsoftonline.com/test-tenant-id/v2.0", + ] + + def test_load_configuration_from_env(self): + # test load_configuration_from_env, passed to AgentAuthConfiguration + mock_environ = { + **environ, + "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID": "test-tenant-id-SERVICE_CONNECTION", + "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID": "test-client-id-SERVICE_CONNECTION", + "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET": "test-client-secret-SERVICE_CONNECTION", + "CONNECTIONS__MCS__SETTINGS__TENANTID": "test-tenant-id-MCS", + "CONNECTIONS__MCS__SETTINGS__CLIENTID": "test-client-id-MCS", + "CONNECTIONS__MCS__SETTINGS__CLIENTSECRET": "test-client-secret-MCS", + } + + mock_config = load_configuration_from_env(mock_environ) + + raw_configurations: Dict[str, Dict] = mock_config.get("CONNECTIONS", {}) + + for name, settings in raw_configurations.items(): + auth_config = AgentAuthConfiguration(**settings["SETTINGS"]) + assert auth_config.AUTH_TYPE == AuthTypes.client_secret + assert auth_config.CLIENT_ID == f"test-client-id-{name}" + assert auth_config.TENANT_ID == f"test-tenant-id-{name}" + assert auth_config.CLIENT_SECRET == f"test-client-secret-{name}" + assert auth_config.ISSUERS == [ + "https://api.botframework.com", + f"https://sts.windows.net/test-tenant-id-{name}/", + f"https://login.microsoftonline.com/test-tenant-id-{name}/v2.0", + ] + + def test_empty_settings(self): + auth_config = AgentAuthConfiguration() + assert auth_config.AUTH_TYPE == AuthTypes.client_secret + assert auth_config.TENANT_ID == None + assert auth_config.CLIENT_ID == None + assert auth_config.CLIENT_SECRET == None + assert auth_config.CERT_PEM_FILE == None + assert auth_config.CERT_KEY_FILE == None + assert auth_config.CONNECTION_NAME == None + assert auth_config.AUTHORITY == None + assert auth_config.SCOPES == None \ No newline at end of file From 7aff0219dc4a8cf4a81cc2c2b12893a76f254402 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 22 Jul 2025 13:54:11 -0700 Subject: [PATCH 03/14] remove unused import --- .../microsoft-agents-hosting-core/tests/test_turn_context.py | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/tests/test_turn_context.py b/libraries/microsoft-agents-hosting-core/tests/test_turn_context.py index 073a61d8..dc3c7920 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_turn_context.py +++ b/libraries/microsoft-agents-hosting-core/tests/test_turn_context.py @@ -7,7 +7,6 @@ ChannelAccount, ConversationAccount, Entity, - Mention, ResourceResponse, ) from microsoft.agents.hosting.core import ChannelAdapter, MessageFactory, TurnContext From f3c04ee2bf1ea31596f13cbd5681e4a23a048465 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 22 Jul 2025 14:29:14 -0700 Subject: [PATCH 04/14] remove unnecessary import path --- .../agents/authentication/msal/msal_connection_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-authentication-msal/microsoft/agents/authentication/msal/msal_connection_manager.py b/libraries/microsoft-agents-authentication-msal/microsoft/agents/authentication/msal/msal_connection_manager.py index 591240d6..6b2accb8 100644 --- a/libraries/microsoft-agents-authentication-msal/microsoft/agents/authentication/msal/msal_connection_manager.py +++ b/libraries/microsoft-agents-authentication-msal/microsoft/agents/authentication/msal/msal_connection_manager.py @@ -1,5 +1,5 @@ from typing import Dict, List, Optional -from microsoft.agents.hosting.core.authorization import ( +from microsoft.agents.hosting.core import ( AgentAuthConfiguration, AccessTokenProviderBase, ClaimsIdentity, From 023e948a89e96b6e6fffe0c5a88ae4b336cc3558 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 22 Jul 2025 14:29:39 -0700 Subject: [PATCH 05/14] add minimal MsalConnectionManager test suite --- .../tests/test_msal_connection_manager.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 libraries/microsoft-agents-authentication-msal/tests/test_msal_connection_manager.py diff --git a/libraries/microsoft-agents-authentication-msal/tests/test_msal_connection_manager.py b/libraries/microsoft-agents-authentication-msal/tests/test_msal_connection_manager.py new file mode 100644 index 00000000..7224eff0 --- /dev/null +++ b/libraries/microsoft-agents-authentication-msal/tests/test_msal_connection_manager.py @@ -0,0 +1,36 @@ +from os import environ +from microsoft.agents.activity import load_configuration_from_env +from microsoft.agents.hosting.core import AuthTypes +from microsoft.agents.authentication.msal import MsalConnectionManager + +class TestMsalConnectionManager: + """ + Test suite for the Msal Connection Manager + """ + def test_msal_connection_manager(self): + mock_environ = { + **environ, + "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__TENANTID": "test-tenant-id-SERVICE_CONNECTION", + "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTID": "test-client-id-SERVICE_CONNECTION", + "CONNECTIONS__SERVICE_CONNECTION__SETTINGS__CLIENTSECRET": "test-client-secret-SERVICE_CONNECTION", + "CONNECTIONS__MCS__SETTINGS__TENANTID": "test-tenant-id-MCS", + "CONNECTIONS__MCS__SETTINGS__CLIENTID": "test-client-id-MCS", + "CONNECTIONS__MCS__SETTINGS__CLIENTSECRET": "test-client-secret-MCS", + } + + config = load_configuration_from_env(mock_environ) + connection_manager = MsalConnectionManager(**config) + for key in connection_manager._connections: + auth = connection_manager.get_connection(key)._msal_configuration + assert auth.AUTH_TYPE == AuthTypes.client_secret + assert auth.CLIENT_ID == f"test-client-id-{key}" + assert auth.TENANT_ID == f"test-tenant-id-{key}" + assert auth.CLIENT_SECRET == f"test-client-secret-{key}" + assert auth.ISSUERS == [ + "https://api.botframework.com", + f"https://sts.windows.net/test-tenant-id-{key}/", + f"https://login.microsoftonline.com/test-tenant-id-{key}/v2.0", + ] + + + From 4d9c3ffb65f121bba0c18765ee34ad3125029196 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 22 Jul 2025 15:26:52 -0700 Subject: [PATCH 06/14] fix formatting/make concurrency friendly --- .../tests/test_msal_auth.py | 62 ++++++++++++------- .../tests/test_msal_connection_manager.py | 5 +- .../tests/test_auth_configuration.py | 3 +- 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py b/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py index b3b75406..a8eefece 100644 --- a/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py @@ -5,52 +5,72 @@ from microsoft.agents.authentication.msal import MsalAuth from microsoft.agents.hosting.core.authorization import AgentAuthConfiguration +class MockMsalAuth(MsalAuth): + """ + Mock object for MsalAuth + """ + def __init__(self, client_type): + super().__init__(AgentAuthConfiguration()) + mock_client = Mock(spec=client_type) + + mock_client.acquire_token_for_client = Mock(return_value={"access_token": "token"}) + mock_client.acquire_token_on_behalf_of= Mock(return_value={"access_token": "token"}) + self.mock_client = mock_client + + self._create_client_application = Mock(return_value=self.mock_client) + + class TestMsalAuth: """ Test suite for testing MsalAuth functionality """ - def setup_method(self): - """Set up test fixtures for each test method.""" - self.mock_client = Mock() - self.mock_client.acquire_token_for_client.return_value = {"access_token": "token"} - self.mock_client.acquire_token_on_behalf_of.return_value = {"access_token": "token"} - self.auth = MsalAuth(AgentAuthConfiguration()) - self.auth._create_client_application = Mock(return_value=self.mock_client) - @pytest.mark.asyncio async def test_get_access_token_managed_identity(self): - self.mock_client.mock_add_spec(ManagedIdentityClient) - token = await self.auth.get_access_token("https://test.api.botframework.com", scopes=["test-scope"]) + mock_auth = MockMsalAuth(ManagedIdentityClient) + token = await mock_auth.get_access_token( + "https://test.api.botframework.com", scopes=["test-scope"] + ) assert token == "token" - self.mock_client.acquire_token_for_client.assert_called_with(resource="https://test.api.botframework.com") + mock_auth.mock_client.acquire_token_for_client.assert_called_with( + resource="https://test.api.botframework.com" + ) @pytest.mark.asyncio async def test_get_access_token_confidential(self): - self.mock_client.mock_add_spec(ConfidentialClientApplication) - token = await self.auth.get_access_token("https://test.api.botframework.com", scopes=["test-scope"]) + mock_auth = MockMsalAuth(ConfidentialClientApplication) + token = await mock_auth.get_access_token( + "https://test.api.botframework.com", scopes=["test-scope"] + ) assert token == "token" - self.mock_client.acquire_token_for_client.assert_called_with(scopes=["test-scope"]) + mock_auth.mock_client.acquire_token_for_client.assert_called_with( + scopes=["test-scope"] + ) @pytest.mark.asyncio async def test_aquire_token_on_behalf_of_managed_identity(self): - self.mock_client.mock_add_spec(ManagedIdentityClient) + mock_auth = MockMsalAuth(ManagedIdentityClient) try: - await self.auth.aquire_token_on_behalf_of(scopes=["test-scope"], user_assertion="test-assertion") + await mock_auth.aquire_token_on_behalf_of( + scopes=["test-scope"], user_assertion="test-assertion" + ) except NotImplementedError: assert True else: assert False - @pytest.mark.asyncio async def test_aquire_token_on_behalf_of_confidential(self): - self.mock_client.mock_add_spec(ConfidentialClientApplication) - self.auth._create_client_application = Mock(return_value=self.mock_client) + mock_auth = MockMsalAuth(ConfidentialClientApplication) + mock_auth._create_client_application = Mock(return_value=mock_auth.mock_client) - token = await self.auth.aquire_token_on_behalf_of(scopes=["test-scope"], user_assertion="test-assertion") + token = await mock_auth.aquire_token_on_behalf_of( + scopes=["test-scope"], user_assertion="test-assertion" + ) assert token == "token" - self.mock_client.acquire_token_on_behalf_of.assert_called_with(scopes=["test-scope"], user_assertion="test-assertion") \ No newline at end of file + mock_auth.mock_client.acquire_token_on_behalf_of.assert_called_with( + scopes=["test-scope"], user_assertion="test-assertion" + ) diff --git a/libraries/microsoft-agents-authentication-msal/tests/test_msal_connection_manager.py b/libraries/microsoft-agents-authentication-msal/tests/test_msal_connection_manager.py index 7224eff0..234796df 100644 --- a/libraries/microsoft-agents-authentication-msal/tests/test_msal_connection_manager.py +++ b/libraries/microsoft-agents-authentication-msal/tests/test_msal_connection_manager.py @@ -3,10 +3,12 @@ from microsoft.agents.hosting.core import AuthTypes from microsoft.agents.authentication.msal import MsalConnectionManager + class TestMsalConnectionManager: """ Test suite for the Msal Connection Manager """ + def test_msal_connection_manager(self): mock_environ = { **environ, @@ -31,6 +33,3 @@ def test_msal_connection_manager(self): f"https://sts.windows.net/test-tenant-id-{key}/", f"https://login.microsoftonline.com/test-tenant-id-{key}/v2.0", ] - - - diff --git a/libraries/microsoft-agents-hosting-core/tests/test_auth_configuration.py b/libraries/microsoft-agents-hosting-core/tests/test_auth_configuration.py index 138fe670..1390b66d 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_auth_configuration.py +++ b/libraries/microsoft-agents-hosting-core/tests/test_auth_configuration.py @@ -3,6 +3,7 @@ from microsoft.agents.activity import load_configuration_from_env from microsoft.agents.hosting.core import AgentAuthConfiguration, AuthTypes + class TestAuthorizationConfiguration: """ Unit tests to validate Authorization Configuration cases @@ -75,4 +76,4 @@ def test_empty_settings(self): assert auth_config.CERT_KEY_FILE == None assert auth_config.CONNECTION_NAME == None assert auth_config.AUTHORITY == None - assert auth_config.SCOPES == None \ No newline at end of file + assert auth_config.SCOPES == None From 8c68debba858a2cb60f9e106e5b1b4e61215c5e1 Mon Sep 17 00:00:00 2001 From: = <=> Date: Tue, 22 Jul 2025 15:27:16 -0700 Subject: [PATCH 07/14] format using black --- .../tests/test_msal_auth.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py b/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py index a8eefece..a30d3ace 100644 --- a/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py @@ -5,16 +5,22 @@ from microsoft.agents.authentication.msal import MsalAuth from microsoft.agents.hosting.core.authorization import AgentAuthConfiguration + class MockMsalAuth(MsalAuth): """ Mock object for MsalAuth """ + def __init__(self, client_type): super().__init__(AgentAuthConfiguration()) mock_client = Mock(spec=client_type) - mock_client.acquire_token_for_client = Mock(return_value={"access_token": "token"}) - mock_client.acquire_token_on_behalf_of= Mock(return_value={"access_token": "token"}) + mock_client.acquire_token_for_client = Mock( + return_value={"access_token": "token"} + ) + mock_client.acquire_token_on_behalf_of = Mock( + return_value={"access_token": "token"} + ) self.mock_client = mock_client self._create_client_application = Mock(return_value=self.mock_client) @@ -24,6 +30,7 @@ class TestMsalAuth: """ Test suite for testing MsalAuth functionality """ + @pytest.mark.asyncio async def test_get_access_token_managed_identity(self): mock_auth = MockMsalAuth(ManagedIdentityClient) From 3c87dfa19ac32291b1365c5f4738c7132300df5c Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 24 Jul 2025 11:11:59 -0700 Subject: [PATCH 08/14] better naming conventions --- .../tests/test_msal_auth.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py b/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py index a30d3ace..041f49f9 100644 --- a/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py +++ b/libraries/microsoft-agents-authentication-msal/tests/test_msal_auth.py @@ -6,7 +6,7 @@ from microsoft.agents.hosting.core.authorization import AgentAuthConfiguration -class MockMsalAuth(MsalAuth): +class TestingMsalAuth(MsalAuth): """ Mock object for MsalAuth """ @@ -33,7 +33,7 @@ class TestMsalAuth: @pytest.mark.asyncio async def test_get_access_token_managed_identity(self): - mock_auth = MockMsalAuth(ManagedIdentityClient) + mock_auth = TestingMsalAuth(ManagedIdentityClient) token = await mock_auth.get_access_token( "https://test.api.botframework.com", scopes=["test-scope"] ) @@ -45,7 +45,7 @@ async def test_get_access_token_managed_identity(self): @pytest.mark.asyncio async def test_get_access_token_confidential(self): - mock_auth = MockMsalAuth(ConfidentialClientApplication) + mock_auth = TestingMsalAuth(ConfidentialClientApplication) token = await mock_auth.get_access_token( "https://test.api.botframework.com", scopes=["test-scope"] ) @@ -57,7 +57,7 @@ async def test_get_access_token_confidential(self): @pytest.mark.asyncio async def test_aquire_token_on_behalf_of_managed_identity(self): - mock_auth = MockMsalAuth(ManagedIdentityClient) + mock_auth = TestingMsalAuth(ManagedIdentityClient) try: await mock_auth.aquire_token_on_behalf_of( @@ -70,7 +70,7 @@ async def test_aquire_token_on_behalf_of_managed_identity(self): @pytest.mark.asyncio async def test_aquire_token_on_behalf_of_confidential(self): - mock_auth = MockMsalAuth(ConfidentialClientApplication) + mock_auth = TestingMsalAuth(ConfidentialClientApplication) mock_auth._create_client_application = Mock(return_value=mock_auth.mock_client) token = await mock_auth.aquire_token_on_behalf_of( From fde1a2f66346fc331168ea88e6df773032570b77 Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 24 Jul 2025 11:12:12 -0700 Subject: [PATCH 09/14] test fixtures for authorization flow --- .../tests/tools/testing_authorization.py | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py diff --git a/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py b/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py new file mode 100644 index 00000000..92d9b051 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py @@ -0,0 +1,106 @@ +from microsoft.agents.hosting.core import ( + Connections, + AccessTokenProviderBase, + AuthHandler, + Storage, + Authorization, + OAuthFlow, + MemoryStorage, +) +from typing import Dict +from microsoft.agents.hosting.core.authorization.agent_auth_configuration import ( + AgentAuthConfiguration, +) +from microsoft.agents.hosting.core.authorization.claims_identity import ClaimsIdentity + +from microsoft.agents.activity import TokenResponse + +from unittest.mock import Mock, MagicMock, AsyncMock + +import jwt + + +def create_test_auth_handler( + name: str, obo: bool = False, title: str = None, text: str = None +): + return AuthHandler( + name, + abs_oauth_connection_name=f"{name}-abs-connection", + obo_connection_name=f"{name}-obo-connection" if obo else None, + title=title, + text=text, + ) + + +class TestingTokenProvider(AccessTokenProviderBase): + """ + Test Token Provider for Unit Tests + """ + + def __init__(self, name: str): + self.name = name + + async def get_access_token( + self, resource_url: str, scopes: list[str], force_refresh: bool = False + ) -> str: + return f"{self.name}-token" + + async def aquire_token_on_behalf_of( + self, scopes: list[str], user_assertion: str + ) -> str: + return f"{self.name}-obo-token" + + +class TestingConnectionManager(Connections): + """ + Test Connection Manager for Unit Tests + """ + + def get_connection(self, connection_name: str) -> AccessTokenProviderBase: + return TestingTokenProvider(connection_name) + + def get_default_connection(self) -> AccessTokenProviderBase: + return TestingTokenProvider("default") + + def get_token_provider( + self, claims_identity: ClaimsIdentity, service_url: str + ) -> AccessTokenProviderBase: + return self.get_default_connection() + + def get_default_connection_configuration(self) -> AgentAuthConfiguration: + return AgentAuthConfiguration() + + +class TestingOAuthFlow(OAuthFlow): + """ + Test OAuthFlow for Unit Tests + """ + + def __init__(self, storage: Storage, abs_oauth_connection_name: str, **kwargs): + super().__init__( + storage=storage, abs_oauth_connection_name=abs_oauth_connection_name + ) + + +class MockOAuthFlow(Mock): + def __init__(self, connection_name: str, token): + token_response = TokenResponse( + connection_name=connection_name, + token=token if token else f"{connection_name}-token", + ) + super().__init__(get_user_token=AsyncMock(return_value=token_response)) + + +class TestingAuthorization(Authorization): + def __init__(self, auth_handlers: Dict[str, AuthHandler], token: str = None): + storage = MemoryStorage() + connection_manager = TestingConnectionManager() + super().__init__( + storage=storage, + auth_handlers=auth_handlers, + connection_manager=connection_manager, + ) + for auth_handler in self._auth_handlers.values(): + auth_handler.flow = MockOAuthFlow( + auth_handler.abs_oauth_connection_name, token=token + ) From 53032b0f36b00c8117272b03c57040a0935a8215 Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 24 Jul 2025 11:13:26 -0700 Subject: [PATCH 10/14] authorization tests - get_token, exchange_token --- .../tests/test_authorization.py | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 libraries/microsoft-agents-hosting-core/tests/test_authorization.py diff --git a/libraries/microsoft-agents-hosting-core/tests/test_authorization.py b/libraries/microsoft-agents-hosting-core/tests/test_authorization.py new file mode 100644 index 00000000..b87b8813 --- /dev/null +++ b/libraries/microsoft-agents-hosting-core/tests/test_authorization.py @@ -0,0 +1,84 @@ +import pytest +from microsoft.agents.hosting.core import Authorization, MemoryStorage +from .tools.testing_authorization import ( + TestingAuthorization, + create_test_auth_handler, + MockOAuthFlow, +) +from .tools.testing_utility import TestingUtility +from unittest.mock import Mock +import jwt + + +class TestAuthorization: + + @pytest.mark.asyncio + async def test_authorization_get_token_single_handler(self): + """ + Test Authorization - get_token() with single Auth Handler + """ + auth = TestingAuthorization( + auth_handlers={ + "auth-handler": create_test_auth_handler("test-auth-a"), + } + ) + + token_res = await auth.get_token(TestingUtility.create_empty_context()) + auth_handler = auth.resolver_handler("auth-handler") + assert token_res.connection_name == auth_handler.abs_oauth_connection_name + assert token_res.token == f"{auth_handler.abs_oauth_connection_name}-token" + + @pytest.mark.asyncio + async def test_authorization_get_token_multiple_handlers(self): + """ + Test Authorization - get_token() with multiple Auth Handlers + """ + auth_handlers = { + "auth-handler": create_test_auth_handler("test-auth-a"), + "auth-handler-obo": create_test_auth_handler("test-auth-b", obo=True), + "auth-handler-with-title": create_test_auth_handler( + "test-auth-c", title="test-title" + ), + "auth-handler-with-title-text": create_test_auth_handler( + "test-auth-d", title="test-title", text="test-text" + ), + } + auth = TestingAuthorization(auth_handlers=auth_handlers) + for id, auth_handler in auth_handlers.items(): + # test value propogation + token_res = await auth.get_token(TestingUtility.create_empty_context(), id) + assert token_res.connection_name == auth_handler.abs_oauth_connection_name + assert token_res.token == f"{auth_handler.abs_oauth_connection_name}-token" + + @pytest.mark.asyncio + async def test_authorization_exchange_token_valid_token(self): + valid_token = jwt.encode({"aud": "api://botframework.test.api"}, "") + scopes = ["scope-a"] + auth = TestingAuthorization( + auth_handlers={ + "auth-handler": create_test_auth_handler("test-auth", obo=True), + }, + token=valid_token, + ) + token_res = await auth.exchange_token( + TestingUtility.create_empty_context(), scopes=scopes + ) + assert ( + token_res.token + == f"{auth.resolver_handler().obo_connection_name}-obo-token" + ) + + @pytest.mark.asyncio + async def test_authorization_exchange_token_invalid_token(self): + invalid_token = jwt.encode({"aud": "invalid://botframework.test.api"}, "") + scopes = ["scope-a"] + auth = TestingAuthorization( + auth_handlers={ + "auth-handler": create_test_auth_handler("test-auth"), + }, + token=invalid_token, + ) + token_res = await auth.exchange_token( + TestingUtility.create_empty_context(), scopes=scopes + ) + assert token_res.token == invalid_token From 289807f3e09d441474edd48006d47bcf605b5cce Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 24 Jul 2025 11:27:25 -0700 Subject: [PATCH 11/14] remove unused imports --- .../microsoft-agents-hosting-core/tests/test_authorization.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/tests/test_authorization.py b/libraries/microsoft-agents-hosting-core/tests/test_authorization.py index b87b8813..9715afc2 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_authorization.py +++ b/libraries/microsoft-agents-hosting-core/tests/test_authorization.py @@ -3,10 +3,8 @@ from .tools.testing_authorization import ( TestingAuthorization, create_test_auth_handler, - MockOAuthFlow, ) from .tools.testing_utility import TestingUtility -from unittest.mock import Mock import jwt From 3da42006cda31eca6ac0bd00cfda91513ae13fa7 Mon Sep 17 00:00:00 2001 From: = <=> Date: Thu, 24 Jul 2025 16:06:48 -0700 Subject: [PATCH 12/14] tests for authorization auth flows --- .../tests/test_authorization.py | 119 ++++++++++++++++-- .../tests/tools/testing_authorization.py | 40 +++++- 2 files changed, 144 insertions(+), 15 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/tests/test_authorization.py b/libraries/microsoft-agents-hosting-core/tests/test_authorization.py index 9715afc2..c69f9245 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_authorization.py +++ b/libraries/microsoft-agents-hosting-core/tests/test_authorization.py @@ -1,14 +1,18 @@ import pytest -from microsoft.agents.hosting.core import Authorization, MemoryStorage from .tools.testing_authorization import ( TestingAuthorization, create_test_auth_handler, ) from .tools.testing_utility import TestingUtility import jwt +from unittest.mock import Mock, AsyncMock +from microsoft.agents.hosting.core import SignInState +from microsoft.agents.hosting.core.oauth_flow import FlowState class TestAuthorization: + def setup_method(self): + self.turn_context = TestingUtility.create_empty_context() @pytest.mark.asyncio async def test_authorization_get_token_single_handler(self): @@ -21,7 +25,7 @@ async def test_authorization_get_token_single_handler(self): } ) - token_res = await auth.get_token(TestingUtility.create_empty_context()) + token_res = await auth.get_token(self.turn_context) auth_handler = auth.resolver_handler("auth-handler") assert token_res.connection_name == auth_handler.abs_oauth_connection_name assert token_res.token == f"{auth_handler.abs_oauth_connection_name}-token" @@ -44,7 +48,7 @@ async def test_authorization_get_token_multiple_handlers(self): auth = TestingAuthorization(auth_handlers=auth_handlers) for id, auth_handler in auth_handlers.items(): # test value propogation - token_res = await auth.get_token(TestingUtility.create_empty_context(), id) + token_res = await auth.get_token(self.turn_context, id) assert token_res.connection_name == auth_handler.abs_oauth_connection_name assert token_res.token == f"{auth_handler.abs_oauth_connection_name}-token" @@ -58,9 +62,7 @@ async def test_authorization_exchange_token_valid_token(self): }, token=valid_token, ) - token_res = await auth.exchange_token( - TestingUtility.create_empty_context(), scopes=scopes - ) + token_res = await auth.exchange_token(self.turn_context, scopes=scopes) assert ( token_res.token == f"{auth.resolver_handler().obo_connection_name}-obo-token" @@ -76,7 +78,106 @@ async def test_authorization_exchange_token_invalid_token(self): }, token=invalid_token, ) - token_res = await auth.exchange_token( - TestingUtility.create_empty_context(), scopes=scopes - ) + token_res = await auth.exchange_token(self.turn_context, scopes=scopes) assert token_res.token == invalid_token + + @pytest.mark.asyncio + async def test_authorization_get_flow_state_unavailable(self): + auth = TestingAuthorization( + auth_handlers={ + "auth-handler": create_test_auth_handler("test-auth-a"), + } + ) + + assert auth.get_flow_state() == FlowState() + + @pytest.mark.asyncio + async def test_authorization_begin_or_continue_flow_not_started(self): + auth = TestingAuthorization( + auth_handlers={ + "auth-handler": create_test_auth_handler("test-auth-a"), + }, + token=None, + ) + mock_turn_state = AsyncMock(get_value=Mock(return_value=SignInState())) + + token_res = await auth.begin_or_continue_flow( + self.turn_context, + mock_turn_state, + "auth-handler", + ) + # Test value propogation + auth_handler = auth.resolver_handler("auth-handler") + assert token_res.connection_name == auth_handler.abs_oauth_connection_name + assert token_res.token == f"{auth_handler.abs_oauth_connection_name}-token" + + # Test function calls + auth_handler.flow._get_flow_state.assert_called_once() + auth_handler.flow.begin_flow.assert_called_once() + mock_turn_state.save.assert_called_once_with(self.turn_context) + mock_turn_state.set_value.assert_called_once_with( + auth.SIGN_IN_STATE_KEY, + SignInState( + continuation_activity=self.turn_context.activity, + handler_id="auth-handler", + ), + ) + + @pytest.mark.asyncio + async def test_authorization_begin_or_continue_flow_started(self): + auth = TestingAuthorization( + auth_handlers={ + "auth-handler": create_test_auth_handler("test-auth-a"), + }, + token=None, + flow_started=True, + ) + mock_turn_state = AsyncMock(get_value=Mock(return_value=SignInState())) + token_res = await auth.begin_or_continue_flow( + self.turn_context, + mock_turn_state, + "auth-handler", + ) + + # Test value propogation + auth_handler = auth.resolver_handler("auth-handler") + assert token_res.connection_name == auth_handler.abs_oauth_connection_name + assert token_res.token == f"{auth_handler.abs_oauth_connection_name}-token" + + # Test function calls + auth_handler.flow._get_flow_state.assert_called_once() + auth_handler.flow.continue_flow.assert_called_once() + mock_turn_state.save.assert_called_once_with(self.turn_context) + mock_turn_state.delete_value.assert_called_once_with(auth.SIGN_IN_STATE_KEY) + + @pytest.mark.asyncio + async def test_authorization_begin_or_continue_flow_started_with_handler(self): + auth = TestingAuthorization( + auth_handlers={ + "auth-handler": create_test_auth_handler("test-auth-a"), + }, + token=None, + flow_started=True, + ) + mock_turn_state = AsyncMock(get_value=Mock(return_value=SignInState())) + auth.on_sign_in_success(AsyncMock()) + + token_res = await auth.begin_or_continue_flow( + self.turn_context, + mock_turn_state, + "auth-handler", + ) + + # Test value propogation + auth_handler = auth.resolver_handler("auth-handler") + assert token_res.connection_name == auth_handler.abs_oauth_connection_name + assert token_res.token == f"{auth_handler.abs_oauth_connection_name}-token" + + # Test function calls + auth_handler.flow._get_flow_state.assert_called_once() + auth_handler.flow.continue_flow.assert_called_once() + mock_turn_state.save.assert_called_once_with(self.turn_context) + mock_turn_state.delete_value.assert_called_once_with(auth.SIGN_IN_STATE_KEY) + auth._sign_in_handler.assert_called_once_with( + self.turn_context, mock_turn_state, "auth-handler" + ) diff --git a/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py b/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py index 92d9b051..ebb34c8a 100644 --- a/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py +++ b/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py @@ -6,6 +6,7 @@ Authorization, OAuthFlow, MemoryStorage, + oauth_flow, ) from typing import Dict from microsoft.agents.hosting.core.authorization.agent_auth_configuration import ( @@ -83,16 +84,41 @@ def __init__(self, storage: Storage, abs_oauth_connection_name: str, **kwargs): class MockOAuthFlow(Mock): - def __init__(self, connection_name: str, token): - token_response = TokenResponse( + def __init__(self, connection_name: str, token: str | None, flow_started): + + default_token = TokenResponse( connection_name=connection_name, - token=token if token else f"{connection_name}-token", + token=f"{connection_name}-token", + ) + + if token == "default": + token_response = default_token + elif token: + token_response = TokenResponse( + connection_name=connection_name, + token=token, + ) + else: + token_response = None + + super().__init__( + get_user_token=AsyncMock(return_value=token_response), + _get_flow_state=AsyncMock( + return_value=oauth_flow.FlowState(flow_started=flow_started) + ), + begin_flow=AsyncMock(return_value=default_token), + continue_flow=AsyncMock(return_value=default_token), ) - super().__init__(get_user_token=AsyncMock(return_value=token_response)) + self.flow_state = None class TestingAuthorization(Authorization): - def __init__(self, auth_handlers: Dict[str, AuthHandler], token: str = None): + def __init__( + self, + auth_handlers: Dict[str, AuthHandler], + token: str | None = "default", + flow_started=False, + ): storage = MemoryStorage() connection_manager = TestingConnectionManager() super().__init__( @@ -102,5 +128,7 @@ def __init__(self, auth_handlers: Dict[str, AuthHandler], token: str = None): ) for auth_handler in self._auth_handlers.values(): auth_handler.flow = MockOAuthFlow( - auth_handler.abs_oauth_connection_name, token=token + auth_handler.abs_oauth_connection_name, + token=token, + flow_started=flow_started, ) From afb9b6f73e909156bf78f5ef3c0f3dec2ee892d3 Mon Sep 17 00:00:00 2001 From: = <=> Date: Fri, 25 Jul 2025 10:54:49 -0700 Subject: [PATCH 13/14] authorization tests - add documentation to supporting utilities, remove OAuthMock class --- .../tests/test_authorization.py | 46 +++- .../tests/tools/testing_authorization.py | 211 ++++++++++++++---- 2 files changed, 200 insertions(+), 57 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/tests/test_authorization.py b/libraries/microsoft-agents-hosting-core/tests/test_authorization.py index c69f9245..e7ee8b69 100644 --- a/libraries/microsoft-agents-hosting-core/tests/test_authorization.py +++ b/libraries/microsoft-agents-hosting-core/tests/test_authorization.py @@ -15,7 +15,7 @@ def setup_method(self): self.turn_context = TestingUtility.create_empty_context() @pytest.mark.asyncio - async def test_authorization_get_token_single_handler(self): + async def test_get_token_single_handler(self): """ Test Authorization - get_token() with single Auth Handler """ @@ -31,7 +31,7 @@ async def test_authorization_get_token_single_handler(self): assert token_res.token == f"{auth_handler.abs_oauth_connection_name}-token" @pytest.mark.asyncio - async def test_authorization_get_token_multiple_handlers(self): + async def test_get_token_multiple_handlers(self): """ Test Authorization - get_token() with multiple Auth Handlers """ @@ -53,7 +53,7 @@ async def test_authorization_get_token_multiple_handlers(self): assert token_res.token == f"{auth_handler.abs_oauth_connection_name}-token" @pytest.mark.asyncio - async def test_authorization_exchange_token_valid_token(self): + async def test_exchange_token_valid_token(self): valid_token = jwt.encode({"aud": "api://botframework.test.api"}, "") scopes = ["scope-a"] auth = TestingAuthorization( @@ -69,7 +69,7 @@ async def test_authorization_exchange_token_valid_token(self): ) @pytest.mark.asyncio - async def test_authorization_exchange_token_invalid_token(self): + async def test_exchange_token_invalid_token(self): invalid_token = jwt.encode({"aud": "invalid://botframework.test.api"}, "") scopes = ["scope-a"] auth = TestingAuthorization( @@ -82,7 +82,7 @@ async def test_authorization_exchange_token_invalid_token(self): assert token_res.token == invalid_token @pytest.mark.asyncio - async def test_authorization_get_flow_state_unavailable(self): + async def test_get_flow_state_unavailable(self): auth = TestingAuthorization( auth_handlers={ "auth-handler": create_test_auth_handler("test-auth-a"), @@ -92,7 +92,7 @@ async def test_authorization_get_flow_state_unavailable(self): assert auth.get_flow_state() == FlowState() @pytest.mark.asyncio - async def test_authorization_begin_or_continue_flow_not_started(self): + async def test_begin_or_continue_flow_not_started(self): auth = TestingAuthorization( auth_handlers={ "auth-handler": create_test_auth_handler("test-auth-a"), @@ -124,7 +124,7 @@ async def test_authorization_begin_or_continue_flow_not_started(self): ) @pytest.mark.asyncio - async def test_authorization_begin_or_continue_flow_started(self): + async def test_begin_or_continue_flow_started(self): auth = TestingAuthorization( auth_handlers={ "auth-handler": create_test_auth_handler("test-auth-a"), @@ -151,7 +151,7 @@ async def test_authorization_begin_or_continue_flow_started(self): mock_turn_state.delete_value.assert_called_once_with(auth.SIGN_IN_STATE_KEY) @pytest.mark.asyncio - async def test_authorization_begin_or_continue_flow_started_with_handler(self): + async def test_begin_or_continue_flow_started_sign_in_success(self): auth = TestingAuthorization( auth_handlers={ "auth-handler": create_test_auth_handler("test-auth-a"), @@ -181,3 +181,33 @@ async def test_authorization_begin_or_continue_flow_started_with_handler(self): auth._sign_in_handler.assert_called_once_with( self.turn_context, mock_turn_state, "auth-handler" ) + + @pytest.mark.asyncio + async def test_begin_or_continue_flow_started_sign_in_failure(self): + auth = TestingAuthorization( + auth_handlers={ + "auth-handler": create_test_auth_handler("test-auth-a"), + }, + token=None, + sign_in_failed=True, + ) + mock_turn_state = AsyncMock(get_value=Mock(return_value=SignInState())) + auth.on_sign_in_failure(AsyncMock()) + + token_res = await auth.begin_or_continue_flow( + self.turn_context, + mock_turn_state, + "auth-handler", + ) + + # Test value propogation + auth_handler = auth.resolver_handler("auth-handler") + assert not token_res + + # Test function calls + auth_handler.flow._get_flow_state.assert_called_once() + auth_handler.flow.continue_flow.assert_called_once() + mock_turn_state.save.assert_called_once_with(self.turn_context) + auth._sign_in_failed_handler.assert_called_once_with( + self.turn_context, mock_turn_state, "auth-handler" + ) diff --git a/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py b/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py index ebb34c8a..b2245e6a 100644 --- a/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py +++ b/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py @@ -1,10 +1,17 @@ +""" +Testing utilities for authorization functionality + +This module provides mock implementations and helper classes for testing authorization, +authentication, and token management scenarios. It includes test doubles for token +providers, connection managers, and authorization handlers that can be configured +to simulate various authentication states and flow conditions. +""" + from microsoft.agents.hosting.core import ( Connections, AccessTokenProviderBase, AuthHandler, - Storage, Authorization, - OAuthFlow, MemoryStorage, oauth_flow, ) @@ -16,14 +23,28 @@ from microsoft.agents.activity import TokenResponse -from unittest.mock import Mock, MagicMock, AsyncMock - -import jwt +from unittest.mock import Mock, AsyncMock def create_test_auth_handler( name: str, obo: bool = False, title: str = None, text: str = None ): + """ + Creates a test AuthHandler instance with standardized connection names. + + This helper function simplifies the creation of AuthHandler objects for testing + by automatically generating connection names based on the provided name and + optionally including On-Behalf-Of (OBO) connection configuration. + + Args: + name: Base name for the auth handler, used to generate connection names + obo: Whether to include On-Behalf-Of connection configuration + title: Optional title for the auth handler + text: Optional descriptive text for the auth handler + + Returns: + AuthHandler: Configured auth handler instance with test-friendly connection names + """ return AuthHandler( name, abs_oauth_connection_name=f"{name}-abs-connection", @@ -35,90 +56,151 @@ def create_test_auth_handler( class TestingTokenProvider(AccessTokenProviderBase): """ - Test Token Provider for Unit Tests + Access token provider for unit tests. + + This test double simulates an access token provider that returns predictable + token values based on the provider name. It implements both standard token + acquisition and On-Behalf-Of (OBO) token flows for comprehensive testing + of authentication scenarios. """ def __init__(self, name: str): + """ + Initialize the testing token provider. + + Args: + name: Identifier used to generate predictable token values + """ self.name = name async def get_access_token( self, resource_url: str, scopes: list[str], force_refresh: bool = False ) -> str: + """ + Get an access token for the specified resource and scopes. + + Returns a predictable token string based on the provider name for testing. + + Args: (unused in test implementation) + resource_url: URL of the resource requiring authentication + scopes: List of OAuth scopes requested + force_refresh: Whether to force token refresh + + Returns: + str: Test token in format "{name}-token" + """ return f"{self.name}-token" async def aquire_token_on_behalf_of( self, scopes: list[str], user_assertion: str ) -> str: + """ + Acquire a token on behalf of another user (OBO flow). + + Returns a predictable OBO token string for testing scenarios involving + delegated permissions and token exchange. + + Args: (unused in test implementation) + scopes: List of OAuth scopes requested for the OBO token + user_assertion: JWT token representing the user's identity + + Returns: + str: Test OBO token in format "{name}-obo-token" + """ return f"{self.name}-obo-token" class TestingConnectionManager(Connections): """ - Test Connection Manager for Unit Tests + Connection manager for unit tests. + + This test double provides a simplified connection management interface that + returns TestingTokenProvider instances for all connection requests. It enables + testing of authorization flows without requiring actual OAuth configurations + or external authentication services. """ def get_connection(self, connection_name: str) -> AccessTokenProviderBase: + """ + Get a token provider for the specified connection name. + + Args: + connection_name: Name of the OAuth connection + + Returns: + AccessTokenProviderBase: TestingTokenProvider configured with the connection name + """ return TestingTokenProvider(connection_name) def get_default_connection(self) -> AccessTokenProviderBase: + """ + Get the default token provider. + + Returns: + AccessTokenProviderBase: TestingTokenProvider configured with "default" name + """ return TestingTokenProvider("default") def get_token_provider( self, claims_identity: ClaimsIdentity, service_url: str ) -> AccessTokenProviderBase: - return self.get_default_connection() - - def get_default_connection_configuration(self) -> AgentAuthConfiguration: - return AgentAuthConfiguration() + """ + Get a token provider based on claims identity and service URL. + In this test implementation, returns the default connection regardless + of the provided parameters. -class TestingOAuthFlow(OAuthFlow): - """ - Test OAuthFlow for Unit Tests - """ - - def __init__(self, storage: Storage, abs_oauth_connection_name: str, **kwargs): - super().__init__( - storage=storage, abs_oauth_connection_name=abs_oauth_connection_name - ) + Args: (unused in test implementation) + claims_identity: User's claims and identity information + service_url: URL of the service requiring authentication + Returns: + AccessTokenProviderBase: The default TestingTokenProvider + """ + return self.get_default_connection() -class MockOAuthFlow(Mock): - def __init__(self, connection_name: str, token: str | None, flow_started): + def get_default_connection_configuration(self) -> AgentAuthConfiguration: + """ + Get the default authentication configuration. - default_token = TokenResponse( - connection_name=connection_name, - token=f"{connection_name}-token", - ) + Returns: + AgentAuthConfiguration: Empty configuration suitable for testing + """ + return AgentAuthConfiguration() - if token == "default": - token_response = default_token - elif token: - token_response = TokenResponse( - connection_name=connection_name, - token=token, - ) - else: - token_response = None - super().__init__( - get_user_token=AsyncMock(return_value=token_response), - _get_flow_state=AsyncMock( - return_value=oauth_flow.FlowState(flow_started=flow_started) - ), - begin_flow=AsyncMock(return_value=default_token), - continue_flow=AsyncMock(return_value=default_token), - ) - self.flow_state = None +class TestingAuthorization(Authorization): + """ + Authorization system for comprehensive unit testing. + This test double extends the Authorization class to provide a fully mocked + authorization environment suitable for testing various authentication scenarios. + It automatically configures auth handlers with mock OAuth flows that can simulate + different states like successful authentication, failed sign-in, or in-progress flows. + """ -class TestingAuthorization(Authorization): def __init__( self, auth_handlers: Dict[str, AuthHandler], token: str | None = "default", flow_started=False, + sign_in_failed=False, ): + """ + Initialize the testing authorization system. + + Sets up a complete test authorization environment with memory storage, + test connection manager, and configures all provided auth handlers with + mock OAuth flows. + + Args: + auth_handlers: Dictionary mapping handler names to AuthHandler instances + token: Token value to use in mock responses. "default" uses auto-generated + tokens, None simulates no token available, or provide custom jwt token string + flow_started: Simulate OAuth flows that have already started + sign_in_failed: Simulate failed sign-in attempts + """ + # Initialize with test-friendly components storage = MemoryStorage() connection_manager = TestingConnectionManager() super().__init__( @@ -126,9 +208,40 @@ def __init__( auth_handlers=auth_handlers, connection_manager=connection_manager, ) + + # Configure each auth handler with mock OAuth flow behavior for auth_handler in self._auth_handlers.values(): - auth_handler.flow = MockOAuthFlow( - auth_handler.abs_oauth_connection_name, - token=token, - flow_started=flow_started, + # Create default token response for this auth handler + default_token = TokenResponse( + connection_name=auth_handler.abs_oauth_connection_name, + token=f"{auth_handler.abs_oauth_connection_name}-token", ) + + # Determine token response based on configuration + if token == "default": + token_response = default_token + elif token: + token_response = TokenResponse( + connection_name=auth_handler.abs_oauth_connection_name, + token=token, + ) + else: + token_response = None + + # Mock the OAuth flow with configurable behavior + auth_handler.flow = Mock( + get_user_token=AsyncMock(return_value=token_response), + _get_flow_state=AsyncMock( + # sign-in failed requires flow to be started + return_value=oauth_flow.FlowState( + flow_started=(flow_started or sign_in_failed) + ) + ), + begin_flow=AsyncMock(return_value=default_token), + # Mock flow continuation with optional failure simulation + continue_flow=AsyncMock( + return_value=None if sign_in_failed else default_token + ), + ) + + auth_handler.flow.flow_state = None From 1e9bbafe5d2f0abd15b97e7b5147338a666c2727 Mon Sep 17 00:00:00 2001 From: = <=> Date: Fri, 25 Jul 2025 11:42:11 -0700 Subject: [PATCH 14/14] python 3.9 compatability --- .../tests/tools/testing_authorization.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py b/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py index b2245e6a..ac90b6c6 100644 --- a/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py +++ b/libraries/microsoft-agents-hosting-core/tests/tools/testing_authorization.py @@ -15,7 +15,7 @@ MemoryStorage, oauth_flow, ) -from typing import Dict +from typing import Dict, Union from microsoft.agents.hosting.core.authorization.agent_auth_configuration import ( AgentAuthConfiguration, ) @@ -182,7 +182,7 @@ class TestingAuthorization(Authorization): def __init__( self, auth_handlers: Dict[str, AuthHandler], - token: str | None = "default", + token: Union[str, None] = "default", flow_started=False, sign_in_failed=False, ):