Repository: microsoft/botbuilder-python

Issue:
Skill consumers should not be able to send Activities to skills without a recipient (Python)
See [parent](https://github.com/microsoft/botframework-sdk/issues/5785).

Issue may be specific to dotnet, need to verify if this is the case.


Discussion:

Resolving patch:
diff --git a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py
--- a/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py
+++ b/libraries/botbuilder-integration-aiohttp/botbuilder/integration/aiohttp/bot_framework_http_client.py
@@ -14,6 +14,7 @@
     ExpectedReplies,
     ConversationReference,
     ConversationAccount,
+    ChannelAccount,
 )
 from botframework.connector.auth import (
     ChannelProvider,
@@ -74,6 +75,7 @@ async def post_activity(
         original_conversation_id = activity.conversation.id
         original_service_url = activity.service_url
         original_relates_to = activity.relates_to
+        original_recipient = activity.recipient
 
         try:
             activity.relates_to = ConversationReference(
@@ -94,30 +96,38 @@ async def post_activity(
             )
             activity.conversation.id = conversation_id
             activity.service_url = service_url
+            if not activity.recipient:
+                activity.recipient = ChannelAccount()
 
-            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
+            status, content = await self._post_content(to_url, token, activity)
 
-            return InvokeResponse(status=resp.status, body=content)
+            return InvokeResponse(status=status, body=content)
 
         finally:
             # Restore activity properties.
             activity.conversation.id = original_conversation_id
             activity.service_url = original_service_url
             activity.relates_to = original_relates_to
+            activity.recipient = original_recipient
+
+    async def _post_content(
+        self, to_url: str, token: str, activity: Activity
+    ) -> (int, object):
+        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()
+        return resp.status, json.loads(data) if data else None
 
     async def post_buffered_activity(
         self,


Test patch:
diff --git a/libraries/botbuilder-integration-aiohttp/tests/test_bot_framework_http_client.py b/libraries/botbuilder-integration-aiohttp/tests/test_bot_framework_http_client.py
--- a/libraries/botbuilder-integration-aiohttp/tests/test_bot_framework_http_client.py
+++ b/libraries/botbuilder-integration-aiohttp/tests/test_bot_framework_http_client.py
@@ -1,8 +1,71 @@
+from unittest.mock import Mock
+
 import aiounittest
+from botbuilder.schema import ConversationAccount, ChannelAccount
 from botbuilder.integration.aiohttp import BotFrameworkHttpClient
+from botframework.connector.auth import CredentialProvider, Activity
 
 
 class TestBotFrameworkHttpClient(aiounittest.AsyncTestCase):
     async def test_should_create_connector_client(self):
         with self.assertRaises(TypeError):
             BotFrameworkHttpClient(None)
+
+    async def test_adds_recipient_and_sets_it_back_to_null(self):
+        mock_credential_provider = Mock(spec=CredentialProvider)
+
+        # pylint: disable=unused-argument
+        async def _mock_post_content(
+            to_url: str, token: str, activity: Activity
+        ) -> (int, object):
+            nonlocal self
+            self.assertIsNotNone(activity.recipient)
+            return 200, None
+
+        client = BotFrameworkHttpClient(credential_provider=mock_credential_provider)
+        client._post_content = _mock_post_content  # pylint: disable=protected-access
+
+        activity = Activity(conversation=ConversationAccount())
+
+        await client.post_activity(
+            None,
+            None,
+            "https://skillbot.com/api/messages",
+            "https://parentbot.com/api/messages",
+            "NewConversationId",
+            activity,
+        )
+
+        assert activity.recipient is None
+
+    async def test_does_not_overwrite_non_null_recipient_values(self):
+        skill_recipient_id = "skillBot"
+        mock_credential_provider = Mock(spec=CredentialProvider)
+
+        # pylint: disable=unused-argument
+        async def _mock_post_content(
+            to_url: str, token: str, activity: Activity
+        ) -> (int, object):
+            nonlocal self
+            self.assertIsNotNone(activity.recipient)
+            self.assertEqual(skill_recipient_id, activity.recipient.id)
+            return 200, None
+
+        client = BotFrameworkHttpClient(credential_provider=mock_credential_provider)
+        client._post_content = _mock_post_content  # pylint: disable=protected-access
+
+        activity = Activity(
+            conversation=ConversationAccount(),
+            recipient=ChannelAccount(id=skill_recipient_id),
+        )
+
+        await client.post_activity(
+            None,
+            None,
+            "https://skillbot.com/api/messages",
+            "https://parentbot.com/api/messages",
+            "NewConversationId",
+            activity,
+        )
+
+        assert activity.recipient.id == skill_recipient_id