Skip to content
This repository was archived by the owner on Jan 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions libraries/botbuilder-core/botbuilder/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from .bot_telemetry_client import BotTelemetryClient
from .card_factory import CardFactory
from .conversation_state import ConversationState
from .invoke_response import InvokeResponse
from .memory_storage import MemoryStorage
from .message_factory import MessageFactory
from .middleware_set import AnonymousReceiveMiddleware, Middleware, MiddlewareSet
Expand All @@ -24,6 +25,7 @@
from .storage import Storage, StoreItem, calculate_change_hash
from .turn_context import TurnContext
from .user_state import UserState
from .user_token_provider import UserTokenProvider

__all__ = ['ActivityHandler',
'AnonymousReceiveMiddleware',
Expand All @@ -37,6 +39,7 @@
'CardFactory',
'ConversationState',
'conversation_reference_extension',
'InvokeResponse',
'MemoryStorage',
'MessageFactory',
'Middleware',
Expand All @@ -48,4 +51,5 @@
'StoreItem',
'TurnContext',
'UserState',
'UserTokenProvider',
'__version__']
126 changes: 114 additions & 12 deletions libraries/botbuilder-core/botbuilder/core/adapters/test_adapter.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,46 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.

# TODO: enable this in the future
# With python 3.7 the line below will allow to do Postponed Evaluation of Annotations. See PEP 563
# from __future__ import annotations

import asyncio
import inspect
from datetime import datetime
from typing import Coroutine, List, Callable
from typing import Coroutine, Dict, List, Callable, Union
from copy import copy
from ..bot_adapter import BotAdapter
from ..turn_context import TurnContext
from ..turn_context import TurnContext
from ..user_token_provider import UserTokenProvider
from botbuilder.schema import (ActivityTypes, Activity, ConversationAccount,
ConversationReference, ChannelAccount, ResourceResponse)
ConversationReference, ChannelAccount, ResourceResponse,
TokenResponse)


class UserToken:
def __init__(self, connection_name: str = None, user_id: str = None, channel_id: str = None, token: str = None):
self.connection_name = connection_name
self.user_id = user_id
self.channel_id = channel_id
self.token = token

def equals_key(self, rhs: 'UserToken'):
return (rhs is not None and
self.connection_name == rhs.connection_name and
self.user_id == rhs.user_id and
self.channel_id == rhs.channel_id)


class TestAdapter(BotAdapter):
def __init__(self, logic: Coroutine=None, conversation: ConversationReference=None, send_trace_activity: bool = False):
class TokenMagicCode:
def __init__(self, key: UserToken = None, magic_code: str = None):
self.key = key
self.magic_code = magic_code


class TestAdapter(BotAdapter, UserTokenProvider):
def __init__(self, logic: Coroutine = None, conversation: ConversationReference = None,
send_trace_activity: bool = False):
"""
Creates a new TestAdapter instance.
:param logic:
Expand All @@ -22,6 +49,8 @@ def __init__(self, logic: Coroutine=None, conversation: ConversationReference=No
super(TestAdapter, self).__init__()
self.logic = logic
self._next_id: int = 0
self._user_tokens: List[UserToken] = []
self._magic_codes: List[TokenMagicCode] = []
self.activity_buffer: List[Activity] = []
self.updated_activities: List[Activity] = []
self.deleted_activities: List[ConversationReference] = []
Expand All @@ -46,10 +75,12 @@ async def send_activities(self, context, activities: List[Activity]):
:param activities:
:return:
"""

def id_mapper(activity):
self.activity_buffer.append(activity)
self._next_id += 1
return ResourceResponse(id=str(self._next_id))

"""This if-else code is temporary until the BotAdapter and Bot/TurnContext are revamped."""
if type(activities) == list:
responses = [id_mapper(activity) for activity in activities]
Expand Down Expand Up @@ -96,7 +127,7 @@ async def receive_activity(self, activity):
"""
if type(activity) == str:
activity = Activity(type='message', text=activity)
# Initialize request
# Initialize request.
request = copy(self.template)

for key, value in vars(activity).items():
Expand All @@ -108,7 +139,7 @@ async def receive_activity(self, activity):
self._next_id += 1
request.id = str(self._next_id)

# Create context object and run middleware
# Create context object and run middleware.
context = TurnContext(self, request)
return await self.run_pipeline(context, self.logic)

Expand Down Expand Up @@ -152,9 +183,71 @@ async def tests(self, *args):
timeout = arg[3]
await self.test(arg[0], arg[1], description, timeout)

def add_user_token(self, connection_name: str, channel_id: str, user_id: str, token: str, magic_code: str = None):
key = UserToken()
key.channel_id = channel_id
key.connection_name = connection_name
key.user_id = user_id
key.token = token

if not magic_code:
self._user_tokens.append(key)
else:
mc = TokenMagicCode()
mc.key = key
mc.magic_code = magic_code
self._magic_codes.append(mc)

async def get_user_token(self, context: TurnContext, connection_name: str, magic_code: str = None) -> TokenResponse:
key = UserToken()
key.channel_id = context.activity.channel_id
key.connection_name = connection_name
key.user_id = context.activity.from_property.id

if magic_code:
magic_code_record = list(filter(lambda x: key.equals_key(x.key), self._magic_codes))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider a list comprehension?

if magic_code_record and len(magic_code_record) > 0 and magic_code_record[0].magic_code == magic_code:
# Move the token to long term dictionary.
self.add_user_token(connection_name, key.channel_id, key.user_id, magic_code_record[0].key.token)

# Remove from the magic code list.
idx = self._magic_codes.index(magic_code_record[0])
self._magic_codes = [self._magic_codes.pop(idx)]

match = list(filter(lambda x: key.equals_key(x), self._user_tokens))

if match and len(match) > 0:
return TokenResponse(
connection_name=match[0].connection_name,
token=match[0].token,
expiration=None
)
else:
# Not found.
return None

async def sign_out_user(self, context: TurnContext, connection_name: str):
channel_id = context.activity.channel_id
user_id = context.activity.from_property.id

new_records = []
for token in self._user_tokens:
if (token.channel_id != channel_id or
token.user_id != user_id or
(connection_name and connection_name != token.connection_name)):
new_records.append(token)
self._user_tokens = new_records

async def get_oauth_sign_in_link(self, context: TurnContext, connection_name: str) -> str:
return f'https://fake.com/oauthsignin/{connection_name}/{context.activity.channel_id}/{context.activity.from_property.id}'

async def get_aad_tokens(self, context: TurnContext, connection_name: str, resource_urls: List[str]) -> Dict[
str, TokenResponse]:
return None


class TestFlow(object):
def __init__(self, previous, adapter: TestAdapter):
def __init__(self, previous: Callable, adapter: TestAdapter):
"""
INTERNAL: creates a new TestFlow instance.
:param previous:
Expand Down Expand Up @@ -183,6 +276,7 @@ async def send(self, user_says) -> 'TestFlow':
:param user_says:
:return:
"""

async def new_previous():
nonlocal self, user_says
if callable(self.previous):
Expand All @@ -191,14 +285,16 @@ async def new_previous():

return TestFlow(await new_previous(), self.adapter)

async def assert_reply(self, expected, description=None, timeout=None) -> 'TestFlow':
async def assert_reply(self, expected: Union[str, Activity, Callable[[Activity, str], None]], description=None,
timeout=None) -> 'TestFlow':
"""
Generates an assertion if the bots response doesn't match the expected text/activity.
:param expected:
:param description:
:param timeout:
:return:
"""

def default_inspector(reply, description=None):
if isinstance(expected, Activity):
validate_activity(reply, expected)
Expand All @@ -209,7 +305,7 @@ def default_inspector(reply, description=None):
if description is None:
description = ''

inspector = expected if type(expected) == 'function' else default_inspector
inspector = expected if callable(expected) else default_inspector

async def test_flow_previous():
nonlocal timeout
Expand All @@ -222,7 +318,7 @@ async def wait_for_activity():
nonlocal expected, timeout
current = datetime.now()
if (current - start).total_seconds() * 1000 > timeout:
if type(expected) == Activity:
if isinstance(expected, Activity):
expecting = expected.text
elif callable(expected):
expecting = inspect.getsourcefile(expected)
Expand All @@ -232,11 +328,17 @@ async def wait_for_activity():
f'{current - start}ms.')
elif len(adapter.activity_buffer) > 0:
reply = adapter.activity_buffer.pop(0)
inspector(reply, description)
try:
await inspector(reply, description)
except Exception:
inspector(reply, description)

else:
await asyncio.sleep(0.05)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sketchy?

await wait_for_activity()

await wait_for_activity()

return TestFlow(await test_flow_previous(), self.adapter)


Expand Down
Loading