From 36421bf22d6e2eb0eba7c45d25af332dab7f1e88 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Tue, 6 Oct 2020 20:02:39 -0700 Subject: [PATCH 1/2] Testing SkillHttpClient --- .../aiohttp/skills/skill_http_client.py | 2 +- .../requirements.txt | 3 +- .../tests/skills/test_skill_http_client.py | 205 ++++++++++++++++++ 3 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 libraries/botbuilder-integration-aiohttp/tests/skills/test_skill_http_client.py diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py index df875f734..68da498ab 100644 --- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py +++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/skills/skill_http_client.py @@ -50,7 +50,7 @@ async def post_activity_to_skill( originating_audience = ( GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE if self._channel_provider is not None - and self._channel_provider.IsGovernment() + and self._channel_provider.is_government() else AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE ) diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt index b2706949b..5983ea638 100644 --- a/libraries/botbuilder-integration-aiohttp/requirements.txt +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -1,4 +1,5 @@ msrest==0.6.10 botframework-connector==4.10.0 botbuilder-schema==4.10.0 -aiohttp==3.6.2 \ No newline at end of file +aiohttp==3.6.2 +ddt==1.2.1 \ No newline at end of file diff --git a/libraries/botbuilder-integration-aiohttp/tests/skills/test_skill_http_client.py b/libraries/botbuilder-integration-aiohttp/tests/skills/test_skill_http_client.py new file mode 100644 index 000000000..df889cc82 --- /dev/null +++ b/libraries/botbuilder-integration-aiohttp/tests/skills/test_skill_http_client.py @@ -0,0 +1,205 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + +from uuid import uuid4 +from typing import Awaitable, Callable, Dict, Union + + +from unittest.mock import Mock +import aiounittest + +from botbuilder.core import MessageFactory, InvokeResponse +from botbuilder.core.skills import ( + BotFrameworkSkill, + ConversationIdFactoryBase, + SkillConversationIdFactoryOptions, + SkillConversationReference, +) +from botbuilder.integration.aiohttp.skills import SkillHttpClient +from botbuilder.schema import Activity, ConversationAccount, ConversationReference +from botframework.connector.auth import ( + AuthenticationConstants, + ChannelProvider, + GovernmentConstants, +) + + +class SimpleConversationIdFactory(ConversationIdFactoryBase): + def __init__(self, conversation_id: str): + self._conversation_id = conversation_id + self._conversation_refs: Dict[str, SkillConversationReference] = {} + # Public property to capture and assert the options passed to CreateSkillConversationIdAsync. + self.creation_options: SkillConversationIdFactoryOptions = None + + async def create_skill_conversation_id( + self, + options_or_conversation_reference: Union[ + SkillConversationIdFactoryOptions, ConversationReference + ], + ) -> str: + self.creation_options = options_or_conversation_reference + + key = self._conversation_id + self._conversation_refs[key] = self._conversation_refs.get( + key, + SkillConversationReference( + conversation_reference=options_or_conversation_reference.activity.get_conversation_reference(), + oauth_scope=options_or_conversation_reference.from_bot_oauth_scope, + ), + ) + return key + + async def get_conversation_reference( + self, skill_conversation_id: str + ) -> SkillConversationReference: + return self._conversation_refs[skill_conversation_id] + + async def delete_conversation_reference(self, skill_conversation_id: str): + raise NotImplementedError() + + +class TestSkillHttpClientTests(aiounittest.AsyncTestCase): + async def test_post_activity_with_originating_audience(self): + conversation_id = str(uuid4()) + conversation_id_factory = SimpleConversationIdFactory(conversation_id) + test_activity = MessageFactory.text("some message") + test_activity.conversation = ConversationAccount() + skill = BotFrameworkSkill( + id="SomeSkill", + app_id="", + skill_endpoint="https://someskill.com/api/messages", + ) + + async def _mock_post_content( + to_url: str, + token: str, # pylint: disable=unused-argument + activity: Activity, + ) -> (int, object): + nonlocal self + self.assertEqual(skill.skill_endpoint, to_url) + # Assert that the activity being sent has what we expect. + self.assertEqual(conversation_id, activity.conversation.id) + self.assertEqual("https://parentbot.com/api/messages", activity.service_url) + + # Create mock response. + return 200, None + + sut = await self._create_http_client_with_mock_handler( + _mock_post_content, conversation_id_factory + ) + + result = await sut.post_activity_to_skill( + "", + skill, + "https://parentbot.com/api/messages", + test_activity, + "someOriginatingAudience", + ) + + # Assert factory options + self.assertEqual("", conversation_id_factory.creation_options.from_bot_id) + self.assertEqual( + "someOriginatingAudience", + conversation_id_factory.creation_options.from_bot_oauth_scope, + ) + self.assertEqual( + test_activity, conversation_id_factory.creation_options.activity + ) + self.assertEqual( + skill, conversation_id_factory.creation_options.bot_framework_skill + ) + + # Assert result + self.assertIsInstance(result, InvokeResponse) + self.assertEqual(200, result.status) + + async def test_post_activity_using_invoke_response(self): + for is_gov in [True, False]: + with self.subTest(is_government=is_gov): + # pylint: disable=undefined-variable + # pylint: disable=cell-var-from-loop + conversation_id = str(uuid4()) + conversation_id_factory = SimpleConversationIdFactory(conversation_id) + test_activity = MessageFactory.text("some message") + test_activity.conversation = ConversationAccount() + expected_oauth_scope = ( + AuthenticationConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + ) + mock_channel_provider: ChannelProvider = Mock(spec=ChannelProvider) + + def is_government_mock(): + nonlocal expected_oauth_scope + if is_government: + expected_oauth_scope = ( + GovernmentConstants.TO_CHANNEL_FROM_BOT_OAUTH_SCOPE + ) + + return is_government + + mock_channel_provider.is_government = Mock( + side_effect=is_government_mock + ) + + skill = BotFrameworkSkill( + id="SomeSkill", + app_id="", + skill_endpoint="https://someskill.com/api/messages", + ) + + async def _mock_post_content( + to_url: str, + token: str, # pylint: disable=unused-argument + activity: Activity, + ) -> (int, object): + nonlocal self + + self.assertEqual(skill.skill_endpoint, to_url) + # Assert that the activity being sent has what we expect. + self.assertEqual(conversation_id, activity.conversation.id) + self.assertEqual( + "https://parentbot.com/api/messages", activity.service_url + ) + + # Create mock response. + return 200, None + + sut = await self._create_http_client_with_mock_handler( + _mock_post_content, conversation_id_factory + ) + result = await sut.post_activity_to_skill( + "", skill, "https://parentbot.com/api/messages", test_activity + ) + + # Assert factory options + self.assertEqual( + "", conversation_id_factory.creation_options.from_bot_id + ) + self.assertEqual( + expected_oauth_scope, + conversation_id_factory.creation_options.from_bot_oauth_scope, + ) + self.assertEqual( + test_activity, conversation_id_factory.creation_options.activity + ) + self.assertEqual( + skill, conversation_id_factory.creation_options.bot_framework_skill + ) + + # Assert result + self.assertIsInstance(result, InvokeResponse) + self.assertEqual(200, result.status) + + # Helper to create an HttpClient with a mock message handler that executes function argument to validate the request + # and mock a response. + async def _create_http_client_with_mock_handler( + self, + value_function: Callable[[object], Awaitable[object]], + id_factory: ConversationIdFactoryBase, + channel_provider: ChannelProvider = None, + ) -> SkillHttpClient: + # pylint: disable=protected-access + client = SkillHttpClient(Mock(), id_factory, channel_provider) + client._post_content = value_function + await client._session.close() + + return client From f2bbd0344b242389262aff14d02915df901d2526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Axel=20Su=C3=A1rez?= Date: Tue, 6 Oct 2020 20:06:58 -0700 Subject: [PATCH 2/2] delete unused dependency --- libraries/botbuilder-integration-aiohttp/requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/libraries/botbuilder-integration-aiohttp/requirements.txt b/libraries/botbuilder-integration-aiohttp/requirements.txt index 5983ea638..d30921ea9 100644 --- a/libraries/botbuilder-integration-aiohttp/requirements.txt +++ b/libraries/botbuilder-integration-aiohttp/requirements.txt @@ -2,4 +2,3 @@ msrest==0.6.10 botframework-connector==4.10.0 botbuilder-schema==4.10.0 aiohttp==3.6.2 -ddt==1.2.1 \ No newline at end of file