From aef2f8260e5f5d76b24ec742dbb82d3df5761c08 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Fri, 7 Nov 2025 15:17:53 -0800 Subject: [PATCH 01/11] Beginning for channel adapter unit tests --- .../test_channel_service_adapter.py | 38 +++++++++++++++++++ ...est_rest_channel_service_client_factory.py | 15 ++++++++ 2 files changed, 53 insertions(+) create mode 100644 tests/hosting_core/test_channel_service_adapter.py create mode 100644 tests/hosting_core/test_rest_channel_service_client_factory.py diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py new file mode 100644 index 00000000..3c795e81 --- /dev/null +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -0,0 +1,38 @@ +import pytest + +from microsoft_agents.hosting.core import ( + ChannelServiceAdapter, + TurnContext, + ConnectorClientBase, + ChannelServiceClientFactoryBase +) + +class TestChannelServiceAdapter: + + @pytest.fixture + def connector_client(self, mocker): + return mocker.Mock(spec=ConnectorClientBase) + + + @pytest.fixture + def context(self, mocker, connector_client): + turn_context = mocker.Mock(spec=TurnContext) + turn_context.turn_state = { + ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY: connector_client + } + + @pytest.fixture + def factory(self, mocker, connector_client, user_token_client): + factory = mocker.Mock(spec=ChannelServiceClientFactoryBase) + factory.create_connector_client.return_value = connector_client + factory.create_user_token_client.return_value = user_token_client + return factory + + @pytest.fixture + def adapter(self, factory): + return ChannelServiceAdapter(factory) + + @pytest.mark.asyncio + async def test_send_activities(self, adapter, context): + + await adapter.send_activities(context, activities=[]) diff --git a/tests/hosting_core/test_rest_channel_service_client_factory.py b/tests/hosting_core/test_rest_channel_service_client_factory.py new file mode 100644 index 00000000..43dc4ddb --- /dev/null +++ b/tests/hosting_core/test_rest_channel_service_client_factory.py @@ -0,0 +1,15 @@ +import pytest + +from microsoft_agents.hosting.core import ( + RestChannelServiceClientFactory, +) + +class TestRestChannelServiceClientFactory: + + @pytest.mark.asyncio + def test_create_connector_client(self): + + factory = RestChannelServiceClientFactory() + client = factory.create_connector_client("http://example.com") + assert client is not None + assert client.base_url == "http://example.com" \ No newline at end of file From 4dd5224c2aafc7db8a144994b59f306f1367b4d6 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 10 Nov 2025 13:55:03 -0800 Subject: [PATCH 02/11] Adjusting create_connector_client to not necessitate context --- .../hosting/core/channel_service_adapter.py | 36 +++++++++---------- .../channel_service_client_factory_base.py | 2 +- .../rest_channel_service_client_factory.py | 8 ++--- 3 files changed, 23 insertions(+), 23 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 60cf0e79..1a5ab0b5 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -262,28 +262,12 @@ async def create_conversation( # pylint: disable=arguments-differ claims_identity = self.create_claims_identity(agent_app_id) claims_identity.claims[AuthenticationConstants.SERVICE_URL_CLAIM] = service_url - # Create a turn context and run the pipeline. - context = self._create_turn_context( - claims_identity, - None, - callback, - ) - - # Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) - user_token_client: UserTokenClient = ( - await self._channel_service_client_factory.create_user_token_client( - context, claims_identity - ) - ) - context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client - # Create the connector client to use for outbound requests. connector_client: ConnectorClient = ( await self._channel_service_client_factory.create_connector_client( - context, claims_identity, service_url, audience + None, claims_identity, service_url, audience ) ) - context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client # Make the actual create conversation call using the connector. create_conversation_result = ( @@ -297,7 +281,23 @@ async def create_conversation( # pylint: disable=arguments-differ create_conversation_result, channel_id, service_url, conversation_parameters ) - context.activity = create_activity + # Create a turn context and run the pipeline. + context = self._create_turn_context( + claims_identity, + None, + callback, + create_activity, + ) + context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client + + # Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) + user_token_client: UserTokenClient = ( + await self._channel_service_client_factory.create_user_token_client( + context, claims_identity + ) + ) + context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client + # Run the pipeline await self.run_pipeline(context, callback) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_client_factory_base.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_client_factory_base.py index faf46646..b7a6b68b 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_client_factory_base.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_client_factory_base.py @@ -16,7 +16,7 @@ class ChannelServiceClientFactoryBase(Protocol): @abstractmethod async def create_connector_client( self, - context: TurnContext, + context: TurnContext | None, claims_identity: ClaimsIdentity, service_url: str, audience: str, diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py index 9e639480..97573817 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py @@ -83,15 +83,15 @@ async def _get_agentic_token(self, context: TurnContext, service_url: str) -> st async def create_connector_client( self, - context: TurnContext, + context: TurnContext | None, claims_identity: ClaimsIdentity, service_url: str, audience: str, scopes: Optional[list[str]] = None, use_anonymous: bool = False, ) -> ConnectorClientBase: - if not context or not claims_identity: - raise TypeError("context and claims_identity are required") + if not claims_identity: + raise TypeError("claims_identity are required") if not service_url: raise TypeError( "RestChannelServiceClientFactory.create_connector_client: service_url can't be None or Empty" @@ -101,7 +101,7 @@ async def create_connector_client( "RestChannelServiceClientFactory.create_connector_client: audience can't be None or Empty" ) - if context.activity.is_agentic_request(): + if context and context.activity.is_agentic_request(): token = await self._get_agentic_token(context, service_url) else: token_provider: AccessTokenProviderBase = ( From bcfb70ecbc71d444e6fcac21b9dc69c1172bdcb3 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 10 Nov 2025 14:38:26 -0800 Subject: [PATCH 03/11] Sketch of RestChannelServiceClientFactory test --- ...est_rest_channel_service_client_factory.py | 75 +++++++++++++++++-- 1 file changed, 70 insertions(+), 5 deletions(-) diff --git a/tests/hosting_core/test_rest_channel_service_client_factory.py b/tests/hosting_core/test_rest_channel_service_client_factory.py index 43dc4ddb..af6c5372 100644 --- a/tests/hosting_core/test_rest_channel_service_client_factory.py +++ b/tests/hosting_core/test_rest_channel_service_client_factory.py @@ -1,15 +1,80 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + import pytest +from unittest.mock import AsyncMock, Mock, MagicMock +from microsoft_agents.activity import Activity, RoleTypes, ChannelAccount from microsoft_agents.hosting.core import ( RestChannelServiceClientFactory, + TurnContext, +) +from microsoft_agents.hosting.core.authorization import ( + AuthenticationConstants, + ClaimsIdentity, + Connections, + AccessTokenProviderBase, ) +from microsoft_agents.hosting.core.connector.teams import TeamsConnectorClient +from microsoft_agents.hosting.core.connector.client import UserTokenClient + +from tests._common.data import DEFAULT_TEST_VALUES + +DEFAULTS = DEFAULT_TEST_VALUES() + class TestRestChannelServiceClientFactory: + + @pytest.mark.parametrize( + "token_service_endpoint, token_service_audience", + [ + (AuthenticationConstants.AGENTS_SDK_OAUTH_URL, AuthenticationConstants.AGENTS_SDK_SCOPE), + ("https://custom.token.endpoint", "https://custom.token.audience"), + ] + ) @pytest.mark.asyncio - def test_create_connector_client(self): + async def test_create_connector_client_anonymous( + self, + mocker, + token_service_endpoint, + token_service_audience + ): + # setup + token_provider = mocker.mock(spec=AccessTokenProviderBase) + token_provider.get_access_token = AsyncMock(return_value=DEFAULTS.token) + + connection_manager = mocker.mock(spec=Connections) + connection_manager.get_token_provider = mocker.Mock( + return_value=token_provider + ) + + factory = RestChannelServiceClientFactory( + connection_manager, + token_service_endpoint, + token_service_audience + ) + + activity = Activity( + type="message", + ) - factory = RestChannelServiceClientFactory() - client = factory.create_connector_client("http://example.com") - assert client is not None - assert client.base_url == "http://example.com" \ No newline at end of file + claims_identity = ClaimsIdentity( + { + + }, + + ) + + context = TurnContext( + + ) + + res = await factory.create_connector_client( + context, + claims_identity, + service_url, + audience, + scopes, + use_anynoymous=True, + ) \ No newline at end of file From 3082380298ccca3f86ef5ec47fb92cd654073b24 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Mon, 10 Nov 2025 15:17:44 -0800 Subject: [PATCH 04/11] Adding create_connector_client tests for non agentic scenarios --- tests/_common/data/default_test_values.py | 1 + .../test_channel_service_adapter.py | 29 +-- ...est_rest_channel_service_client_factory.py | 182 ++++++++++++++++-- 3 files changed, 174 insertions(+), 38 deletions(-) diff --git a/tests/_common/data/default_test_values.py b/tests/_common/data/default_test_values.py index 9687c5a6..1bbafbb6 100644 --- a/tests/_common/data/default_test_values.py +++ b/tests/_common/data/default_test_values.py @@ -14,6 +14,7 @@ def __init__(self): self.user_id = "__user_id" self.bot_url = "https://botframework.com" self.ms_app_id = "__ms_app_id" + self.service_url = "https://service.url/" # Auth Handler Settings self.abs_oauth_connection_name = "connection_name" diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index 3c795e81..b6a60021 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -9,30 +9,7 @@ class TestChannelServiceAdapter: - @pytest.fixture - def connector_client(self, mocker): - return mocker.Mock(spec=ConnectorClientBase) - - - @pytest.fixture - def context(self, mocker, connector_client): - turn_context = mocker.Mock(spec=TurnContext) - turn_context.turn_state = { - ChannelServiceAdapter._AGENT_CONNECTOR_CLIENT_KEY: connector_client - } - - @pytest.fixture - def factory(self, mocker, connector_client, user_token_client): - factory = mocker.Mock(spec=ChannelServiceClientFactoryBase) - factory.create_connector_client.return_value = connector_client - factory.create_user_token_client.return_value = user_token_client - return factory - - @pytest.fixture - def adapter(self, factory): - return ChannelServiceAdapter(factory) - @pytest.mark.asyncio - async def test_send_activities(self, adapter, context): - - await adapter.send_activities(context, activities=[]) + async def test_create_conversation(self, mocker): + + \ No newline at end of file diff --git a/tests/hosting_core/test_rest_channel_service_client_factory.py b/tests/hosting_core/test_rest_channel_service_client_factory.py index af6c5372..810037ec 100644 --- a/tests/hosting_core/test_rest_channel_service_client_factory.py +++ b/tests/hosting_core/test_rest_channel_service_client_factory.py @@ -14,6 +14,7 @@ ClaimsIdentity, Connections, AccessTokenProviderBase, + AnonymousTokenProvider ) from microsoft_agents.hosting.core.connector.teams import TeamsConnectorClient from microsoft_agents.hosting.core.connector.client import UserTokenClient @@ -25,7 +26,55 @@ class TestRestChannelServiceClientFactory: + @pytest.fixture + def activity(self): + return Activity( + type="message", + channel_id="msteams", + from_property=ChannelAccount(id="user1", role=RoleTypes.user), + recipient=ChannelAccount(id="bot1", role=RoleTypes.agent), + service_url="https://service.url/", + conversation={"id": "conv1"}, + id="activity1", + text="Hello, World!", + ) + @pytest.fixture(params=[True, False]) + def context_flag(self, req): + return req.param + + # @pytest.fixture + # def activity_agentic_user(self): + # return Activity( + # type="message", + # channel_id="msteams", + # from_property=ChannelAccount(id="agentic_user", role=RoleTypes.USER), + # recipient=ChannelAccount(id="bot1", role=RoleTypes.BOT), + # service_url="https://service.url/", + # conversation={"id": "conv1"}, + # id="activity_agentic1", + # text="Hello, World!", + # properties={ + # "agenticRequest": True + # } + # ) + + # @pytest.fixture + # def activity_agentic_identity(self): + # return Activity( + # type="message", + # channel_id="msteams", + # from_property=ChannelAccount(id="agentic_user", role=RoleTypes.USER), + # recipient=ChannelAccount(id="bot1", role=RoleTypes.BOT), + # service_url="https://service.url/", + # conversation={"id": "conv1"}, + # id="activity_agentic1", + # text="Hello, World!", + # properties={ + # "agenticRequest": True + # } + # ) + @pytest.mark.parametrize( "token_service_endpoint, token_service_audience", [ @@ -37,8 +86,56 @@ class TestRestChannelServiceClientFactory: async def test_create_connector_client_anonymous( self, mocker, + activity, token_service_endpoint, - token_service_audience + token_service_audience, + context_flag + ): + mock_connector_client = mocker.mock(spec=TeamsConnectorClient) + mocker.patch.object(TeamsConnectorClient, "__new__", return_value=mock_connector_client) + + factory = RestChannelServiceClientFactory( + mocker.mock(spec=Connections), + token_service_endpoint, + token_service_audience + ) + + context = mocker.Mock(spec=TurnContext) + context.activity = activity + claims_identity = mocker.Mock(spec=ClaimsIdentity) + audience = [] + + res = await factory.create_connector_client( + context if context_flag else None, + claims_identity, + DEFAULTS.service_url, + audience, + scopes, + use_anynoymous=True, + ) + + #verify + TeamsConnectorClient.__new__.assert_called_once_with( + endpoint=DEFAULTS.service_url, + token="" + ) + assert res == mock_connector_client + + @pytest.mark.parametrize( + "token_service_endpoint, token_service_audience", + [ + (AuthenticationConstants.AGENTS_SDK_OAUTH_URL, AuthenticationConstants.AGENTS_SDK_SCOPE), + ("https://custom.token.endpoint", "https://custom.token.audience"), + ] + ) + @pytest.mark.asyncio + async def test_create_connector_client_normal_no_scopes( + self, + mocker, + activity, + token_service_endpoint, + token_service_audience, + context_flag ): # setup token_provider = mocker.mock(spec=AccessTokenProviderBase) @@ -54,27 +151,88 @@ async def test_create_connector_client_anonymous( token_service_endpoint, token_service_audience ) - - activity = Activity( - type="message", - ) - claims_identity = ClaimsIdentity( - { + claims_identity = mocker.mock(spec=ClaimsIdentity) + service_url = DEFAULTS.service_url + audience = "https://service.audience/" + + context = mocker.mock(spec=TurnContext) + context.activity = activity + + # test - }, + res = await factory.create_connector_client( + context if context_flag else None, + claims_identity, + service_url, + audience, + None + ) + # verify + assert connection_manager.get_token_provider.call_count == 1 + connection_manager.get_token_provider.assert_called_once_with( + claims_identity, service_url + ) + assert token_provider.get_access_token.call_count == 1 + token_provider.get_access_token.assert_called_once_with( + audience, [f"{audience}/.default"] ) - context = TurnContext( + @pytest.mark.parametrize( + "token_service_endpoint, token_service_audience", + [ + (AuthenticationConstants.AGENTS_SDK_OAUTH_URL, AuthenticationConstants.AGENTS_SDK_SCOPE), + ("https://custom.token.endpoint", "https://custom.token.audience"), + ] + ) + @pytest.mark.asyncio + async def test_create_connector_client_normal( + self, + mocker, + activity, + token_service_endpoint, + token_service_audience + ): + # setup + token_provider = mocker.mock(spec=AccessTokenProviderBase) + token_provider.get_access_token = AsyncMock(return_value=DEFAULTS.token) + connection_manager = mocker.mock(spec=Connections) + connection_manager.get_token_provider = mocker.Mock( + return_value=token_provider ) + factory = RestChannelServiceClientFactory( + connection_manager, + token_service_endpoint, + token_service_audience + ) + + claims_identity = mocker.mock(spec=ClaimsIdentity) + service_url = DEFAULTS.service_url + audience = "https://service.audience/" + scopes = ["scope1", "scope2"] + + context = mocker.mock(spec=TurnContext) + context.activity = activity + + # test + res = await factory.create_connector_client( - context, + context if context_flag else None, claims_identity, service_url, audience, - scopes, - use_anynoymous=True, + scopes + ) + + # verify + assert connection_manager.get_token_provider.call_count == 1 + connection_manager.get_token_provider.assert_called_once_with( + claims_identity, service_url + ) + assert token_provider.get_access_token.call_count == 1 + token_provider.get_access_token.assert_called_once_with( + audience, scopes ) \ No newline at end of file From 96ebfeacb759389af6d8e2e4c726332f9cc5732d Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 11 Nov 2025 12:47:46 -0800 Subject: [PATCH 05/11] Another commit --- .../test_channel_service_adapter.py | 3 ++ ...est_rest_channel_service_client_factory.py | 54 ++++++++++++------- 2 files changed, 39 insertions(+), 18 deletions(-) diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index b6a60021..51f8e84f 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -7,6 +7,9 @@ ChannelServiceClientFactoryBase ) +class MyChannelServiceAdapter(ChannelServiceAdapter): + pass + class TestChannelServiceAdapter: @pytest.mark.asyncio diff --git a/tests/hosting_core/test_rest_channel_service_client_factory.py b/tests/hosting_core/test_rest_channel_service_client_factory.py index 810037ec..b2af6e87 100644 --- a/tests/hosting_core/test_rest_channel_service_client_factory.py +++ b/tests/hosting_core/test_rest_channel_service_client_factory.py @@ -2,7 +2,6 @@ # Licensed under the MIT License. import pytest -from unittest.mock import AsyncMock, Mock, MagicMock from microsoft_agents.activity import Activity, RoleTypes, ChannelAccount from microsoft_agents.hosting.core import ( @@ -40,8 +39,8 @@ def activity(self): ) @pytest.fixture(params=[True, False]) - def context_flag(self, req): - return req.param + def context_flag(self, request): + return request.param # @pytest.fixture # def activity_agentic_user(self): @@ -91,11 +90,11 @@ async def test_create_connector_client_anonymous( token_service_audience, context_flag ): - mock_connector_client = mocker.mock(spec=TeamsConnectorClient) + mock_connector_client = mocker.Mock(spec=TeamsConnectorClient) mocker.patch.object(TeamsConnectorClient, "__new__", return_value=mock_connector_client) factory = RestChannelServiceClientFactory( - mocker.mock(spec=Connections), + mocker.Mock(spec=Connections), token_service_endpoint, token_service_audience ) @@ -103,7 +102,8 @@ async def test_create_connector_client_anonymous( context = mocker.Mock(spec=TurnContext) context.activity = activity claims_identity = mocker.Mock(spec=ClaimsIdentity) - audience = [] + scopes = ["scope1"] + audience = "https://service.audience/" res = await factory.create_connector_client( context if context_flag else None, @@ -111,11 +111,12 @@ async def test_create_connector_client_anonymous( DEFAULTS.service_url, audience, scopes, - use_anynoymous=True, + use_anonymous=True, ) #verify TeamsConnectorClient.__new__.assert_called_once_with( + TeamsConnectorClient, endpoint=DEFAULTS.service_url, token="" ) @@ -138,10 +139,13 @@ async def test_create_connector_client_normal_no_scopes( context_flag ): # setup - token_provider = mocker.mock(spec=AccessTokenProviderBase) - token_provider.get_access_token = AsyncMock(return_value=DEFAULTS.token) + mock_connector_client = mocker.Mock(spec=TeamsConnectorClient) + mocker.patch.object(TeamsConnectorClient, "__new__", return_value=mock_connector_client) + + token_provider = mocker.Mock(spec=AccessTokenProviderBase) + token_provider.get_access_token = mocker.AsyncMock(return_value=DEFAULTS.token) - connection_manager = mocker.mock(spec=Connections) + connection_manager = mocker.Mock(spec=Connections) connection_manager.get_token_provider = mocker.Mock( return_value=token_provider ) @@ -152,11 +156,11 @@ async def test_create_connector_client_normal_no_scopes( token_service_audience ) - claims_identity = mocker.mock(spec=ClaimsIdentity) + claims_identity = mocker.Mock(spec=ClaimsIdentity) service_url = DEFAULTS.service_url audience = "https://service.audience/" - context = mocker.mock(spec=TurnContext) + context = mocker.Mock(spec=TurnContext) context.activity = activity # test @@ -178,6 +182,11 @@ async def test_create_connector_client_normal_no_scopes( token_provider.get_access_token.assert_called_once_with( audience, [f"{audience}/.default"] ) + TeamsConnectorClient.__new__.assert_called_once_with( + TeamsConnectorClient, + endpoint=DEFAULTS.service_url, + token=DEFAULTS.token + ) @pytest.mark.parametrize( "token_service_endpoint, token_service_audience", @@ -192,13 +201,17 @@ async def test_create_connector_client_normal( mocker, activity, token_service_endpoint, - token_service_audience + token_service_audience, + context_flag ): # setup - token_provider = mocker.mock(spec=AccessTokenProviderBase) - token_provider.get_access_token = AsyncMock(return_value=DEFAULTS.token) + mock_connector_client = mocker.Mock(spec=TeamsConnectorClient) + mocker.patch.object(TeamsConnectorClient, "__new__", return_value=mock_connector_client) - connection_manager = mocker.mock(spec=Connections) + token_provider = mocker.Mock(spec=AccessTokenProviderBase) + token_provider.get_access_token = mocker.AsyncMock(return_value=DEFAULTS.token) + + connection_manager = mocker.Mock(spec=Connections) connection_manager.get_token_provider = mocker.Mock( return_value=token_provider ) @@ -209,12 +222,12 @@ async def test_create_connector_client_normal( token_service_audience ) - claims_identity = mocker.mock(spec=ClaimsIdentity) + claims_identity = mocker.Mock(spec=ClaimsIdentity) service_url = DEFAULTS.service_url audience = "https://service.audience/" scopes = ["scope1", "scope2"] - context = mocker.mock(spec=TurnContext) + context = mocker.Mock(spec=TurnContext) context.activity = activity # test @@ -235,4 +248,9 @@ async def test_create_connector_client_normal( assert token_provider.get_access_token.call_count == 1 token_provider.get_access_token.assert_called_once_with( audience, scopes + ) + TeamsConnectorClient.__new__.assert_called_once_with( + TeamsConnectorClient, + endpoint=DEFAULTS.service_url, + token=DEFAULTS.token ) \ No newline at end of file From a8c9ebe600835f8dea7105f2795f973d27580386 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 11 Nov 2025 13:11:58 -0800 Subject: [PATCH 06/11] TestChannelServiceAdapter outline --- .../test_channel_service_adapter.py | 69 ++++++++++++++++++- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index 51f8e84f..c585697e 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -1,10 +1,18 @@ import pytest +from microsoft_agents.activity import ( + ConversationResourceResponse, + ConversationParameters +) from microsoft_agents.hosting.core import ( ChannelServiceAdapter, TurnContext, ConnectorClientBase, - ChannelServiceClientFactoryBase + UserTokenClientBase, + ChannelServiceClientFactoryBase, + ConversationsBase, + TeamsConnectorClient, + UserTokenClient, ) class MyChannelServiceAdapter(ChannelServiceAdapter): @@ -12,7 +20,62 @@ class MyChannelServiceAdapter(ChannelServiceAdapter): class TestChannelServiceAdapter: + @pytest.fixture + def connector_client(self, mocker): + connector_client = mocker.Mock(spec=TeamsConnectorClient) + mocker.patch.object(TeamsConnectorClient, "__new__", return_value=connector_client) + return connector_client + + @pytest.fixture + def user_token_client(self, mocker): + user_token_client = mocker.Mock(spec=UserTokenClient) + mocker.patch.object(UserTokenClient, "__new__", return_value=user_token_client) + return user_token_client + + @pytest.fixture + def client_factory(self, mocker, connector_client, user_token_client): + factory = RestChannelServiceClientFactory( + mocker.Mock(spec=Connections), + token_service_endpoint, + token_service_audience + ) + + @pytest.fixture + def adapter(self, client_factory): + return MyChannelServiceAdapter(client_factory) + + @pytest.mark.asyncio - async def test_create_conversation(self, mocker): + async def test_create_conversation_basic(self, mocker, connector_client, factory, adapter): + + +# context = mocker.Mock(spec=TurnContext) +# context.activity = activity +# claims_identity = mocker.Mock(spec=ClaimsIdentity) +# scopes = ["scope1"] +# audience = "https://service.audience/" + adapter.run_pipeline = mocker.AsyncMock() - \ No newline at end of file + connector_client.conversations = mocker.Mock(spec=ConversationsBase) + connector_client.conversations.create_conversation.return_value = ConversationResourceResponse( + activity_id="activity123", + service_url="https://service.url", + id="conversation123" + ) + + async def callback(context: TurnContext): + return None + + res = await adapter.create_conversation( + "agent_app_id", + "channel_id", + "service_url", + "audience", + ConversationParameters(), + callback + ) + + assert factory.create_connector_client.call_count == 1 + assert factory.create_user_token_client.call_count == 1 + + assert adapter.run_pipeline.called \ No newline at end of file From 0d7a7467d4e65ec5caeb0e123e737400224d9548 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 11 Nov 2025 13:22:00 -0800 Subject: [PATCH 07/11] First pass at create_conversation unit test --- .../test_channel_service_adapter.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index c585697e..141fadfb 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -10,11 +10,14 @@ ConnectorClientBase, UserTokenClientBase, ChannelServiceClientFactoryBase, - ConversationsBase, + RestChannelServiceClientFactory, TeamsConnectorClient, UserTokenClient, + Connections, ) +from microsoft_agents.hosting.core.connector.conversations_base import ConversationsBase + class MyChannelServiceAdapter(ChannelServiceAdapter): pass @@ -31,22 +34,29 @@ def user_token_client(self, mocker): user_token_client = mocker.Mock(spec=UserTokenClient) mocker.patch.object(UserTokenClient, "__new__", return_value=user_token_client) return user_token_client + + @pytest.fixture + def connection_manager(self, mocker, user_token_client): + connection_manager = mocker.Mock(spec=Connections) + connection_manager.get_token_provider = mocker.Mock( + return_value=user_token_client + ) + return connection_manager @pytest.fixture - def client_factory(self, mocker, connector_client, user_token_client): - factory = RestChannelServiceClientFactory( - mocker.Mock(spec=Connections), - token_service_endpoint, - token_service_audience + def factory(self, connection_manager): + client_factory = RestChannelServiceClientFactory( + connection_manager ) + return client_factory @pytest.fixture - def adapter(self, client_factory): - return MyChannelServiceAdapter(client_factory) + def adapter(self, factory): + return MyChannelServiceAdapter(factory) @pytest.mark.asyncio - async def test_create_conversation_basic(self, mocker, connector_client, factory, adapter): + async def test_create_conversation_basic(self, mocker, user_token_client, connector_client, factory, adapter): # context = mocker.Mock(spec=TurnContext) @@ -54,6 +64,7 @@ async def test_create_conversation_basic(self, mocker, connector_client, factory # claims_identity = mocker.Mock(spec=ClaimsIdentity) # scopes = ["scope1"] # audience = "https://service.audience/" + user_token_client.get_access_token = mocker.AsyncMock(return_value="user_token_value") adapter.run_pipeline = mocker.AsyncMock() connector_client.conversations = mocker.Mock(spec=ConversationsBase) @@ -75,7 +86,4 @@ async def callback(context: TurnContext): callback ) - assert factory.create_connector_client.call_count == 1 - assert factory.create_user_token_client.call_count == 1 - assert adapter.run_pipeline.called \ No newline at end of file From 0340b5a9cf7f4440697a96051208f312fdb8ef23 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 11 Nov 2025 13:27:46 -0800 Subject: [PATCH 08/11] adding more assertions to test --- .../test_channel_service_adapter.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index 141fadfb..ea113741 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -56,14 +56,8 @@ def adapter(self, factory): @pytest.mark.asyncio - async def test_create_conversation_basic(self, mocker, user_token_client, connector_client, factory, adapter): + async def test_create_conversation_basic(self, mocker, user_token_client, connector_client, adapter): - -# context = mocker.Mock(spec=TurnContext) -# context.activity = activity -# claims_identity = mocker.Mock(spec=ClaimsIdentity) -# scopes = ["scope1"] -# audience = "https://service.audience/" user_token_client.get_access_token = mocker.AsyncMock(return_value="user_token_value") adapter.run_pipeline = mocker.AsyncMock() @@ -77,7 +71,7 @@ async def test_create_conversation_basic(self, mocker, user_token_client, connec async def callback(context: TurnContext): return None - res = await adapter.create_conversation( + await adapter.create_conversation( "agent_app_id", "channel_id", "service_url", @@ -86,4 +80,10 @@ async def callback(context: TurnContext): callback ) - assert adapter.run_pipeline.called \ No newline at end of file + adapter.run_pipeline.assert_awaited_once() + + context_arg, callback_arg = adapter.run_pipeline.call_args[0] + assert callback_arg == callback + assert context_arg.activity.conversation.id == "conversation123" + assert context_arg.activity.channel_id == "channel_id" + assert context_arg.activity.service_url == "service_url" \ No newline at end of file From a536596e9384caeb8668c33836ba105012af8bcc Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 11 Nov 2025 13:28:19 -0800 Subject: [PATCH 09/11] Formatting --- .../hosting/core/channel_service_adapter.py | 3 +- .../test_channel_service_adapter.py | 41 ++++--- ...est_rest_channel_service_client_factory.py | 103 +++++++++--------- 3 files changed, 76 insertions(+), 71 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 1a5ab0b5..5eed20d0 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -290,7 +290,7 @@ async def create_conversation( # pylint: disable=arguments-differ ) context.turn_state[self._AGENT_CONNECTOR_CLIENT_KEY] = connector_client - # Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) + # Create a UserTokenClient instance for the application to use. (For example, in the OAuthPrompt.) user_token_client: UserTokenClient = ( await self._channel_service_client_factory.create_user_token_client( context, claims_identity @@ -298,7 +298,6 @@ async def create_conversation( # pylint: disable=arguments-differ ) context.turn_state[self.USER_TOKEN_CLIENT_KEY] = user_token_client - # Run the pipeline await self.run_pipeline(context, callback) diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index ea113741..777f7cef 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -2,7 +2,7 @@ from microsoft_agents.activity import ( ConversationResourceResponse, - ConversationParameters + ConversationParameters, ) from microsoft_agents.hosting.core import ( ChannelServiceAdapter, @@ -18,23 +18,27 @@ from microsoft_agents.hosting.core.connector.conversations_base import ConversationsBase + class MyChannelServiceAdapter(ChannelServiceAdapter): pass + class TestChannelServiceAdapter: @pytest.fixture def connector_client(self, mocker): connector_client = mocker.Mock(spec=TeamsConnectorClient) - mocker.patch.object(TeamsConnectorClient, "__new__", return_value=connector_client) + mocker.patch.object( + TeamsConnectorClient, "__new__", return_value=connector_client + ) return connector_client - + @pytest.fixture def user_token_client(self, mocker): user_token_client = mocker.Mock(spec=UserTokenClient) mocker.patch.object(UserTokenClient, "__new__", return_value=user_token_client) return user_token_client - + @pytest.fixture def connection_manager(self, mocker, user_token_client): connection_manager = mocker.Mock(spec=Connections) @@ -45,27 +49,30 @@ def connection_manager(self, mocker, user_token_client): @pytest.fixture def factory(self, connection_manager): - client_factory = RestChannelServiceClientFactory( - connection_manager - ) + client_factory = RestChannelServiceClientFactory(connection_manager) return client_factory @pytest.fixture def adapter(self, factory): return MyChannelServiceAdapter(factory) - @pytest.mark.asyncio - async def test_create_conversation_basic(self, mocker, user_token_client, connector_client, adapter): + async def test_create_conversation_basic( + self, mocker, user_token_client, connector_client, adapter + ): - user_token_client.get_access_token = mocker.AsyncMock(return_value="user_token_value") + user_token_client.get_access_token = mocker.AsyncMock( + return_value="user_token_value" + ) adapter.run_pipeline = mocker.AsyncMock() - + connector_client.conversations = mocker.Mock(spec=ConversationsBase) - connector_client.conversations.create_conversation.return_value = ConversationResourceResponse( - activity_id="activity123", - service_url="https://service.url", - id="conversation123" + connector_client.conversations.create_conversation.return_value = ( + ConversationResourceResponse( + activity_id="activity123", + service_url="https://service.url", + id="conversation123", + ) ) async def callback(context: TurnContext): @@ -77,7 +84,7 @@ async def callback(context: TurnContext): "service_url", "audience", ConversationParameters(), - callback + callback, ) adapter.run_pipeline.assert_awaited_once() @@ -86,4 +93,4 @@ async def callback(context: TurnContext): assert callback_arg == callback assert context_arg.activity.conversation.id == "conversation123" assert context_arg.activity.channel_id == "channel_id" - assert context_arg.activity.service_url == "service_url" \ No newline at end of file + assert context_arg.activity.service_url == "service_url" diff --git a/tests/hosting_core/test_rest_channel_service_client_factory.py b/tests/hosting_core/test_rest_channel_service_client_factory.py index b2af6e87..6f3b691a 100644 --- a/tests/hosting_core/test_rest_channel_service_client_factory.py +++ b/tests/hosting_core/test_rest_channel_service_client_factory.py @@ -13,7 +13,7 @@ ClaimsIdentity, Connections, AccessTokenProviderBase, - AnonymousTokenProvider + AnonymousTokenProvider, ) from microsoft_agents.hosting.core.connector.teams import TeamsConnectorClient from microsoft_agents.hosting.core.connector.client import UserTokenClient @@ -37,7 +37,7 @@ def activity(self): id="activity1", text="Hello, World!", ) - + @pytest.fixture(params=[True, False]) def context_flag(self, request): return request.param @@ -57,7 +57,7 @@ def context_flag(self, request): # "agenticRequest": True # } # ) - + # @pytest.fixture # def activity_agentic_identity(self): # return Activity( @@ -77,26 +77,31 @@ def context_flag(self, request): @pytest.mark.parametrize( "token_service_endpoint, token_service_audience", [ - (AuthenticationConstants.AGENTS_SDK_OAUTH_URL, AuthenticationConstants.AGENTS_SDK_SCOPE), + ( + AuthenticationConstants.AGENTS_SDK_OAUTH_URL, + AuthenticationConstants.AGENTS_SDK_SCOPE, + ), ("https://custom.token.endpoint", "https://custom.token.audience"), - ] + ], ) @pytest.mark.asyncio async def test_create_connector_client_anonymous( - self, + self, mocker, activity, token_service_endpoint, token_service_audience, - context_flag + context_flag, ): mock_connector_client = mocker.Mock(spec=TeamsConnectorClient) - mocker.patch.object(TeamsConnectorClient, "__new__", return_value=mock_connector_client) + mocker.patch.object( + TeamsConnectorClient, "__new__", return_value=mock_connector_client + ) factory = RestChannelServiceClientFactory( mocker.Mock(spec=Connections), token_service_endpoint, - token_service_audience + token_service_audience, ) context = mocker.Mock(spec=TurnContext) @@ -114,48 +119,47 @@ async def test_create_connector_client_anonymous( use_anonymous=True, ) - #verify + # verify TeamsConnectorClient.__new__.assert_called_once_with( - TeamsConnectorClient, - endpoint=DEFAULTS.service_url, - token="" + TeamsConnectorClient, endpoint=DEFAULTS.service_url, token="" ) assert res == mock_connector_client - + @pytest.mark.parametrize( "token_service_endpoint, token_service_audience", [ - (AuthenticationConstants.AGENTS_SDK_OAUTH_URL, AuthenticationConstants.AGENTS_SDK_SCOPE), + ( + AuthenticationConstants.AGENTS_SDK_OAUTH_URL, + AuthenticationConstants.AGENTS_SDK_SCOPE, + ), ("https://custom.token.endpoint", "https://custom.token.audience"), - ] + ], ) @pytest.mark.asyncio async def test_create_connector_client_normal_no_scopes( - self, + self, mocker, activity, token_service_endpoint, token_service_audience, - context_flag + context_flag, ): # setup mock_connector_client = mocker.Mock(spec=TeamsConnectorClient) - mocker.patch.object(TeamsConnectorClient, "__new__", return_value=mock_connector_client) - + mocker.patch.object( + TeamsConnectorClient, "__new__", return_value=mock_connector_client + ) + token_provider = mocker.Mock(spec=AccessTokenProviderBase) token_provider.get_access_token = mocker.AsyncMock(return_value=DEFAULTS.token) connection_manager = mocker.Mock(spec=Connections) - connection_manager.get_token_provider = mocker.Mock( - return_value=token_provider - ) + connection_manager.get_token_provider = mocker.Mock(return_value=token_provider) factory = RestChannelServiceClientFactory( - connection_manager, - token_service_endpoint, - token_service_audience + connection_manager, token_service_endpoint, token_service_audience ) - + claims_identity = mocker.Mock(spec=ClaimsIdentity) service_url = DEFAULTS.service_url audience = "https://service.audience/" @@ -170,7 +174,7 @@ async def test_create_connector_client_normal_no_scopes( claims_identity, service_url, audience, - None + None, ) # verify @@ -180,48 +184,47 @@ async def test_create_connector_client_normal_no_scopes( ) assert token_provider.get_access_token.call_count == 1 token_provider.get_access_token.assert_called_once_with( - audience, [f"{audience}/.default"] + audience, [f"{audience}/.default"] ) TeamsConnectorClient.__new__.assert_called_once_with( - TeamsConnectorClient, - endpoint=DEFAULTS.service_url, - token=DEFAULTS.token + TeamsConnectorClient, endpoint=DEFAULTS.service_url, token=DEFAULTS.token ) @pytest.mark.parametrize( "token_service_endpoint, token_service_audience", [ - (AuthenticationConstants.AGENTS_SDK_OAUTH_URL, AuthenticationConstants.AGENTS_SDK_SCOPE), + ( + AuthenticationConstants.AGENTS_SDK_OAUTH_URL, + AuthenticationConstants.AGENTS_SDK_SCOPE, + ), ("https://custom.token.endpoint", "https://custom.token.audience"), - ] + ], ) @pytest.mark.asyncio async def test_create_connector_client_normal( - self, + self, mocker, activity, token_service_endpoint, token_service_audience, - context_flag + context_flag, ): # setup mock_connector_client = mocker.Mock(spec=TeamsConnectorClient) - mocker.patch.object(TeamsConnectorClient, "__new__", return_value=mock_connector_client) + mocker.patch.object( + TeamsConnectorClient, "__new__", return_value=mock_connector_client + ) token_provider = mocker.Mock(spec=AccessTokenProviderBase) token_provider.get_access_token = mocker.AsyncMock(return_value=DEFAULTS.token) connection_manager = mocker.Mock(spec=Connections) - connection_manager.get_token_provider = mocker.Mock( - return_value=token_provider - ) + connection_manager.get_token_provider = mocker.Mock(return_value=token_provider) factory = RestChannelServiceClientFactory( - connection_manager, - token_service_endpoint, - token_service_audience + connection_manager, token_service_endpoint, token_service_audience ) - + claims_identity = mocker.Mock(spec=ClaimsIdentity) service_url = DEFAULTS.service_url audience = "https://service.audience/" @@ -237,7 +240,7 @@ async def test_create_connector_client_normal( claims_identity, service_url, audience, - scopes + scopes, ) # verify @@ -246,11 +249,7 @@ async def test_create_connector_client_normal( claims_identity, service_url ) assert token_provider.get_access_token.call_count == 1 - token_provider.get_access_token.assert_called_once_with( - audience, scopes - ) + token_provider.get_access_token.assert_called_once_with(audience, scopes) TeamsConnectorClient.__new__.assert_called_once_with( - TeamsConnectorClient, - endpoint=DEFAULTS.service_url, - token=DEFAULTS.token - ) \ No newline at end of file + TeamsConnectorClient, endpoint=DEFAULTS.service_url, token=DEFAULTS.token + ) From 6527ec704b53e009fbd5b45b7483ee1c1a932690 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 12 Nov 2025 08:06:56 -0800 Subject: [PATCH 10/11] Adding create_connector_client calls that use agentic identity --- ...est_rest_channel_service_client_factory.py | 194 +++++++++++++++--- 1 file changed, 163 insertions(+), 31 deletions(-) diff --git a/tests/hosting_core/test_rest_channel_service_client_factory.py b/tests/hosting_core/test_rest_channel_service_client_factory.py index 6f3b691a..09665916 100644 --- a/tests/hosting_core/test_rest_channel_service_client_factory.py +++ b/tests/hosting_core/test_rest_channel_service_client_factory.py @@ -14,6 +14,7 @@ Connections, AccessTokenProviderBase, AnonymousTokenProvider, + AgentAuthConfiguration, ) from microsoft_agents.hosting.core.connector.teams import TeamsConnectorClient from microsoft_agents.hosting.core.connector.client import UserTokenClient @@ -42,37 +43,42 @@ def activity(self): def context_flag(self, request): return request.param - # @pytest.fixture - # def activity_agentic_user(self): - # return Activity( - # type="message", - # channel_id="msteams", - # from_property=ChannelAccount(id="agentic_user", role=RoleTypes.USER), - # recipient=ChannelAccount(id="bot1", role=RoleTypes.BOT), - # service_url="https://service.url/", - # conversation={"id": "conv1"}, - # id="activity_agentic1", - # text="Hello, World!", - # properties={ - # "agenticRequest": True - # } - # ) - - # @pytest.fixture - # def activity_agentic_identity(self): - # return Activity( - # type="message", - # channel_id="msteams", - # from_property=ChannelAccount(id="agentic_user", role=RoleTypes.USER), - # recipient=ChannelAccount(id="bot1", role=RoleTypes.BOT), - # service_url="https://service.url/", - # conversation={"id": "conv1"}, - # id="activity_agentic1", - # text="Hello, World!", - # properties={ - # "agenticRequest": True - # } - # ) + @pytest.fixture + def activity_agentic_user(self): + return Activity( + type="message", + channel_id="msteams", + from_property=ChannelAccount(id="agentic_user", role=RoleTypes.user), + recipient=ChannelAccount( + id="bot1", + agentic_app_id="agentic_app_id", + agentic_user_id="agentic_user_id", + role=RoleTypes.agentic_user, + ), + service_url="https://service.url/", + conversation={"id": "conv1"}, + id="activity_agentic1", + text="Hello, World!", + properties={"agenticRequest": True}, + ) + + @pytest.fixture + def activity_agentic_identity(self): + return Activity( + type="message", + channel_id="msteams", + from_property=ChannelAccount(id="agentic_user", role=RoleTypes.user), + recipient=ChannelAccount( + id="bot1", + agentic_app_id="agentic_app_id", + role=RoleTypes.agentic_identity, + ), + service_url="https://service.url/", + conversation={"id": "conv1"}, + id="activity_agentic1", + text="Hello, World!", + properties={"agenticRequest": True}, + ) @pytest.mark.parametrize( "token_service_endpoint, token_service_audience", @@ -253,3 +259,129 @@ async def test_create_connector_client_normal( TeamsConnectorClient.__new__.assert_called_once_with( TeamsConnectorClient, endpoint=DEFAULTS.service_url, token=DEFAULTS.token ) + + @pytest.mark.parametrize("alt_blueprint", [True, False]) + @pytest.mark.asyncio + async def test_create_connector_client_agentic_identity( + self, mocker, activity_agentic_identity, alt_blueprint + ): + # setup + mock_connector_client = mocker.Mock(spec=TeamsConnectorClient) + mocker.patch.object( + TeamsConnectorClient, "__new__", return_value=mock_connector_client + ) + + token_provider = mocker.Mock(spec=AccessTokenProviderBase) + token_provider.get_agentic_instance_token = mocker.AsyncMock( + return_value=(DEFAULTS.token, None) + ) + + connection_manager = mocker.Mock(spec=Connections) + connection_manager.get_token_provider = mocker.Mock(return_value=token_provider) + + auth_config = AgentAuthConfiguration() + if alt_blueprint: + auth_config.ALT_BLUEPRINT_ID = "alt_blueprint_id" + connection_manager.get_connection = mocker.Mock(return_value=token_provider) + token_provider._msal_configuration = auth_config + + factory = RestChannelServiceClientFactory(connection_manager) + + claims_identity = mocker.Mock(spec=ClaimsIdentity) + service_url = DEFAULTS.service_url + audience = "https://service.audience/" + scopes = ["scope1", "scope2"] + + context = mocker.Mock(spec=TurnContext) + context.activity = activity_agentic_identity + + # test + + res = await factory.create_connector_client( + context, + claims_identity, + service_url, + audience, + scopes, + ) + + # verify + assert connection_manager.get_token_provider.call_count == 1 + connection_manager.get_token_provider.assert_called_once_with( + context.identity, service_url + ) + if alt_blueprint: + connection_manager.get_connection.assert_called_once_with( + "alt_blueprint_id" + ) + assert token_provider.get_agentic_instance_token.call_count == 1 + token_provider.get_agentic_instance_token.assert_called_once_with( + "agentic_app_id" + ) + TeamsConnectorClient.__new__.assert_called_once_with( + TeamsConnectorClient, endpoint=DEFAULTS.service_url, token=DEFAULTS.token + ) + + @pytest.mark.parametrize("alt_blueprint", [True, False]) + @pytest.mark.asyncio + async def test_create_connector_client_agentic_user( + self, mocker, activity_agentic_user, alt_blueprint + ): + # setup + mock_connector_client = mocker.Mock(spec=TeamsConnectorClient) + mocker.patch.object( + TeamsConnectorClient, "__new__", return_value=mock_connector_client + ) + + token_provider = mocker.Mock(spec=AccessTokenProviderBase) + token_provider.get_agentic_user_token = mocker.AsyncMock( + return_value=DEFAULTS.token + ) + + connection_manager = mocker.Mock(spec=Connections) + connection_manager.get_token_provider = mocker.Mock(return_value=token_provider) + + auth_config = AgentAuthConfiguration() + if alt_blueprint: + auth_config.ALT_BLUEPRINT_ID = "alt_blueprint_id" + connection_manager.get_connection = mocker.Mock(return_value=token_provider) + token_provider._msal_configuration = auth_config + + factory = RestChannelServiceClientFactory(connection_manager) + + claims_identity = mocker.Mock(spec=ClaimsIdentity) + service_url = DEFAULTS.service_url + audience = "https://service.audience/" + scopes = ["scope1", "scope2"] + + context = mocker.Mock(spec=TurnContext) + context.activity = activity_agentic_user + + # test + + res = await factory.create_connector_client( + context, + claims_identity, + service_url, + audience, + scopes, + ) + + # verify + assert connection_manager.get_token_provider.call_count == 1 + connection_manager.get_token_provider.assert_called_once_with( + context.identity, service_url + ) + if alt_blueprint: + connection_manager.get_connection.assert_called_once_with( + "alt_blueprint_id" + ) + assert token_provider.get_agentic_user_token.call_count == 1 + token_provider.get_agentic_user_token.assert_called_once_with( + "agentic_app_id", + "agentic_user_id", + [AuthenticationConstants.APX_PRODUCTION_SCOPE], + ) + TeamsConnectorClient.__new__.assert_called_once_with( + TeamsConnectorClient, endpoint=DEFAULTS.service_url, token=DEFAULTS.token + ) From 2587a6089f21aedc01d918399b7fe21403dcd161 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Thu, 13 Nov 2025 10:14:31 -0800 Subject: [PATCH 11/11] Small change to error message --- .../hosting/core/rest_channel_service_client_factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py index 97573817..b2bc6440 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/rest_channel_service_client_factory.py @@ -91,7 +91,7 @@ async def create_connector_client( use_anonymous: bool = False, ) -> ConnectorClientBase: if not claims_identity: - raise TypeError("claims_identity are required") + raise TypeError("claims_identity is required") if not service_url: raise TypeError( "RestChannelServiceClientFactory.create_connector_client: service_url can't be None or Empty"