Repository: microsoft/botbuilder-python

Issue:
[PORT] Updating mention stripping scenario for Teams with special character support
> Port this change from botbuilder-dotnet/master branch:
https://github.com/microsoft/botbuilder-dotnet/pull/3107

Fixing bug: #3106



Discussion:

Resolving patch:
diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py
--- a/libraries/botbuilder-core/botbuilder/core/turn_context.py
+++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py
@@ -1,383 +1,383 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-import re
-from copy import copy, deepcopy
-from datetime import datetime
-from typing import List, Callable, Union, Dict
-from botbuilder.schema import (
-    Activity,
-    ActivityTypes,
-    ConversationReference,
-    InputHints,
-    Mention,
-    ResourceResponse,
-)
-
-
-class TurnContext:
-    def __init__(self, adapter_or_context, request: Activity = None):
-        """
-        Creates a new TurnContext instance.
-        :param adapter_or_context:
-        :param request:
-        """
-        if isinstance(adapter_or_context, TurnContext):
-            adapter_or_context.copy_to(self)
-        else:
-            self.adapter = adapter_or_context
-            self._activity = request
-            self.responses: List[Activity] = []
-            self._services: dict = {}
-            self._on_send_activities: Callable[
-                ["TurnContext", List[Activity], Callable], List[ResourceResponse]
-            ] = []
-            self._on_update_activity: Callable[
-                ["TurnContext", Activity, Callable], ResourceResponse
-            ] = []
-            self._on_delete_activity: Callable[
-                ["TurnContext", ConversationReference, Callable], None
-            ] = []
-            self._responded: bool = False
-
-        if self.adapter is None:
-            raise TypeError("TurnContext must be instantiated with an adapter.")
-        if self.activity is None:
-            raise TypeError(
-                "TurnContext must be instantiated with a request parameter of type Activity."
-            )
-
-        self._turn_state = {}
-
-    @property
-    def turn_state(self) -> Dict[str, object]:
-        return self._turn_state
-
-    def copy_to(self, context: "TurnContext") -> None:
-        """
-        Called when this TurnContext instance is passed into the constructor of a new TurnContext
-        instance. Can be overridden in derived classes.
-        :param context:
-        :return:
-        """
-        for attribute in [
-            "adapter",
-            "activity",
-            "_responded",
-            "_services",
-            "_on_send_activities",
-            "_on_update_activity",
-            "_on_delete_activity",
-        ]:
-            setattr(context, attribute, getattr(self, attribute))
-
-    @property
-    def activity(self):
-        """
-        The received activity.
-        :return:
-        """
-        return self._activity
-
-    @activity.setter
-    def activity(self, value):
-        """
-        Used to set TurnContext._activity when a context object is created. Only takes instances of Activities.
-        :param value:
-        :return:
-        """
-        if not isinstance(value, Activity):
-            raise TypeError(
-                "TurnContext: cannot set `activity` to a type other than Activity."
-            )
-        self._activity = value
-
-    @property
-    def responded(self) -> bool:
-        """
-        If `true` at least one response has been sent for the current turn of conversation.
-        :return:
-        """
-        return self._responded
-
-    @responded.setter
-    def responded(self, value: bool):
-        if not value:
-            raise ValueError("TurnContext: cannot set TurnContext.responded to False.")
-        self._responded = True
-
-    @property
-    def services(self):
-        """
-        Map of services and other values cached for the lifetime of the turn.
-        :return:
-        """
-        return self._services
-
-    def get(self, key: str) -> object:
-        if not key or not isinstance(key, str):
-            raise TypeError('"key" must be a valid string.')
-        try:
-            return self._services[key]
-        except KeyError:
-            raise KeyError("%s not found in TurnContext._services." % key)
-
-    def has(self, key: str) -> bool:
-        """
-        Returns True is set() has been called for a key. The cached value may be of type 'None'.
-        :param key:
-        :return:
-        """
-        if key in self._services:
-            return True
-        return False
-
-    def set(self, key: str, value: object) -> None:
-        """
-        Caches a value for the lifetime of the current turn.
-        :param key:
-        :param value:
-        :return:
-        """
-        if not key or not isinstance(key, str):
-            raise KeyError('"key" must be a valid string.')
-
-        self._services[key] = value
-
-    async def send_activity(
-        self,
-        activity_or_text: Union[Activity, str],
-        speak: str = None,
-        input_hint: str = None,
-    ) -> ResourceResponse:
-        """
-        Sends a single activity or message to the user.
-        :param activity_or_text:
-        :return:
-        """
-        if isinstance(activity_or_text, str):
-            activity_or_text = Activity(
-                text=activity_or_text,
-                input_hint=input_hint or InputHints.accepting_input,
-                speak=speak,
-            )
-
-        result = await self.send_activities([activity_or_text])
-        return result[0] if result else None
-
-    async def send_activities(
-        self, activities: List[Activity]
-    ) -> List[ResourceResponse]:
-        sent_non_trace_activity = False
-        ref = TurnContext.get_conversation_reference(self.activity)
-
-        def activity_validator(activity: Activity) -> Activity:
-            if not getattr(activity, "type", None):
-                activity.type = ActivityTypes.message
-            if activity.type != ActivityTypes.trace:
-                nonlocal sent_non_trace_activity
-                sent_non_trace_activity = True
-            if not activity.input_hint:
-                activity.input_hint = "acceptingInput"
-            activity.id = None
-            return activity
-
-        output = [
-            activity_validator(
-                TurnContext.apply_conversation_reference(deepcopy(act), ref)
-            )
-            for act in activities
-        ]
-
-        async def logic():
-            responses = await self.adapter.send_activities(self, output)
-            if sent_non_trace_activity:
-                self.responded = True
-            return responses
-
-        return await self._emit(self._on_send_activities, output, logic())
-
-    async def update_activity(self, activity: Activity):
-        """
-        Replaces an existing activity.
-        :param activity:
-        :return:
-        """
-        reference = TurnContext.get_conversation_reference(self.activity)
-
-        return await self._emit(
-            self._on_update_activity,
-            TurnContext.apply_conversation_reference(activity, reference),
-            self.adapter.update_activity(self, activity),
-        )
-
-    async def delete_activity(self, id_or_reference: Union[str, ConversationReference]):
-        """
-        Deletes an existing activity.
-        :param id_or_reference:
-        :return:
-        """
-        if isinstance(id_or_reference, str):
-            reference = TurnContext.get_conversation_reference(self.activity)
-            reference.activity_id = id_or_reference
-        else:
-            reference = id_or_reference
-        return await self._emit(
-            self._on_delete_activity,
-            reference,
-            self.adapter.delete_activity(self, reference),
-        )
-
-    def on_send_activities(self, handler) -> "TurnContext":
-        """
-        Registers a handler to be notified of and potentially intercept the sending of activities.
-        :param handler:
-        :return:
-        """
-        self._on_send_activities.append(handler)
-        return self
-
-    def on_update_activity(self, handler) -> "TurnContext":
-        """
-        Registers a handler to be notified of and potentially intercept an activity being updated.
-        :param handler:
-        :return:
-        """
-        self._on_update_activity.append(handler)
-        return self
-
-    def on_delete_activity(self, handler) -> "TurnContext":
-        """
-        Registers a handler to be notified of and potentially intercept an activity being deleted.
-        :param handler:
-        :return:
-        """
-        self._on_delete_activity.append(handler)
-        return self
-
-    async def _emit(self, plugins, arg, logic):
-        handlers = copy(plugins)
-
-        async def emit_next(i: int):
-            context = self
-            try:
-                if i < len(handlers):
-
-                    async def next_handler():
-                        await emit_next(i + 1)
-
-                    await handlers[i](context, arg, next_handler)
-
-            except Exception as error:
-                raise error
-
-        await emit_next(0)
-        # logic does not use parentheses because it's a coroutine
-        return await logic
-
-    async def send_trace_activity(
-        self, name: str, value: object, value_type: str, label: str
-    ) -> ResourceResponse:
-        trace_activity = Activity(
-            type=ActivityTypes.trace,
-            timestamp=datetime.utcnow(),
-            name=name,
-            value=value,
-            value_type=value_type,
-            label=label,
-        )
-
-        return await self.send_activity(trace_activity)
-
-    @staticmethod
-    def get_conversation_reference(activity: Activity) -> ConversationReference:
-        """
-        Returns the conversation reference for an activity. This can be saved as a plain old JSON
-        object and then later used to message the user proactively.
-
-        Usage Example:
-        reference = TurnContext.get_conversation_reference(context.request)
-        :param activity:
-        :return:
-        """
-        return ConversationReference(
-            activity_id=activity.id,
-            user=copy(activity.from_property),
-            bot=copy(activity.recipient),
-            conversation=copy(activity.conversation),
-            channel_id=activity.channel_id,
-            service_url=activity.service_url,
-        )
-
-    @staticmethod
-    def apply_conversation_reference(
-        activity: Activity, reference: ConversationReference, is_incoming: bool = False
-    ) -> Activity:
-        """
-        Updates an activity with the delivery information from a conversation reference. Calling
-        this after get_conversation_reference on an incoming activity
-        will properly address the reply to a received activity.
-        :param activity:
-        :param reference:
-        :param is_incoming:
-        :return:
-        """
-        activity.channel_id = reference.channel_id
-        activity.service_url = reference.service_url
-        activity.conversation = reference.conversation
-        if is_incoming:
-            activity.from_property = reference.user
-            activity.recipient = reference.bot
-            if reference.activity_id:
-                activity.id = reference.activity_id
-        else:
-            activity.from_property = reference.bot
-            activity.recipient = reference.user
-            if reference.activity_id:
-                activity.reply_to_id = reference.activity_id
-
-        return activity
-
-    @staticmethod
-    def get_reply_conversation_reference(
-        activity: Activity, reply: ResourceResponse
-    ) -> ConversationReference:
-        reference: ConversationReference = TurnContext.get_conversation_reference(
-            activity
-        )
-
-        # Update the reference with the new outgoing Activity's id.
-        reference.activity_id = reply.id
-
-        return reference
-
-    @staticmethod
-    def remove_recipient_mention(activity: Activity) -> str:
-        return TurnContext.remove_mention_text(activity, activity.recipient.id)
-
-    @staticmethod
-    def remove_mention_text(activity: Activity, identifier: str) -> str:
-        mentions = TurnContext.get_mentions(activity)
-        for mention in mentions:
-            if mention.additional_properties["mentioned"]["id"] == identifier:
-                mention_name_match = re.match(
-                    r"<at(.*)>(.*?)<\/at>",
-                    mention.additional_properties["text"],
-                    re.IGNORECASE,
-                )
-                if mention_name_match:
-                    activity.text = re.sub(
-                        mention_name_match.groups()[1], "", activity.text
-                    )
-                    activity.text = re.sub(r"<at><\/at>", "", activity.text)
-        return activity.text
-
-    @staticmethod
-    def get_mentions(activity: Activity) -> List[Mention]:
-        result: List[Mention] = []
-        if activity.entities is not None:
-            for entity in activity.entities:
-                if entity.type.lower() == "mention":
-                    result.append(entity)
-
-        return result
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import re
+from copy import copy, deepcopy
+from datetime import datetime
+from typing import List, Callable, Union, Dict
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ConversationReference,
+    InputHints,
+    Mention,
+    ResourceResponse,
+)
+
+
+class TurnContext:
+    def __init__(self, adapter_or_context, request: Activity = None):
+        """
+        Creates a new TurnContext instance.
+        :param adapter_or_context:
+        :param request:
+        """
+        if isinstance(adapter_or_context, TurnContext):
+            adapter_or_context.copy_to(self)
+        else:
+            self.adapter = adapter_or_context
+            self._activity = request
+            self.responses: List[Activity] = []
+            self._services: dict = {}
+            self._on_send_activities: Callable[
+                ["TurnContext", List[Activity], Callable], List[ResourceResponse]
+            ] = []
+            self._on_update_activity: Callable[
+                ["TurnContext", Activity, Callable], ResourceResponse
+            ] = []
+            self._on_delete_activity: Callable[
+                ["TurnContext", ConversationReference, Callable], None
+            ] = []
+            self._responded: bool = False
+
+        if self.adapter is None:
+            raise TypeError("TurnContext must be instantiated with an adapter.")
+        if self.activity is None:
+            raise TypeError(
+                "TurnContext must be instantiated with a request parameter of type Activity."
+            )
+
+        self._turn_state = {}
+
+    @property
+    def turn_state(self) -> Dict[str, object]:
+        return self._turn_state
+
+    def copy_to(self, context: "TurnContext") -> None:
+        """
+        Called when this TurnContext instance is passed into the constructor of a new TurnContext
+        instance. Can be overridden in derived classes.
+        :param context:
+        :return:
+        """
+        for attribute in [
+            "adapter",
+            "activity",
+            "_responded",
+            "_services",
+            "_on_send_activities",
+            "_on_update_activity",
+            "_on_delete_activity",
+        ]:
+            setattr(context, attribute, getattr(self, attribute))
+
+    @property
+    def activity(self):
+        """
+        The received activity.
+        :return:
+        """
+        return self._activity
+
+    @activity.setter
+    def activity(self, value):
+        """
+        Used to set TurnContext._activity when a context object is created. Only takes instances of Activities.
+        :param value:
+        :return:
+        """
+        if not isinstance(value, Activity):
+            raise TypeError(
+                "TurnContext: cannot set `activity` to a type other than Activity."
+            )
+        self._activity = value
+
+    @property
+    def responded(self) -> bool:
+        """
+        If `true` at least one response has been sent for the current turn of conversation.
+        :return:
+        """
+        return self._responded
+
+    @responded.setter
+    def responded(self, value: bool):
+        if not value:
+            raise ValueError("TurnContext: cannot set TurnContext.responded to False.")
+        self._responded = True
+
+    @property
+    def services(self):
+        """
+        Map of services and other values cached for the lifetime of the turn.
+        :return:
+        """
+        return self._services
+
+    def get(self, key: str) -> object:
+        if not key or not isinstance(key, str):
+            raise TypeError('"key" must be a valid string.')
+        try:
+            return self._services[key]
+        except KeyError:
+            raise KeyError("%s not found in TurnContext._services." % key)
+
+    def has(self, key: str) -> bool:
+        """
+        Returns True is set() has been called for a key. The cached value may be of type 'None'.
+        :param key:
+        :return:
+        """
+        if key in self._services:
+            return True
+        return False
+
+    def set(self, key: str, value: object) -> None:
+        """
+        Caches a value for the lifetime of the current turn.
+        :param key:
+        :param value:
+        :return:
+        """
+        if not key or not isinstance(key, str):
+            raise KeyError('"key" must be a valid string.')
+
+        self._services[key] = value
+
+    async def send_activity(
+        self,
+        activity_or_text: Union[Activity, str],
+        speak: str = None,
+        input_hint: str = None,
+    ) -> ResourceResponse:
+        """
+        Sends a single activity or message to the user.
+        :param activity_or_text:
+        :return:
+        """
+        if isinstance(activity_or_text, str):
+            activity_or_text = Activity(
+                text=activity_or_text,
+                input_hint=input_hint or InputHints.accepting_input,
+                speak=speak,
+            )
+
+        result = await self.send_activities([activity_or_text])
+        return result[0] if result else None
+
+    async def send_activities(
+        self, activities: List[Activity]
+    ) -> List[ResourceResponse]:
+        sent_non_trace_activity = False
+        ref = TurnContext.get_conversation_reference(self.activity)
+
+        def activity_validator(activity: Activity) -> Activity:
+            if not getattr(activity, "type", None):
+                activity.type = ActivityTypes.message
+            if activity.type != ActivityTypes.trace:
+                nonlocal sent_non_trace_activity
+                sent_non_trace_activity = True
+            if not activity.input_hint:
+                activity.input_hint = "acceptingInput"
+            activity.id = None
+            return activity
+
+        output = [
+            activity_validator(
+                TurnContext.apply_conversation_reference(deepcopy(act), ref)
+            )
+            for act in activities
+        ]
+
+        async def logic():
+            responses = await self.adapter.send_activities(self, output)
+            if sent_non_trace_activity:
+                self.responded = True
+            return responses
+
+        return await self._emit(self._on_send_activities, output, logic())
+
+    async def update_activity(self, activity: Activity):
+        """
+        Replaces an existing activity.
+        :param activity:
+        :return:
+        """
+        reference = TurnContext.get_conversation_reference(self.activity)
+
+        return await self._emit(
+            self._on_update_activity,
+            TurnContext.apply_conversation_reference(activity, reference),
+            self.adapter.update_activity(self, activity),
+        )
+
+    async def delete_activity(self, id_or_reference: Union[str, ConversationReference]):
+        """
+        Deletes an existing activity.
+        :param id_or_reference:
+        :return:
+        """
+        if isinstance(id_or_reference, str):
+            reference = TurnContext.get_conversation_reference(self.activity)
+            reference.activity_id = id_or_reference
+        else:
+            reference = id_or_reference
+        return await self._emit(
+            self._on_delete_activity,
+            reference,
+            self.adapter.delete_activity(self, reference),
+        )
+
+    def on_send_activities(self, handler) -> "TurnContext":
+        """
+        Registers a handler to be notified of and potentially intercept the sending of activities.
+        :param handler:
+        :return:
+        """
+        self._on_send_activities.append(handler)
+        return self
+
+    def on_update_activity(self, handler) -> "TurnContext":
+        """
+        Registers a handler to be notified of and potentially intercept an activity being updated.
+        :param handler:
+        :return:
+        """
+        self._on_update_activity.append(handler)
+        return self
+
+    def on_delete_activity(self, handler) -> "TurnContext":
+        """
+        Registers a handler to be notified of and potentially intercept an activity being deleted.
+        :param handler:
+        :return:
+        """
+        self._on_delete_activity.append(handler)
+        return self
+
+    async def _emit(self, plugins, arg, logic):
+        handlers = copy(plugins)
+
+        async def emit_next(i: int):
+            context = self
+            try:
+                if i < len(handlers):
+
+                    async def next_handler():
+                        await emit_next(i + 1)
+
+                    await handlers[i](context, arg, next_handler)
+
+            except Exception as error:
+                raise error
+
+        await emit_next(0)
+        # logic does not use parentheses because it's a coroutine
+        return await logic
+
+    async def send_trace_activity(
+        self, name: str, value: object, value_type: str, label: str
+    ) -> ResourceResponse:
+        trace_activity = Activity(
+            type=ActivityTypes.trace,
+            timestamp=datetime.utcnow(),
+            name=name,
+            value=value,
+            value_type=value_type,
+            label=label,
+        )
+
+        return await self.send_activity(trace_activity)
+
+    @staticmethod
+    def get_conversation_reference(activity: Activity) -> ConversationReference:
+        """
+        Returns the conversation reference for an activity. This can be saved as a plain old JSON
+        object and then later used to message the user proactively.
+
+        Usage Example:
+        reference = TurnContext.get_conversation_reference(context.request)
+        :param activity:
+        :return:
+        """
+        return ConversationReference(
+            activity_id=activity.id,
+            user=copy(activity.from_property),
+            bot=copy(activity.recipient),
+            conversation=copy(activity.conversation),
+            channel_id=activity.channel_id,
+            service_url=activity.service_url,
+        )
+
+    @staticmethod
+    def apply_conversation_reference(
+        activity: Activity, reference: ConversationReference, is_incoming: bool = False
+    ) -> Activity:
+        """
+        Updates an activity with the delivery information from a conversation reference. Calling
+        this after get_conversation_reference on an incoming activity
+        will properly address the reply to a received activity.
+        :param activity:
+        :param reference:
+        :param is_incoming:
+        :return:
+        """
+        activity.channel_id = reference.channel_id
+        activity.service_url = reference.service_url
+        activity.conversation = reference.conversation
+        if is_incoming:
+            activity.from_property = reference.user
+            activity.recipient = reference.bot
+            if reference.activity_id:
+                activity.id = reference.activity_id
+        else:
+            activity.from_property = reference.bot
+            activity.recipient = reference.user
+            if reference.activity_id:
+                activity.reply_to_id = reference.activity_id
+
+        return activity
+
+    @staticmethod
+    def get_reply_conversation_reference(
+        activity: Activity, reply: ResourceResponse
+    ) -> ConversationReference:
+        reference: ConversationReference = TurnContext.get_conversation_reference(
+            activity
+        )
+
+        # Update the reference with the new outgoing Activity's id.
+        reference.activity_id = reply.id
+
+        return reference
+
+    @staticmethod
+    def remove_recipient_mention(activity: Activity) -> str:
+        return TurnContext.remove_mention_text(activity, activity.recipient.id)
+
+    @staticmethod
+    def remove_mention_text(activity: Activity, identifier: str) -> str:
+        mentions = TurnContext.get_mentions(activity)
+        for mention in mentions:
+            if mention.additional_properties["mentioned"]["id"] == identifier:
+                mention_name_match = re.match(
+                    r"<at(.*)>(.*?)<\/at>",
+                    re.escape(mention.additional_properties["text"]),
+                    re.IGNORECASE,
+                )
+                if mention_name_match:
+                    activity.text = re.sub(
+                        mention_name_match.groups()[1], "", activity.text
+                    )
+                    activity.text = re.sub(r"<at><\/at>", "", activity.text)
+        return activity.text
+
+    @staticmethod
+    def get_mentions(activity: Activity) -> List[Mention]:
+        result: List[Mention] = []
+        if activity.entities is not None:
+            for entity in activity.entities:
+                if entity.type.lower() == "mention":
+                    result.append(entity)
+
+        return result


Test patch:
diff --git a/libraries/botbuilder-core/tests/test_turn_context.py b/libraries/botbuilder-core/tests/test_turn_context.py
--- a/libraries/botbuilder-core/tests/test_turn_context.py
+++ b/libraries/botbuilder-core/tests/test_turn_context.py
@@ -1,351 +1,372 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-from typing import Callable, List
-import aiounittest
-
-from botbuilder.schema import (
-    Activity,
-    ActivityTypes,
-    ChannelAccount,
-    ConversationAccount,
-    Entity,
-    Mention,
-    ResourceResponse,
-)
-from botbuilder.core import BotAdapter, MessageFactory, TurnContext
-
-ACTIVITY = Activity(
-    id="1234",
-    type="message",
-    text="test",
-    from_property=ChannelAccount(id="user", name="User Name"),
-    recipient=ChannelAccount(id="bot", name="Bot Name"),
-    conversation=ConversationAccount(id="convo", name="Convo Name"),
-    channel_id="UnitTest",
-    service_url="https://example.org",
-)
-
-
-class SimpleAdapter(BotAdapter):
-    async def send_activities(self, context, activities) -> List[ResourceResponse]:
-        responses = []
-        assert context is not None
-        assert activities is not None
-        assert isinstance(activities, list)
-        assert activities
-        for (idx, activity) in enumerate(activities):  # pylint: disable=unused-variable
-            assert isinstance(activity, Activity)
-            assert activity.type == "message" or activity.type == ActivityTypes.trace
-            responses.append(ResourceResponse(id="5678"))
-        return responses
-
-    async def update_activity(self, context, activity):
-        assert context is not None
-        assert activity is not None
-        return ResourceResponse(id=activity.id)
-
-    async def delete_activity(self, context, reference):
-        assert context is not None
-        assert reference is not None
-        assert reference.activity_id == ACTIVITY.id
-
-
-class TestBotContext(aiounittest.AsyncTestCase):
-    def test_should_create_context_with_request_and_adapter(self):
-        TurnContext(SimpleAdapter(), ACTIVITY)
-
-    def test_should_not_create_context_without_request(self):
-        try:
-            TurnContext(SimpleAdapter(), None)
-        except TypeError:
-            pass
-        except Exception as error:
-            raise error
-
-    def test_should_not_create_context_without_adapter(self):
-        try:
-            TurnContext(None, ACTIVITY)
-        except TypeError:
-            pass
-        except Exception as error:
-            raise error
-
-    def test_should_create_context_with_older_context(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        TurnContext(context)
-
-    def test_copy_to_should_copy_all_references(self):
-        # pylint: disable=protected-access
-        old_adapter = SimpleAdapter()
-        old_activity = Activity(id="2", type="message", text="test copy")
-        old_context = TurnContext(old_adapter, old_activity)
-        old_context.responded = True
-
-        async def send_activities_handler(context, activities, next_handler):
-            assert context is not None
-            assert activities is not None
-            assert next_handler is not None
-            await next_handler
-
-        async def delete_activity_handler(context, reference, next_handler):
-            assert context is not None
-            assert reference is not None
-            assert next_handler is not None
-            await next_handler
-
-        async def update_activity_handler(context, activity, next_handler):
-            assert context is not None
-            assert activity is not None
-            assert next_handler is not None
-            await next_handler
-
-        old_context.on_send_activities(send_activities_handler)
-        old_context.on_delete_activity(delete_activity_handler)
-        old_context.on_update_activity(update_activity_handler)
-
-        adapter = SimpleAdapter()
-        new_context = TurnContext(adapter, ACTIVITY)
-        assert not new_context._on_send_activities  # pylint: disable=protected-access
-        assert not new_context._on_update_activity  # pylint: disable=protected-access
-        assert not new_context._on_delete_activity  # pylint: disable=protected-access
-
-        old_context.copy_to(new_context)
-
-        assert new_context.adapter == old_adapter
-        assert new_context.activity == old_activity
-        assert new_context.responded is True
-        assert (
-            len(new_context._on_send_activities) == 1
-        )  # pylint: disable=protected-access
-        assert (
-            len(new_context._on_update_activity) == 1
-        )  # pylint: disable=protected-access
-        assert (
-            len(new_context._on_delete_activity) == 1
-        )  # pylint: disable=protected-access
-
-    def test_responded_should_be_automatically_set_to_false(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        assert context.responded is False
-
-    def test_should_be_able_to_set_responded_to_true(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        assert context.responded is False
-        context.responded = True
-        assert context.responded
-
-    def test_should_not_be_able_to_set_responded_to_false(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        try:
-            context.responded = False
-        except ValueError:
-            pass
-        except Exception as error:
-            raise error
-
-    async def test_should_call_on_delete_activity_handlers_before_deletion(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        called = False
-
-        async def delete_handler(context, reference, next_handler_coroutine):
-            nonlocal called
-            called = True
-            assert reference is not None
-            assert context is not None
-            assert reference.activity_id == "1234"
-            await next_handler_coroutine()
-
-        context.on_delete_activity(delete_handler)
-        await context.delete_activity(ACTIVITY.id)
-        assert called is True
-
-    async def test_should_call_multiple_on_delete_activity_handlers_in_order(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        called_first = False
-        called_second = False
-
-        async def first_delete_handler(context, reference, next_handler_coroutine):
-            nonlocal called_first, called_second
-            assert (
-                called_first is False
-            ), "called_first should not be True before first_delete_handler is called."
-            called_first = True
-            assert (
-                called_second is False
-            ), "Second on_delete_activity handler was called before first."
-            assert reference is not None
-            assert context is not None
-            assert reference.activity_id == "1234"
-            await next_handler_coroutine()
-
-        async def second_delete_handler(context, reference, next_handler_coroutine):
-            nonlocal called_first, called_second
-            assert called_first
-            assert (
-                called_second is False
-            ), "called_second was set to True before second handler was called."
-            called_second = True
-            assert reference is not None
-            assert context is not None
-            assert reference.activity_id == "1234"
-            await next_handler_coroutine()
-
-        context.on_delete_activity(first_delete_handler)
-        context.on_delete_activity(second_delete_handler)
-        await context.delete_activity(ACTIVITY.id)
-        assert called_first is True
-        assert called_second is True
-
-    async def test_should_call_send_on_activities_handler_before_send(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        called = False
-
-        async def send_handler(context, activities, next_handler_coroutine):
-            nonlocal called
-            called = True
-            assert activities is not None
-            assert context is not None
-            assert not activities[0].id
-            await next_handler_coroutine()
-
-        context.on_send_activities(send_handler)
-        await context.send_activity(ACTIVITY)
-        assert called is True
-
-    async def test_should_call_on_update_activity_handler_before_update(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        called = False
-
-        async def update_handler(context, activity, next_handler_coroutine):
-            nonlocal called
-            called = True
-            assert activity is not None
-            assert context is not None
-            assert activity.id == "1234"
-            await next_handler_coroutine()
-
-        context.on_update_activity(update_handler)
-        await context.update_activity(ACTIVITY)
-        assert called is True
-
-    async def test_update_activity_should_apply_conversation_reference(self):
-        activity_id = "activity ID"
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        called = False
-
-        async def update_handler(context, activity, next_handler_coroutine):
-            nonlocal called
-            called = True
-            assert context is not None
-            assert activity.id == activity_id
-            assert activity.conversation.id == ACTIVITY.conversation.id
-            await next_handler_coroutine()
-
-        context.on_update_activity(update_handler)
-        new_activity = MessageFactory.text("test text")
-        new_activity.id = activity_id
-        update_result = await context.update_activity(new_activity)
-        assert called is True
-        assert update_result.id == activity_id
-
-    def test_get_conversation_reference_should_return_valid_reference(self):
-        reference = TurnContext.get_conversation_reference(ACTIVITY)
-
-        assert reference.activity_id == ACTIVITY.id
-        assert reference.user == ACTIVITY.from_property
-        assert reference.bot == ACTIVITY.recipient
-        assert reference.conversation == ACTIVITY.conversation
-        assert reference.channel_id == ACTIVITY.channel_id
-        assert reference.service_url == ACTIVITY.service_url
-
-    def test_apply_conversation_reference_should_return_prepare_reply_when_is_incoming_is_false(
-        self,
-    ):
-        reference = TurnContext.get_conversation_reference(ACTIVITY)
-        reply = TurnContext.apply_conversation_reference(
-            Activity(type="message", text="reply"), reference
-        )
-
-        assert reply.recipient == ACTIVITY.from_property
-        assert reply.from_property == ACTIVITY.recipient
-        assert reply.conversation == ACTIVITY.conversation
-        assert reply.service_url == ACTIVITY.service_url
-        assert reply.channel_id == ACTIVITY.channel_id
-
-    def test_apply_conversation_reference_when_is_incoming_is_true_should_not_prepare_a_reply(
-        self,
-    ):
-        reference = TurnContext.get_conversation_reference(ACTIVITY)
-        reply = TurnContext.apply_conversation_reference(
-            Activity(type="message", text="reply"), reference, True
-        )
-
-        assert reply.recipient == ACTIVITY.recipient
-        assert reply.from_property == ACTIVITY.from_property
-        assert reply.conversation == ACTIVITY.conversation
-        assert reply.service_url == ACTIVITY.service_url
-        assert reply.channel_id == ACTIVITY.channel_id
-
-    async def test_should_get_conversation_reference_using_get_reply_conversation_reference(
-        self,
-    ):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        reply = await context.send_activity("test")
-
-        assert reply.id, "reply has an id"
-
-        reference = TurnContext.get_reply_conversation_reference(
-            context.activity, reply
-        )
-
-        assert reference.activity_id, "reference has an activity id"
-        assert (
-            reference.activity_id == reply.id
-        ), "reference id matches outgoing reply id"
-
-    def test_should_remove_at_mention_from_activity(self):
-        activity = Activity(
-            type="message",
-            text="<at>TestOAuth619</at> test activity",
-            recipient=ChannelAccount(id="TestOAuth619"),
-            entities=[
-                Entity().deserialize(
-                    Mention(
-                        type="mention",
-                        text="<at>TestOAuth619</at>",
-                        mentioned=ChannelAccount(name="Bot", id="TestOAuth619"),
-                    ).serialize()
-                )
-            ],
-        )
-
-        text = TurnContext.remove_recipient_mention(activity)
-
-        assert text, " test activity"
-        assert activity.text, " test activity"
-
-    async def test_should_send_a_trace_activity(self):
-        context = TurnContext(SimpleAdapter(), ACTIVITY)
-        called = False
-
-        #  pylint: disable=unused-argument
-        async def aux_func(
-            ctx: TurnContext, activities: List[Activity], next: Callable
-        ):
-            nonlocal called
-            called = True
-            assert isinstance(activities, list), "activities not array."
-            assert len(activities) == 1, "invalid count of activities."
-            assert activities[0].type == ActivityTypes.trace, "type wrong."
-            assert activities[0].name == "name-text", "name wrong."
-            assert activities[0].value == "value-text", "value worng."
-            assert activities[0].value_type == "valueType-text", "valeuType wrong."
-            assert activities[0].label == "label-text", "label wrong."
-            return []
-
-        context.on_send_activities(aux_func)
-        await context.send_trace_activity(
-            "name-text", "value-text", "valueType-text", "label-text"
-        )
-        assert called
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Callable, List
+import aiounittest
+
+from botbuilder.schema import (
+    Activity,
+    ActivityTypes,
+    ChannelAccount,
+    ConversationAccount,
+    Entity,
+    Mention,
+    ResourceResponse,
+)
+from botbuilder.core import BotAdapter, MessageFactory, TurnContext
+
+ACTIVITY = Activity(
+    id="1234",
+    type="message",
+    text="test",
+    from_property=ChannelAccount(id="user", name="User Name"),
+    recipient=ChannelAccount(id="bot", name="Bot Name"),
+    conversation=ConversationAccount(id="convo", name="Convo Name"),
+    channel_id="UnitTest",
+    service_url="https://example.org",
+)
+
+
+class SimpleAdapter(BotAdapter):
+    async def send_activities(self, context, activities) -> List[ResourceResponse]:
+        responses = []
+        assert context is not None
+        assert activities is not None
+        assert isinstance(activities, list)
+        assert activities
+        for (idx, activity) in enumerate(activities):  # pylint: disable=unused-variable
+            assert isinstance(activity, Activity)
+            assert activity.type == "message" or activity.type == ActivityTypes.trace
+            responses.append(ResourceResponse(id="5678"))
+        return responses
+
+    async def update_activity(self, context, activity):
+        assert context is not None
+        assert activity is not None
+        return ResourceResponse(id=activity.id)
+
+    async def delete_activity(self, context, reference):
+        assert context is not None
+        assert reference is not None
+        assert reference.activity_id == ACTIVITY.id
+
+
+class TestBotContext(aiounittest.AsyncTestCase):
+    def test_should_create_context_with_request_and_adapter(self):
+        TurnContext(SimpleAdapter(), ACTIVITY)
+
+    def test_should_not_create_context_without_request(self):
+        try:
+            TurnContext(SimpleAdapter(), None)
+        except TypeError:
+            pass
+        except Exception as error:
+            raise error
+
+    def test_should_not_create_context_without_adapter(self):
+        try:
+            TurnContext(None, ACTIVITY)
+        except TypeError:
+            pass
+        except Exception as error:
+            raise error
+
+    def test_should_create_context_with_older_context(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        TurnContext(context)
+
+    def test_copy_to_should_copy_all_references(self):
+        # pylint: disable=protected-access
+        old_adapter = SimpleAdapter()
+        old_activity = Activity(id="2", type="message", text="test copy")
+        old_context = TurnContext(old_adapter, old_activity)
+        old_context.responded = True
+
+        async def send_activities_handler(context, activities, next_handler):
+            assert context is not None
+            assert activities is not None
+            assert next_handler is not None
+            await next_handler
+
+        async def delete_activity_handler(context, reference, next_handler):
+            assert context is not None
+            assert reference is not None
+            assert next_handler is not None
+            await next_handler
+
+        async def update_activity_handler(context, activity, next_handler):
+            assert context is not None
+            assert activity is not None
+            assert next_handler is not None
+            await next_handler
+
+        old_context.on_send_activities(send_activities_handler)
+        old_context.on_delete_activity(delete_activity_handler)
+        old_context.on_update_activity(update_activity_handler)
+
+        adapter = SimpleAdapter()
+        new_context = TurnContext(adapter, ACTIVITY)
+        assert not new_context._on_send_activities  # pylint: disable=protected-access
+        assert not new_context._on_update_activity  # pylint: disable=protected-access
+        assert not new_context._on_delete_activity  # pylint: disable=protected-access
+
+        old_context.copy_to(new_context)
+
+        assert new_context.adapter == old_adapter
+        assert new_context.activity == old_activity
+        assert new_context.responded is True
+        assert (
+            len(new_context._on_send_activities) == 1
+        )  # pylint: disable=protected-access
+        assert (
+            len(new_context._on_update_activity) == 1
+        )  # pylint: disable=protected-access
+        assert (
+            len(new_context._on_delete_activity) == 1
+        )  # pylint: disable=protected-access
+
+    def test_responded_should_be_automatically_set_to_false(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        assert context.responded is False
+
+    def test_should_be_able_to_set_responded_to_true(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        assert context.responded is False
+        context.responded = True
+        assert context.responded
+
+    def test_should_not_be_able_to_set_responded_to_false(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        try:
+            context.responded = False
+        except ValueError:
+            pass
+        except Exception as error:
+            raise error
+
+    async def test_should_call_on_delete_activity_handlers_before_deletion(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called = False
+
+        async def delete_handler(context, reference, next_handler_coroutine):
+            nonlocal called
+            called = True
+            assert reference is not None
+            assert context is not None
+            assert reference.activity_id == "1234"
+            await next_handler_coroutine()
+
+        context.on_delete_activity(delete_handler)
+        await context.delete_activity(ACTIVITY.id)
+        assert called is True
+
+    async def test_should_call_multiple_on_delete_activity_handlers_in_order(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called_first = False
+        called_second = False
+
+        async def first_delete_handler(context, reference, next_handler_coroutine):
+            nonlocal called_first, called_second
+            assert (
+                called_first is False
+            ), "called_first should not be True before first_delete_handler is called."
+            called_first = True
+            assert (
+                called_second is False
+            ), "Second on_delete_activity handler was called before first."
+            assert reference is not None
+            assert context is not None
+            assert reference.activity_id == "1234"
+            await next_handler_coroutine()
+
+        async def second_delete_handler(context, reference, next_handler_coroutine):
+            nonlocal called_first, called_second
+            assert called_first
+            assert (
+                called_second is False
+            ), "called_second was set to True before second handler was called."
+            called_second = True
+            assert reference is not None
+            assert context is not None
+            assert reference.activity_id == "1234"
+            await next_handler_coroutine()
+
+        context.on_delete_activity(first_delete_handler)
+        context.on_delete_activity(second_delete_handler)
+        await context.delete_activity(ACTIVITY.id)
+        assert called_first is True
+        assert called_second is True
+
+    async def test_should_call_send_on_activities_handler_before_send(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called = False
+
+        async def send_handler(context, activities, next_handler_coroutine):
+            nonlocal called
+            called = True
+            assert activities is not None
+            assert context is not None
+            assert not activities[0].id
+            await next_handler_coroutine()
+
+        context.on_send_activities(send_handler)
+        await context.send_activity(ACTIVITY)
+        assert called is True
+
+    async def test_should_call_on_update_activity_handler_before_update(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called = False
+
+        async def update_handler(context, activity, next_handler_coroutine):
+            nonlocal called
+            called = True
+            assert activity is not None
+            assert context is not None
+            assert activity.id == "1234"
+            await next_handler_coroutine()
+
+        context.on_update_activity(update_handler)
+        await context.update_activity(ACTIVITY)
+        assert called is True
+
+    async def test_update_activity_should_apply_conversation_reference(self):
+        activity_id = "activity ID"
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called = False
+
+        async def update_handler(context, activity, next_handler_coroutine):
+            nonlocal called
+            called = True
+            assert context is not None
+            assert activity.id == activity_id
+            assert activity.conversation.id == ACTIVITY.conversation.id
+            await next_handler_coroutine()
+
+        context.on_update_activity(update_handler)
+        new_activity = MessageFactory.text("test text")
+        new_activity.id = activity_id
+        update_result = await context.update_activity(new_activity)
+        assert called is True
+        assert update_result.id == activity_id
+
+    def test_get_conversation_reference_should_return_valid_reference(self):
+        reference = TurnContext.get_conversation_reference(ACTIVITY)
+
+        assert reference.activity_id == ACTIVITY.id
+        assert reference.user == ACTIVITY.from_property
+        assert reference.bot == ACTIVITY.recipient
+        assert reference.conversation == ACTIVITY.conversation
+        assert reference.channel_id == ACTIVITY.channel_id
+        assert reference.service_url == ACTIVITY.service_url
+
+    def test_apply_conversation_reference_should_return_prepare_reply_when_is_incoming_is_false(
+        self,
+    ):
+        reference = TurnContext.get_conversation_reference(ACTIVITY)
+        reply = TurnContext.apply_conversation_reference(
+            Activity(type="message", text="reply"), reference
+        )
+
+        assert reply.recipient == ACTIVITY.from_property
+        assert reply.from_property == ACTIVITY.recipient
+        assert reply.conversation == ACTIVITY.conversation
+        assert reply.service_url == ACTIVITY.service_url
+        assert reply.channel_id == ACTIVITY.channel_id
+
+    def test_apply_conversation_reference_when_is_incoming_is_true_should_not_prepare_a_reply(
+        self,
+    ):
+        reference = TurnContext.get_conversation_reference(ACTIVITY)
+        reply = TurnContext.apply_conversation_reference(
+            Activity(type="message", text="reply"), reference, True
+        )
+
+        assert reply.recipient == ACTIVITY.recipient
+        assert reply.from_property == ACTIVITY.from_property
+        assert reply.conversation == ACTIVITY.conversation
+        assert reply.service_url == ACTIVITY.service_url
+        assert reply.channel_id == ACTIVITY.channel_id
+
+    async def test_should_get_conversation_reference_using_get_reply_conversation_reference(
+        self,
+    ):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        reply = await context.send_activity("test")
+
+        assert reply.id, "reply has an id"
+
+        reference = TurnContext.get_reply_conversation_reference(
+            context.activity, reply
+        )
+
+        assert reference.activity_id, "reference has an activity id"
+        assert (
+            reference.activity_id == reply.id
+        ), "reference id matches outgoing reply id"
+
+    def test_should_remove_at_mention_from_activity(self):
+        activity = Activity(
+            type="message",
+            text="<at>TestOAuth619</at> test activity",
+            recipient=ChannelAccount(id="TestOAuth619"),
+            entities=[
+                Entity().deserialize(
+                    Mention(
+                        type="mention",
+                        text="<at>TestOAuth619</at>",
+                        mentioned=ChannelAccount(name="Bot", id="TestOAuth619"),
+                    ).serialize()
+                )
+            ],
+        )
+
+        text = TurnContext.remove_recipient_mention(activity)
+
+        assert text, " test activity"
+        assert activity.text, " test activity"
+
+    def test_should_remove_at_mention_with_regex_characters(self):
+        activity = Activity(
+            type="message",
+            text="<at>Test (*.[]$%#^&?)</at> test activity",
+            recipient=ChannelAccount(id="Test (*.[]$%#^&?)"),
+            entities=[
+                Entity().deserialize(
+                    Mention(
+                        type="mention",
+                        text="<at>Test (*.[]$%#^&?)</at>",
+                        mentioned=ChannelAccount(name="Bot", id="Test (*.[]$%#^&?)"),
+                    ).serialize()
+                )
+            ],
+        )
+
+        text = TurnContext.remove_recipient_mention(activity)
+
+        assert text, " test activity"
+        assert activity.text, " test activity"
+
+    async def test_should_send_a_trace_activity(self):
+        context = TurnContext(SimpleAdapter(), ACTIVITY)
+        called = False
+
+        #  pylint: disable=unused-argument
+        async def aux_func(
+            ctx: TurnContext, activities: List[Activity], next: Callable
+        ):
+            nonlocal called
+            called = True
+            assert isinstance(activities, list), "activities not array."
+            assert len(activities) == 1, "invalid count of activities."
+            assert activities[0].type == ActivityTypes.trace, "type wrong."
+            assert activities[0].name == "name-text", "name wrong."
+            assert activities[0].value == "value-text", "value worng."
+            assert activities[0].value_type == "valueType-text", "valeuType wrong."
+            assert activities[0].label == "label-text", "label wrong."
+            return []
+
+        context.on_send_activities(aux_func)
+        await context.send_trace_activity(
+            "name-text", "value-text", "valueType-text", "label-text"
+        )
+        assert called