# Licensed under the MIT License. import aiounittest import unittest from copy import copy, deepcopy from unittest.mock import Mock from botbuilder.core import BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext from botbuilder.schema import Activity, ActivityTypes, ConversationAccount, ConversationReference, ChannelAccount from botframework.connector.aio import ConnectorClient from botframework.connector.auth import ClaimsIdentity reference = ConversationReference( activity_id='1234', channel_id='test', service_url='https://example.org/channel', user=ChannelAccount(id='user', name='User Name'), bot=ChannelAccount(id='bot', name='Bot Name'), conversation=ConversationAccount(id='convo1')) test_activity = Activity(text='test', type=ActivityTypes.message) incoming_message = TurnContext.apply_conversation_reference( copy(test_activity), reference, True) outgoing_message = TurnContext.apply_conversation_reference( copy(test_activity), reference) incoming_invoke = TurnContext.apply_conversation_reference( Activity(type=ActivityTypes.invoke), reference, True) class AdapterUnderTest(BotFrameworkAdapter):
# Copyright (c) Microsoft Corporation. All rights reserved. # Licensed under the MIT License. import aiounittest from botbuilder.schema import Activity, ConversationReference from botbuilder.core import TurnContext from botbuilder.core.adapters import TestAdapter RECEIVED_MESSAGE = Activity(type="message", text="received") UPDATED_ACTIVITY = Activity(type="message", text="update") DELETED_ACTIVITY_REFERENCE = ConversationReference(activity_id="1234") class TestTestAdapter(aiounittest.AsyncTestCase): async def test_should_call_bog_logic_when_receive_activity_is_called(self): async def logic(context: TurnContext): assert context assert context.activity assert context.activity.type == "message" assert context.activity.text == "test" assert context.activity.id assert context.activity.from_property assert context.activity.recipient assert context.activity.conversation assert context.activity.channel_id assert context.activity.service_url adapter = TestAdapter(logic) await adapter.receive_activity("test") async def test_should_support_receive_activity_with_activity(self):
async def get_conversation_reference( self, skill_conversation_id: str) -> ConversationReference: conversation_reference = ConversationReference().deserialize( json.loads(self._conversation_refs[skill_conversation_id])) return conversation_reference
async def post_activity( self, from_bot_id: str, to_bot_id: str, to_url: str, service_url: str, conversation_id: str, activity: Activity, ) -> InvokeResponse: app_credentials = await self._get_app_credentials(from_bot_id, to_bot_id) if not app_credentials: raise KeyError("Unable to get appCredentials to connect to the skill") # Get token for the skill call token = ( app_credentials.get_access_token() if app_credentials.microsoft_app_id else None ) # Capture current activity settings before changing them. # TODO: DO we need to set the activity ID? (events that are created manually don't have it). original_conversation_id = activity.conversation.id original_service_url = activity.service_url original_caller_id = activity.caller_id original_relates_to = activity.relates_to try: # TODO: The relato has to be ported to the adapter in the new integration library when # resolving conflicts in merge activity.relates_to = ConversationReference( service_url=activity.service_url, activity_id=activity.id, channel_id=activity.channel_id, conversation=ConversationAccount( id=activity.conversation.id, name=activity.conversation.name, conversation_type=activity.conversation.conversation_type, aad_object_id=activity.conversation.aad_object_id, is_group=activity.conversation.is_group, role=activity.conversation.role, tenant_id=activity.conversation.tenant_id, properties=activity.conversation.properties, ), bot=None, ) activity.conversation.id = conversation_id activity.service_url = service_url activity.caller_id = f"urn:botframework:aadappid:{from_bot_id}" headers_dict = { "Content-type": "application/json; charset=utf-8", } if token: headers_dict.update( {"Authorization": f"Bearer {token}",} ) json_content = json.dumps(activity.serialize()) resp = await self._session.post( to_url, data=json_content.encode("utf-8"), headers=headers_dict, ) resp.raise_for_status() data = (await resp.read()).decode() content = json.loads(data) if data else None if content: return InvokeResponse(status=resp.status, body=content) finally: # Restore activity properties. activity.conversation.id = original_conversation_id activity.service_url = original_service_url activity.caller_id = original_caller_id activity.relates_to = original_relates_to
async def _process_activity( self, claims_identity: ClaimsIdentity, conversation_id: str, reply_to_activity_id: str, activity: Activity, ) -> ResourceResponse: conversation_reference_result = await self._conversation_id_factory.get_conversation_reference( conversation_id) oauth_scope = None conversation_reference = None if isinstance(conversation_reference_result, SkillConversationReference): oauth_scope = conversation_reference_result.oauth_scope conversation_reference = ( conversation_reference_result.conversation_reference) else: conversation_reference = conversation_reference_result oauth_scope = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE if self._channel_provider and self._channel_provider.is_government() else AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE) if not conversation_reference: raise KeyError("ConversationReference not found") activity_conversation_reference = ConversationReference( activity_id=activity.id, user=activity.from_property, bot=activity.recipient, conversation=activity.conversation, channel_id=activity.channel_id, service_url=activity.service_url, ) async def callback(context: TurnContext): context.turn_state[ SkillHandler. SKILL_CONVERSATION_REFERENCE_KEY] = activity_conversation_reference TurnContext.apply_conversation_reference(activity, conversation_reference) context.activity.id = reply_to_activity_id if activity.type == ActivityTypes.end_of_conversation: await self._conversation_id_factory.delete_conversation_reference( conversation_id) self._apply_eoc_to_turn_context_activity(context, activity) await self._bot.on_turn(context) elif activity.type == ActivityTypes.event: self._apply_event_to_turn_context_activity(context, activity) await self._bot.on_turn(context) else: await context.send_activity(activity) await self._adapter.continue_conversation( conversation_reference, callback, claims_identity=claims_identity, audience=oauth_scope, ) return ResourceResponse(id=str(uuid4()))
) from botbuilder.schema import ( Activity, ActivityTypes, ConversationAccount, ConversationReference, ConversationResourceResponse, ChannelAccount, ) from botframework.connector.aio import ConnectorClient from botframework.connector.auth import ClaimsIdentity REFERENCE = ConversationReference( activity_id="1234", channel_id="test", service_url="https://example.org/channel", user=ChannelAccount(id="user", name="User Name"), bot=ChannelAccount(id="bot", name="Bot Name"), conversation=ConversationAccount(id="convo1"), ) TEST_ACTIVITY = Activity(text="test", type=ActivityTypes.message) INCOMING_MESSAGE = TurnContext.apply_conversation_reference( copy(TEST_ACTIVITY), REFERENCE, True ) OUTGOING_MESSAGE = TurnContext.apply_conversation_reference( copy(TEST_ACTIVITY), REFERENCE ) INCOMING_INVOKE = TurnContext.apply_conversation_reference( Activity(type=ActivityTypes.invoke), REFERENCE, True )
async def test_continue_conversation_with_audience(self): mock_credential_provider = unittest.mock.create_autospec(CredentialProvider) settings = BotFrameworkAdapterSettings( app_id="bot_id", credential_provider=mock_credential_provider ) adapter = BotFrameworkAdapter(settings) skill_1_app_id = "00000000-0000-0000-0000-000000skill1" skill_2_app_id = "00000000-0000-0000-0000-000000skill2" skills_identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: skill_1_app_id, AuthenticationConstants.APP_ID_CLAIM: skill_2_app_id, AuthenticationConstants.VERSION_CLAIM: "1.0", }, is_authenticated=True, ) skill_2_service_url = "https://skill2.com/api/skills/" async def callback(context: TurnContext): TestBotFrameworkAdapter.get_creds_and_assert_values( context, skill_1_app_id, skill_2_app_id, 1, ) TestBotFrameworkAdapter.get_client_and_assert_values( context, skill_1_app_id, skill_2_app_id, skill_2_service_url, 1, ) # pylint: disable=protected-access client_cache = context.adapter._connector_client_cache client = client_cache.get( BotFrameworkAdapter.key_for_connector_client( skill_2_service_url, skill_1_app_id, skill_2_app_id, ) ) assert client turn_state_client = context.turn_state.get( BotFrameworkAdapter.BOT_CONNECTOR_CLIENT_KEY ) assert turn_state_client client_creds = turn_state_client.config.credentials assert skill_1_app_id == client_creds.microsoft_app_id assert skill_2_app_id == client_creds.oauth_scope assert client.config.base_url == turn_state_client.config.base_url scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY] assert skill_2_app_id == scope # Ensure the serviceUrl was added to the trusted hosts assert AppCredentials.is_trusted_service(skill_2_service_url) refs = ConversationReference(service_url=skill_2_service_url) # Ensure the serviceUrl is NOT in the trusted hosts assert not AppCredentials.is_trusted_service(skill_2_service_url) await adapter.continue_conversation( refs, callback, claims_identity=skills_identity, audience=skill_2_app_id )
from botframework.connector.aio import ConnectorClient from botframework.connector.auth import ( ClaimsIdentity, AuthenticationConstants, AppCredentials, CredentialProvider, SimpleChannelProvider, GovernmentConstants, SimpleCredentialProvider, ) REFERENCE = ConversationReference( activity_id="1234", channel_id="test", locale="en-uS", # Intentionally oddly-cased to check that it isn't defaulted somewhere, but tests stay in English service_url="https://example.org/channel", user=ChannelAccount(id="user", name="User Name"), bot=ChannelAccount(id="bot", name="Bot Name"), conversation=ConversationAccount(id="convo1"), ) TEST_ACTIVITY = Activity(text="test", type=ActivityTypes.message) INCOMING_MESSAGE = TurnContext.apply_conversation_reference( copy(TEST_ACTIVITY), REFERENCE, True ) OUTGOING_MESSAGE = TurnContext.apply_conversation_reference( copy(TEST_ACTIVITY), REFERENCE ) INCOMING_INVOKE = TurnContext.apply_conversation_reference( Activity(type=ActivityTypes.invoke), REFERENCE, True
async def post_activity( self, from_bot_id: str, to_bot_id: str, to_url: str, service_url: str, conversation_id: str, activity: Activity, ) -> InvokeResponse: if not from_bot_id: raise TypeError("from_bot_id") if not to_bot_id: raise TypeError("to_bot_id") if not to_url: raise TypeError("to_url") if not service_url: raise TypeError("service_url") if not conversation_id: raise TypeError("conversation_id") if not activity: raise TypeError("activity") if self._logger: self._logger.log(20, f"post to skill '{to_bot_id}' at '{to_url}'") credentials = await self._credentials_factory.create_credentials( from_bot_id, to_bot_id, self._login_endpoint, True ) # Get token for the skill call token = credentials.get_access_token() if credentials.microsoft_app_id else None # Clone the activity so we can modify it before sending without impacting the original object. activity_copy = deepcopy(activity) # Apply the appropriate addressing to the newly created Activity. activity_copy.relates_to = ConversationReference( service_url=activity_copy.service_url, activity_id=activity_copy.id, channel_id=activity_copy.channel_id, conversation=ConversationAccount( id=activity_copy.conversation.id, name=activity_copy.conversation.name, conversation_type=activity_copy.conversation.conversation_type, aad_object_id=activity_copy.conversation.aad_object_id, is_group=activity_copy.conversation.is_group, role=activity_copy.conversation.role, tenant_id=activity_copy.conversation.tenant_id, properties=activity_copy.conversation.properties, ), bot=None, ) activity_copy.conversation.id = conversation_id activity_copy.service_url = service_url if not activity_copy.recipient: activity_copy.recipient = ChannelAccount(role=RoleTypes.skill) else: activity_copy.recipient.role = RoleTypes.skill headers_dict = { "Content-type": "application/json; charset=utf-8", } if token: headers_dict.update( {"Authorization": f"Bearer {token}",} ) json_content = dumps(activity_copy.serialize()).encode("utf-8") request = HttpRequest( request_uri=to_url, content=json_content, headers=headers_dict ) response = await self._http_client.post(request=request) data = await response.read_content_str() if not await response.is_succesful() and self._logger: # Otherwise we can assume we don't have to deserialize - so just log the content so it's not lost. self._logger.log( 40, f"Bot Framework call failed to '{to_url}' returning '{int(response.status_code)}' and '{data}'", ) return InvokeResponse( status=response.status_code, body=loads(data) if data else None )
async def test_continue_conversation_without_audience(self): mock_credential_provider = unittest.mock.create_autospec( CredentialProvider) settings = BotFrameworkAdapterSettings( app_id="bot_id", credential_provider=mock_credential_provider) adapter = BotFrameworkAdapter(settings) skill_1_app_id = "00000000-0000-0000-0000-000000skill1" skill_2_app_id = "00000000-0000-0000-0000-000000skill2" skills_identity = ClaimsIdentity( claims={ AuthenticationConstants.AUDIENCE_CLAIM: skill_1_app_id, AuthenticationConstants.APP_ID_CLAIM: skill_2_app_id, AuthenticationConstants.VERSION_CLAIM: "1.0", }, is_authenticated=True, ) channel_service_url = "https://smba.trafficmanager.net/amer/" async def callback(context: TurnContext): TestBotFrameworkAdapter.get_creds_and_assert_values( context, skill_1_app_id, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, 1, ) TestBotFrameworkAdapter.get_client_and_assert_values( context, skill_1_app_id, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, channel_service_url, 1, ) # pylint: disable=protected-access client_cache = context.adapter._connector_client_cache client = client_cache.get( BotFrameworkAdapter.key_for_connector_client( channel_service_url, skill_1_app_id, AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE, )) assert client turn_state_client = context.turn_state.get( BotFrameworkAdapter.BOT_CONNECTOR_CLIENT_KEY) assert turn_state_client client_creds = turn_state_client.config.credentials assert skill_1_app_id == client_creds.microsoft_app_id assert (AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE == client_creds.oauth_scope) assert client.config.base_url == turn_state_client.config.base_url scope = context.turn_state[BotFrameworkAdapter.BOT_OAUTH_SCOPE_KEY] assert AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE == scope refs = ConversationReference(service_url=channel_service_url) await adapter.continue_conversation(refs, callback, claims_identity=skills_identity)
def _build_conversation_reference(self) -> ConversationReference: return ConversationReference( conversation=ConversationAccount(id=str(uuid())), service_url=self.SERVICE_URL, )