Repository: microsoft/botbuilder-python
Pull request #180 (2019-05-20 21:27:34)

On turn error on Bot Adapter
- Added on_turn_error functionality to bot_adapter with updates to all related samples 
- Ported bot assert
- Added some missing annotations.

*This PR address issue #167  


Patch:
diff --git a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer.py b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer.py
index 89adbffc..8015d0e3 100644
--- a/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer.py
+++ b/libraries/botbuilder-ai/botbuilder/ai/luis/luis_recognizer.py
@@ -290,7 +290,7 @@ class LuisRecognizer(object):
         telemetry_metrics: Dict[str, float],
     ) -> RecognizerResult:
 
-        BotAssert.context_not_null(turn_context)
+        BotAssert.context_not_none(turn_context)
 
         if turn_context.activity.type != ActivityTypes.message:
             return None
diff --git a/libraries/botbuilder-core/botbuilder/core/__init__.py b/libraries/botbuilder-core/botbuilder/core/__init__.py
index 80ac54c2..8a7ebe9e 100644
--- a/libraries/botbuilder-core/botbuilder/core/__init__.py
+++ b/libraries/botbuilder-core/botbuilder/core/__init__.py
@@ -4,10 +4,11 @@
 # Licensed under the MIT License. See License.txt in the project root for
 # license information.
 # --------------------------------------------------------------------------
+from . import conversation_reference_extension
 
 from .about import __version__
 from .activity_handler import ActivityHandler
-from .assertions import BotAssert
+from .bot_assert import BotAssert
 from .bot_adapter import BotAdapter
 from .bot_framework_adapter import BotFrameworkAdapter, BotFrameworkAdapterSettings
 from .bot_state import BotState
@@ -35,6 +36,7 @@ __all__ = ['ActivityHandler',
            'calculate_change_hash',
            'CardFactory',
            'ConversationState',
+           'conversation_reference_extension',
            'MemoryStorage',
            'MessageFactory',
            'Middleware',
diff --git a/libraries/botbuilder-core/botbuilder/core/assertions.py b/libraries/botbuilder-core/botbuilder/core/assertions.py
deleted file mode 100644
index c2d48341..00000000
--- a/libraries/botbuilder-core/botbuilder/core/assertions.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
-
-class BotAssert(object):
-
-    @staticmethod
-    def activity_not_null(activity):
-        if not activity:
-            raise TypeError()
-
-    @staticmethod
-    def context_not_null(context):
-        if not context:
-            raise TypeError()
-
-    @staticmethod
-    def conversation_reference_not_null(reference):
-        if not reference:
-            raise TypeError()
-
-    @staticmethod
-    def adapter_not_null(adapter):
-        if not adapter:
-            raise TypeError()
-
-    @staticmethod
-    def activity_list_not_null(activity_list):
-        if not activity_list:
-            raise TypeError()
-
-    @staticmethod
-    def middleware_not_null(middleware):
-        if not middleware:
-            raise TypeError()
-
-    @staticmethod
-    def middleware_set_not_null(middleware):
-        if not middleware:
-            raise TypeError()
diff --git a/libraries/botbuilder-core/botbuilder/core/bot_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_adapter.py
index 83868375..aef52f17 100644
--- a/libraries/botbuilder-core/botbuilder/core/bot_adapter.py
+++ b/libraries/botbuilder-core/botbuilder/core/bot_adapter.py
@@ -2,16 +2,19 @@
 # Licensed under the MIT License.
 
 from abc import ABC, abstractmethod
-from typing import List, Callable
+from typing import List, Callable, Awaitable
 from botbuilder.schema import Activity, ConversationReference
 
+from . import conversation_reference_extension
+from .bot_assert import BotAssert
 from .turn_context import TurnContext
 from .middleware_set import MiddlewareSet
 
 
 class BotAdapter(ABC):
-    def __init__(self):
+    def __init__(self, on_turn_error: Callable[[TurnContext, Exception], Awaitable] = None):
         self._middleware = MiddlewareSet()
+        self.on_turn_error = on_turn_error
 
     @abstractmethod
     async def send_activities(self, context: TurnContext, activities: List[Activity]):
@@ -47,8 +50,23 @@ class BotAdapter(ABC):
         :return:
         """
         self._middleware.use(middleware)
+        return self
+    
+    async def continue_conversation(self, bot_id: str, reference: ConversationReference, callback: Callable):
+        """
+        Sends a proactive message to a conversation. Call this method to proactively send a message to a conversation.
+        Most _channels require a user to initiate a conversation with a bot before the bot can send activities 
+        to the user.
+        :param bot_id: The application ID of the bot. This paramter is ignored in
+        single tenant the Adpters (Console, Test, etc) but is critical to the BotFrameworkAdapter
+        which is multi-tenant aware. </param>
+        :param reference: A reference to the conversation to continue.</param>
+        :param callback: The method to call for the resulting bot turn.</param>
+        """
+        context = TurnContext(self, conversation_reference_extension.get_continuation_activity(reference))
+        return await self.run_pipeline(context, callback)
 
-    async def run_middleware(self, context: TurnContext, callback: Callable=None):
+    async def run_pipeline(self, context: TurnContext, callback: Callable[[TurnContext], Awaitable]= None):
         """
         Called by the parent class to run the adapters middleware set and calls the passed in `callback()` handler at
         the end of the chain.
@@ -56,4 +74,17 @@ class BotAdapter(ABC):
         :param callback:
         :return:
         """
-        return await self._middleware.receive_activity_with_status(context, callback)
+        BotAssert.context_not_none(context)
+
+        if context.activity is not None:
+            try:
+                return await self._middleware.receive_activity_with_status(context, callback)
+            except Exception as error:
+                if self.on_turn_error is not None:
+                    await self.on_turn_error(context, error)
+                else:
+                    raise error
+        else:
+            # callback to caller on proactive case
+            if callback is not None:
+                await callback(context)
diff --git a/libraries/botbuilder-core/botbuilder/core/bot_assert.py b/libraries/botbuilder-core/botbuilder/core/bot_assert.py
new file mode 100644
index 00000000..8b1f42b7
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/bot_assert.py
@@ -0,0 +1,66 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import List
+
+from botbuilder.schema import Activity, ConversationReference
+from .middleware_set import Middleware
+from .turn_context import TurnContext
+
+class BotAssert():
+
+    @staticmethod
+    def activity_not_none(activity: Activity) -> None:
+        """
+        Checks that an activity object is not None
+        :param activity: The activity object
+        """
+        if activity is None:
+            raise TypeError(activity.__class__.__name__)
+       
+    @staticmethod
+    def context_not_none(turn_context: TurnContext) -> None:
+        """
+        Checks that a context object is not None
+        :param turn_context: The context object
+        """
+        if turn_context is None:
+            raise TypeError(turn_context.__class__.__name__)
+       
+    @staticmethod
+    def conversation_reference_not_none(reference: ConversationReference) -> None:
+        """
+        Checks that a conversation reference object is not None
+        :param reference: The conversation reference object
+        """
+        if reference is None:
+            raise TypeError(reference.__class__.__name__)
+    
+    @staticmethod
+    def activity_list_not_none(activities: List[Activity]) -> None:
+        """
+        Checks that an activity list is not None
+        :param activities: The activity list
+        """
+        if activities is None:
+            raise TypeError(activities.__class__.__name__)
+       
+    @staticmethod
+    def middleware_not_none(middleware: Middleware) -> None:
+        """
+        Checks that a middleware object is not None
+        :param middleware: The middleware object
+        """
+        if middleware is None:
+            raise TypeError(middleware.__class__.__name__)
+       
+    @staticmethod
+    def middleware_list_not_none(middleware: List[Middleware]) -> None:
+        """
+        Checks that a middeware list is not None
+        :param activities: The middleware list
+        """
+        if middleware is None:
+            raise TypeError(middleware.__class__.__name__)
+       
+   
\ No newline at end of file
diff --git a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
index 6ae018b9..bbebb3e2 100644
--- a/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
+++ b/libraries/botbuilder-core/botbuilder/core/bot_framework_adapter.py
@@ -2,7 +2,7 @@
 # Licensed under the MIT License.
 
 import asyncio
-from typing import List, Callable
+from typing import List, Callable, Awaitable
 from botbuilder.schema import (Activity, ChannelAccount,
                                ConversationAccount,
                                ConversationParameters, ConversationReference,
@@ -46,9 +46,9 @@ class BotFrameworkAdapter(BotAdapter):
         """
         request = TurnContext.apply_conversation_reference(Activity(), reference, is_incoming=True)
         context = self.create_context(request)
-        return await self.run_middleware(context, logic)
+        return await self.run_pipeline(context, logic)
 
-    async def create_conversation(self, reference: ConversationReference, logic):
+    async def create_conversation(self, reference: ConversationReference, logic: Callable[[TurnContext], Awaitable]=None):
         """
         Starts a new conversation with a user. This is typically used to Direct Message (DM) a member
         of a group.
@@ -71,7 +71,7 @@ class BotFrameworkAdapter(BotAdapter):
                 request.service_url = resource_response.service_url
 
             context = self.create_context(request)
-            return await self.run_middleware(context, logic)
+            return await self.run_pipeline(context, logic)
 
         except Exception as e:
             raise e
@@ -92,7 +92,7 @@ class BotFrameworkAdapter(BotAdapter):
         await self.authenticate_request(activity, auth_header)
         context = self.create_context(activity)
 
-        return await self.run_middleware(context, logic)
+        return await self.run_pipeline(context, logic)
 
     async def authenticate_request(self, request: Activity, auth_header: str):
         """
diff --git a/libraries/botbuilder-core/botbuilder/core/conversation_reference_extension.py b/libraries/botbuilder-core/botbuilder/core/conversation_reference_extension.py
new file mode 100644
index 00000000..80c4e9da
--- /dev/null
+++ b/libraries/botbuilder-core/botbuilder/core/conversation_reference_extension.py
@@ -0,0 +1,17 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import uuid
+from botbuilder.schema import Activity, ActivityTypes, ConversationReference
+
+def get_continuation_activity(reference: ConversationReference) -> Activity:
+    return Activity(
+        type= ActivityTypes.event,
+        name= "ContinueConversation",
+        id= str(uuid.uuid1()),
+        channel_id= reference.channel_id,
+        service_url= reference.service_url,
+        conversation= reference.conversation,
+        recipient= reference.bot,
+        from_property= reference.user,
+        relates_to= reference
+    )
\ No newline at end of file
diff --git a/libraries/botbuilder-core/botbuilder/core/middleware_set.py b/libraries/botbuilder-core/botbuilder/core/middleware_set.py
index 6c74e22b..e0f0f8a6 100644
--- a/libraries/botbuilder-core/botbuilder/core/middleware_set.py
+++ b/libraries/botbuilder-core/botbuilder/core/middleware_set.py
@@ -3,13 +3,14 @@
 
 from asyncio import iscoroutinefunction
 from abc import ABC, abstractmethod
+from typing import Awaitable, Callable
 
 from .turn_context import TurnContext
 
 
 class Middleware(ABC):
     @abstractmethod
-    def on_process_request(self, context: TurnContext, next): pass
+    def on_process_request(self, context: TurnContext, next: Callable): pass
 
 
 class AnonymousReceiveMiddleware(Middleware):
@@ -48,16 +49,16 @@ class MiddlewareSet(Middleware):
     async def receive_activity(self, context: TurnContext):
         await self.receive_activity_internal(context, None)
 
-    async def on_process_request(self, context, logic):
+    async def on_process_request(self, context: TurnContext, logic: Callable[[TurnContext], Awaitable]):
         await self.receive_activity_internal(context, None)
         await logic()
 
-    async def receive_activity_with_status(self, context: TurnContext, callback):
+    async def receive_activity_with_status(self, context: TurnContext, callback: Callable[[TurnContext], Awaitable]):
         return await self.receive_activity_internal(context, callback)
 
-    async def receive_activity_internal(self, context, callback, next_middleware_index=0):
+    async def receive_activity_internal(self, context: TurnContext, callback: Callable[[TurnContext], Awaitable], next_middleware_index: int = 0):
         if next_middleware_index == len(self._middleware):
-            if callback:
+            if callback is not None:
                 return await callback(context)
             else:
                 return None
diff --git a/libraries/botbuilder-core/botbuilder/core/turn_context.py b/libraries/botbuilder-core/botbuilder/core/turn_context.py
index 1e4326f4..872b1cc4 100644
--- a/libraries/botbuilder-core/botbuilder/core/turn_context.py
+++ b/libraries/botbuilder-core/botbuilder/core/turn_context.py
@@ -10,7 +10,6 @@ from botbuilder.schema import (
                             ConversationReference, 
                             ResourceResponse
                             )
-from .assertions import BotAssert
 
 
 class TurnContext(object):
@@ -148,7 +147,9 @@ class TurnContext(object):
             context._responded = True
             return responses
 
-        await self._emit(self._on_send_activities, output, callback(self, output))
+        result = await self._emit(self._on_send_activities, output, callback(self, output))
+
+        return result[0] if result is not None and len(result) > 0 else ResourceResponse()
 
     async def update_activity(self, activity: Activity):
         """
diff --git a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py
index 3eee7c60..e6d48cca 100644
--- a/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py
+++ b/libraries/botbuilder-dialogs/botbuilder/dialogs/dialog_set.py
@@ -57,7 +57,7 @@ class DialogSet():
         return self
 
     async def create_context(self, turn_context: TurnContext) -> DialogContext:
-        BotAssert.context_not_null(turn_context)
+        BotAssert.context_not_none(turn_context)
         
         if not self._dialog_state:
             raise RuntimeError("DialogSet.CreateContextAsync(): DialogSet created with a null IStatePropertyAccessor.")
diff --git a/samples/Console-EchoBot/adapter/console_adapter.py b/samples/Console-EchoBot/adapter/console_adapter.py
index cda807d2..31e12ea5 100644
--- a/samples/Console-EchoBot/adapter/console_adapter.py
+++ b/samples/Console-EchoBot/adapter/console_adapter.py
@@ -83,7 +83,7 @@ class ConsoleAdapter(BotAdapter):
 
                 activity = TurnContext.apply_conversation_reference(activity, self.reference, True)
                 context = TurnContext(self, activity)
-                await self.run_middleware(context, logic)
+                await self.run_pipeline(context, logic)
 
     async def send_activities(self, context: TurnContext, activities: List[Activity]):
         """
diff --git a/samples/Core-Bot/main.py b/samples/Core-Bot/main.py
index 82f580e2..cd95604d 100644
--- a/samples/Core-Bot/main.py
+++ b/samples/Core-Bot/main.py
@@ -7,6 +7,7 @@ This sample shows how to create a simple EchoBot with state.
 
 import yaml
 import os
+import sys
 from aiohttp import web
 from botbuilder.schema import (Activity, ActivityTypes)
 from botbuilder.core import (BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext,
@@ -25,6 +26,19 @@ PORT = cfg['Settings']['Port']
 SETTINGS = BotFrameworkAdapterSettings(cfg['Settings']['AppId'], cfg['Settings']['AppPassword'])
 ADAPTER = BotFrameworkAdapter(SETTINGS)
 
+# Catch-all for errors.
+async def on_error(context: TurnContext, error: Exception):
+    # This check writes out errors to console log
+    # NOTE: In production environment, you should consider logging this to Azure
+    #       application insights.
+    print(f'\n [on_turn_error]: { error }', file=sys.stderr)
+    # Send a message to the user
+    await context.send_activity('Oops. Something went wrong!')
+    # Clear out state
+    await conversation_state.delete(context)
+
+ADAPTER.on_turn_error = on_error
+
 # Create MemoryStorage, UserState and ConversationState
 memory = MemoryStorage()
 
diff --git a/samples/Rich-Cards-Bot/main.py b/samples/Rich-Cards-Bot/main.py
index 093f72ca..36a44b96 100644
--- a/samples/Rich-Cards-Bot/main.py
+++ b/samples/Rich-Cards-Bot/main.py
@@ -6,6 +6,7 @@ This sample shows how to use different types of rich cards.
 """
 import os
 import yaml
+import sys
 
 from aiohttp import web
 from botbuilder.schema import Activity, ActivityTypes
@@ -24,6 +25,19 @@ PORT = cfg['Settings']['Port']
 SETTINGS = BotFrameworkAdapterSettings(cfg['Settings']['AppId'], cfg['Settings']['AppPassword'])
 ADAPTER = BotFrameworkAdapter(SETTINGS)
 
+# Catch-all for errors.
+async def on_error(context: TurnContext, error: Exception):
+    # This check writes out errors to console log
+    # NOTE: In production environment, you should consider logging this to Azure
+    #       application insights.
+    print(f'\n [on_turn_error]: { error }', file=sys.stderr)
+    # Send a message to the user
+    await context.send_activity('Oops. Something went wrong!')
+    # Clear out state
+    await conversation_state.delete(context)
+
+ADAPTER.on_turn_error = on_error
+
 # Create MemoryStorage, UserState and ConversationState
 memory = MemoryStorage()
 
diff --git a/samples/State-Management-Bot/main.py b/samples/State-Management-Bot/main.py
index 1f625eba..d26dfd8f 100644
--- a/samples/State-Management-Bot/main.py
+++ b/samples/State-Management-Bot/main.py
@@ -6,6 +6,7 @@ This sample shows how to manage state in a bot.
 """
 import os
 import yaml
+import sys
 
 from aiohttp import web
 from botbuilder.schema import Activity, ActivityTypes
@@ -23,6 +24,19 @@ PORT = cfg['Settings']['Port']
 SETTINGS = BotFrameworkAdapterSettings(cfg['Settings']['AppId'], cfg['Settings']['AppPassword'])
 ADAPTER = BotFrameworkAdapter(SETTINGS)
 
+# Catch-all for errors.
+async def on_error(context: TurnContext, error: Exception):
+    # This check writes out errors to console log
+    # NOTE: In production environment, you should consider logging this to Azure
+    #       application insights.
+    print(f'\n [on_turn_error]: { error }', file=sys.stderr)
+    # Send a message to the user
+    await context.send_activity('Oops. Something went wrong!')
+    # Clear out state
+    await conversation_state.delete(context)
+
+ADAPTER.on_turn_error = on_error
+
 # Create MemoryStorage, UserState and ConversationState
 memory = MemoryStorage()
 
diff --git a/samples/python-flask/13.core-bot/main.py b/samples/python-flask/13.core-bot/main.py
index d7de4131..670af0d4 100644
--- a/samples/python-flask/13.core-bot/main.py
+++ b/samples/python-flask/13.core-bot/main.py
@@ -13,6 +13,7 @@ gi
 from functools import wraps
 import json
 import asyncio
+import sys
 from flask import Flask, jsonify, request, Response
 from botbuilder.schema import (Activity, ActivityTypes)
 from botbuilder.core import (BotFrameworkAdapter, BotFrameworkAdapterSettings, TurnContext,
@@ -29,6 +30,19 @@ app.config.from_object('config.DefaultConfig')
 SETTINGS = BotFrameworkAdapterSettings(app.config['APP_ID'], app.config['APP_PASSWORD'])
 ADAPTER = BotFrameworkAdapter(SETTINGS)
 
+# Catch-all for errors.
+async def on_error(context: TurnContext, error: Exception):
+    # This check writes out errors to console log
+    # NOTE: In production environment, you should consider logging this to Azure
+    #       application insights.
+    print(f'\n [on_turn_error]: { error }', file=sys.stderr)
+    # Send a message to the user
+    await context.send_activity('Oops. Something went wrong!')
+    # Clear out state
+    await conversation_state.delete(context)
+
+ADAPTER.on_turn_error = on_error
+
 # Create MemoryStorage, UserState and ConversationState
 memory = MemoryStorage()
 


Test patch:
diff --git a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
index 540ff61e..cf4896fa 100644
--- a/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
+++ b/libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
@@ -4,7 +4,7 @@
 import asyncio
 import inspect
 from datetime import datetime
-from typing import Coroutine, List
+from typing import Coroutine, List, Callable
 from copy import copy
 from ..bot_adapter import BotAdapter
 from ..turn_context import TurnContext 
@@ -77,15 +77,15 @@ class TestAdapter(BotAdapter):
         """
         self.updated_activities.append(activity)
 
-    async def continue_conversation(self, reference, logic):
+    async def continue_conversation(self, bot_id: str, reference: ConversationReference, callback: Callable):
         """
-        The `TestAdapter` doesn't implement `continueConversation()` and will return an error if it's
-        called.
+        The `TestAdapter` just calls parent implementation.
+        :param bot_id
         :param reference:
-        :param logic:
+        :param callback:
         :return:
         """
-        raise NotImplementedError('TestAdapter.continue_conversation(): is not implemented.')
+        await super().continue_conversation(bot_id, reference, callback)
 
     async def receive_activity(self, activity):
         """
@@ -110,7 +110,7 @@ class TestAdapter(BotAdapter):
 
         # Create context object and run middleware
         context = TurnContext(self, request)
-        return await self.run_middleware(context, self.logic)
+        return await self.run_pipeline(context, self.logic)
 
     async def send(self, user_says) -> object:
         """
diff --git a/libraries/botbuilder-core/tests/call_counting_middleware.py b/libraries/botbuilder-core/tests/call_counting_middleware.py
new file mode 100644
index 00000000..059ba07c
--- /dev/null
+++ b/libraries/botbuilder-core/tests/call_counting_middleware.py
@@ -0,0 +1,14 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from typing import Callable
+from botbuilder.core import Middleware, TurnContext
+
+class CallCountingMiddleware(Middleware):
+
+    def __init__(self):
+        self.counter = 0
+
+    def on_process_request(self, context: TurnContext, next: Callable):
+        self.counter += 1
+        next()
\ No newline at end of file
diff --git a/libraries/botbuilder-core/tests/simple_adapter.py b/libraries/botbuilder-core/tests/simple_adapter.py
new file mode 100644
index 00000000..853bb69e
--- /dev/null
+++ b/libraries/botbuilder-core/tests/simple_adapter.py
@@ -0,0 +1,43 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import unittest
+from typing import Callable, List
+from botbuilder.core import BotAdapter, TurnContext
+from botbuilder.schema import Activity, ConversationReference, ResourceResponse
+
+class SimpleAdapter(BotAdapter):
+        
+    def __init__(self, call_on_send = None, call_on_update = None, call_on_delete = None):
+        super(SimpleAdapter, self).__init__()
+        self.test_aux = unittest.TestCase('__init__')
+        self._call_on_send = call_on_send
+        self._call_on_update = call_on_update
+        self._call_on_delete = call_on_delete
+        
+    
+    async def delete_activity(self, context: TurnContext, reference: ConversationReference):
+        self.test_aux.assertIsNotNone(reference, "SimpleAdapter.delete_activity: missing reference")
+        if self._call_on_delete is not None: self._call_on_delete(reference)
+    
+    async def send_activities(self, context: TurnContext, activities: List[Activity]):
+        self.test_aux.assertIsNotNone(activities, "SimpleAdapter.delete_activity: missing reference")
+        self.test_aux.assertTrue(len(activities) > 0, "SimpleAdapter.send_activities: empty activities array.")
+
+        if self._call_on_send is not None: self._call_on_send(activities)
+        responses = []
+
+        for activity in activities:
+            responses.append(ResourceResponse(id=activity.id))
+        
+        return responses
+    
+    async def update_activity(self, context: TurnContext, activity: Activity):
+        self.test_aux.assertIsNotNone(activity, "SimpleAdapter.update_activity: missing activity")
+        if self._call_on_update is not None: self._call_on_update(activity)
+
+        return ResourceResponse(activity.id)
+    
+    async def process_request(self, activity, handler):
+        context = TurnContext(self, activity)
+        return self.run_pipeline(context, handler)
\ No newline at end of file
diff --git a/libraries/botbuilder-core/tests/test_bot_adapter.py b/libraries/botbuilder-core/tests/test_bot_adapter.py
new file mode 100644
index 00000000..ee5069bc
--- /dev/null
+++ b/libraries/botbuilder-core/tests/test_bot_adapter.py
@@ -0,0 +1,84 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+import aiounittest
+import uuid
+from typing import List
+
+from botbuilder.core import BotAdapter, TurnContext
+from botbuilder.core.adapters import TestAdapter
+from botbuilder.schema import Activity, ConversationAccount, ConversationReference, ChannelAccount
+
+from simple_adapter import SimpleAdapter
+from call_counting_middleware import CallCountingMiddleware
+from test_message import TestMessage
+
+class TestBotAdapter(aiounittest.AsyncTestCase):
+    
+    def test_adapter_single_use(self):
+        adapter = SimpleAdapter()
+        adapter.use(CallCountingMiddleware())
+    
+    def test_adapter_use_chaining(self):
+        adapter = SimpleAdapter()
+        adapter.use(CallCountingMiddleware()).use(CallCountingMiddleware())
+    
+    async def test_pass_resource_responses_through(self):
+        def validate_responses(activities: List[Activity]):
+            pass # no need to do anything.
+
+        adapter = SimpleAdapter(call_on_send= validate_responses)
+        context = TurnContext(adapter, Activity())
+
+        activity_id = str(uuid.uuid1())
+        activity = TestMessage.message(activity_id)
+
+        resource_response = await context.send_activity(activity)
+        self.assertTrue(resource_response.id == activity_id, "Incorrect response Id returned")
+    
+    async def test_continue_conversation_direct_msg(self):
+        callback_invoked = False
+        adapter = TestAdapter()
+        reference = ConversationReference(
+            activity_id = "activityId",
+            bot = ChannelAccount(
+                id = "channelId",
+                name = "testChannelAccount",
+                role = "bot",
+            ),
+            channel_id = "testChannel",
+            service_url = "testUrl",
+            conversation = ConversationAccount(
+                conversation_type = "",
+                id = "testConversationId",
+                is_group = False,
+                name = "testConversationName",
+                role = "user",
+            ),
+            user = ChannelAccount(
+                id = "channelId",
+                name = "testChannelAccount",
+                role = "bot",
+            )
+        )
+
+        async def continue_callback(turn_context):
+            nonlocal callback_invoked
+            callback_invoked = True
+
+        await adapter.continue_conversation("MyBot", reference, continue_callback)
+        self.assertTrue(callback_invoked)
+    
+    async def test_turn_error(self):
+        async def on_error(turn_context: TurnContext, err: Exception):
+            nonlocal self
+            self.assertIsNotNone(turn_context, "turn_context not found.")
+            self.assertIsNotNone(err, "error not found.")
+            self.assertEqual(err.__class__, Exception, "unexpected error thrown.")
+
+        adapter = SimpleAdapter()
+        adapter.on_turn_error = on_error
+
+        def handler(context: TurnContext):
+            raise Exception
+        await adapter.process_request(TestMessage.message(), handler)
diff --git a/libraries/botbuilder-core/tests/test_message.py b/libraries/botbuilder-core/tests/test_message.py
new file mode 100644
index 00000000..38f650c8
--- /dev/null
+++ b/libraries/botbuilder-core/tests/test_message.py
@@ -0,0 +1,28 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from botbuilder.schema import Activity, ActivityTypes, ChannelAccount, ConversationAccount
+
+class TestMessage:
+
+    @staticmethod
+    def message(id: str= '1234') -> Activity:
+        return Activity(
+            type= ActivityTypes.message,
+            id= id,
+            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'
+        )
\ No newline at end of file