From 4a485b4bc40a4a824eb1fe7162d9d468e43162c2 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 29 Sep 2026 14:37:10 -0700 Subject: [PATCH 1/6] Adding msteams to dev install script --- .../microsoft_agents/hosting/msteams/_teams_api_client.py | 7 +++++++ libraries/microsoft-agents-hosting-msteams/setup.py | 2 +- scripts/dev_setup.ps1 | 1 + scripts/dev_setup.sh | 1 + 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py index dbbd0c7c8..071a108f4 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py @@ -8,6 +8,8 @@ available. """ +from httpx import AsyncClient + from microsoft_teams.common import ClientOptions from microsoft_teams.api import ApiClient @@ -16,6 +18,11 @@ TurnContext, ) +def _create_httpx_client() -> AsyncClient: + return AsyncClient( + verify= + ) + def _get_teams_api_client(context: TurnContext) -> ApiClient: """ diff --git a/libraries/microsoft-agents-hosting-msteams/setup.py b/libraries/microsoft-agents-hosting-msteams/setup.py index e85687671..c3cb4c4fa 100644 --- a/libraries/microsoft-agents-hosting-msteams/setup.py +++ b/libraries/microsoft-agents-hosting-msteams/setup.py @@ -14,7 +14,7 @@ install_requires=[ f"microsoft-agents-hosting-core=={package_version}", "aiohttp>=3.11.11", - "microsoft-teams-api==2.0.16", + "microsoft-teams-api==2.1.0", "msgraph-sdk>=1.58.0", ], ) diff --git a/scripts/dev_setup.ps1 b/scripts/dev_setup.ps1 index 2b3f8d35d..5d473ec3c 100644 --- a/scripts/dev_setup.ps1 +++ b/scripts/dev_setup.ps1 @@ -9,6 +9,7 @@ pip install -e ./libraries/microsoft-agents-copilotstudio-client/ --config-setti pip install -e ./libraries/microsoft-agents-hosting-aiohttp/ --config-settings editable_mode=compat pip install -e ./libraries/microsoft-agents-hosting-core/ --config-settings editable_mode=compat pip install -e ./libraries/microsoft-agents-hosting-teams/ --config-settings editable_mode=compat +pip install -e ./libraries/microsoft-agents-hosting-msteams/ --config-settings editable_mode=compat pip install -e ./libraries/microsoft-agents-hosting-dialogs/ --config-settings editable_mode=compat pip install -e ./libraries/microsoft-agents-storage-blob/ --config-settings editable_mode=compat pip install -e ./libraries/microsoft-agents-storage-cosmos/ --config-settings editable_mode=compat diff --git a/scripts/dev_setup.sh b/scripts/dev_setup.sh index b013fced4..eeb075f84 100644 --- a/scripts/dev_setup.sh +++ b/scripts/dev_setup.sh @@ -9,6 +9,7 @@ pip install -e ./libraries/microsoft-agents-copilotstudio-client/ --config-setti pip install -e ./libraries/microsoft-agents-hosting-aiohttp/ --config-settings editable_mode=compat pip install -e ./libraries/microsoft-agents-hosting-core/ --config-settings editable_mode=compat pip install -e ./libraries/microsoft-agents-hosting-teams/ --config-settings editable_mode=compat +pip install -e ./libraries/microsoft-agents-hosting-msteams/ --config-settings editable_mode=compat pip install -e ./libraries/microsoft-agents-hosting-dialogs/ --config-settings editable_mode=compat pip install -e ./libraries/microsoft-agents-storage-blob/ --config-settings editable_mode=compat pip install -e ./libraries/microsoft-agents-storage-cosmos/ --config-settings editable_mode=compat From 8c44698a52e835717eb8e4611518993cea7de073 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 29 Sep 2026 15:26:18 -0700 Subject: [PATCH 2/6] SSL context caching --- .../hosting/msteams/_teams_api_client.py | 74 +++++++++++++++---- .../hosting/msteams/teams_agent_extension.py | 14 ++++ tests/hosting_msteams/helpers.py | 3 +- 3 files changed, 77 insertions(+), 14 deletions(-) diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py index 071a108f4..b9d7b151c 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py @@ -8,9 +8,14 @@ available. """ -from httpx import AsyncClient +import ssl +import certifi -from microsoft_teams.common import ClientOptions +from typing import Callable, Awaitable + +import httpx + +from microsoft_teams.common import Client, ClientOptions from microsoft_teams.api import ApiClient from microsoft_agents.hosting.core import ( @@ -18,9 +23,52 @@ TurnContext, ) -def _create_httpx_client() -> AsyncClient: - return AsyncClient( - verify= +_ssl_context: ssl.SSLContext | None = None + + +def _get_ssl_context() -> ssl.SSLContext: + """Get or create the SSL context for verifying HTTPS requests. + + httpx by default creates a new SSL context for each new client instance. This is can be + inefficient. For example, at the time of writing this, without caching the SSL context, + the hosting_msteams unit tests took 32 seconds to complete. With caching, the total + time for the unit tests dropped to 7 seconds. For an agent with lots of traffic, + caching the SSL context may significantly improve performance and avoid extra io operations. + """ + global _ssl_context + + if _ssl_context is None: + _ssl_context = ssl.create_default_context(cafile=certifi.where()) + return _ssl_context + + +def _client( + base_url: str, + headers: dict, + token_factory: Callable[[], Awaitable[str]] | None = None, +) -> Client: + """ + Create a new Client instance configured with the given base URL, headers, and token factory. + + :param base_url: The base URL for the client. + :param headers: The headers to include in the client requests. + :param token_factory: A callable that returns an access token asynchronously, or None if no token is required. + :return: A configured Client instance. + """ + + options = ClientOptions( + base_url=base_url, + headers=headers, + token=token_factory, + ) + + return Client( + options, + _http=httpx.AsyncClient( + base_url=base_url, + headers=headers, + verify=_get_ssl_context(), + ), ) @@ -56,28 +104,28 @@ def _set_teams_api_client( "Content-Type": "application/json", } - options: ClientOptions + token_factory: Callable[[], Awaitable[str]] | None = None if context.identity: provider = connection_manager.get_token_provider( context.identity, context.activity.service_url ) - async def token_factory() -> str: + async def _token_factory() -> str: return await provider.get_access_token( "https://api.botframework.com", ["https://api.botframework.com/.default"], ) - options = ClientOptions( - base_url=context.activity.service_url, headers=headers, token=token_factory - ) - else: - options = ClientOptions(base_url=context.activity.service_url, headers=headers) + token_factory = _token_factory api_client = ApiClient( context.activity.service_url, - options, + options=_client( + base_url=context.activity.service_url, + headers=headers, + token_factory=token_factory, + ), ) context.services.set(ApiClient, api_client) diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py index 0b6cfd0e5..66fe0c36d 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py @@ -130,7 +130,21 @@ async def on_before_turn(context: TurnContext, state: StateT) -> bool: context.activity.channel_data = _try_get_channel_data(context.activity) return True + # this is a placeholder until we figure out a better way of handling the + # lifecycle of the turn. This ensures that any resources associated with the Teams API client are properly cleaned up after the turn. + async def on_after_turn(context: TurnContext, state: StateT) -> bool: + if context.activity.channel_id == Channels.ms_teams: + try: + api_client = context.services.get(ApiClient) + if api_client is not None: + await api_client.http.http.aclose() + except ValueError: + # Handle the case where the Teams API client is not found + pass + return True + self._app.before_turn(on_before_turn) + self._app.after_turn(on_after_turn) @property def channels(self) -> Channel[StateT]: diff --git a/tests/hosting_msteams/helpers.py b/tests/hosting_msteams/helpers.py index 9bcd83c49..3708d08c0 100644 --- a/tests/hosting_msteams/helpers.py +++ b/tests/hosting_msteams/helpers.py @@ -92,7 +92,8 @@ def _make_context( def _cache_teams_api_client(context: TurnContext) -> None: - context.services.set(ApiClient, object.__new__(ApiClient)) + pass + # context.services.set(ApiClient, object.__new__(ApiClient)) def _make_teams_context() -> "TeamsTurnContext": From 0ef3da92e122384aaf0d0c37ced8bd5f8c125824 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Tue, 29 Sep 2026 15:38:33 -0700 Subject: [PATCH 3/6] Client cleanup logic --- changelog.md | 5 +++++ .../hosting/msteams/teams_agent_extension.py | 10 +++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/changelog.md b/changelog.md index 71765f086..59b76fbcc 100644 --- a/changelog.md +++ b/changelog.md @@ -3,6 +3,11 @@ **Release Date:** Unreleased **Previous Version:** 1.7.0 (Released 2026-09-17) +## Major Features & Enhancements + +- **Teams API Client Performance**: Reused a cached SSL context when constructing per-turn Teams API clients, reducing repeated CA certificate loading and client initialization overhead +- **Teams API Dependency**: Updated `microsoft-agents-hosting-msteams` to use `microsoft-teams-api` 2.1.0 + ## Bug Fixes - **Client Citation Deserialization**: Fixed deserialization of citations containing `@id` values diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py index 66fe0c36d..c0ec22f75 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py @@ -134,13 +134,9 @@ async def on_before_turn(context: TurnContext, state: StateT) -> bool: # lifecycle of the turn. This ensures that any resources associated with the Teams API client are properly cleaned up after the turn. async def on_after_turn(context: TurnContext, state: StateT) -> bool: if context.activity.channel_id == Channels.ms_teams: - try: - api_client = context.services.get(ApiClient) - if api_client is not None: - await api_client.http.http.aclose() - except ValueError: - # Handle the case where the Teams API client is not found - pass + api_client = context.services.get(ApiClient) + if api_client is not None: + await api_client.http.http.aclose() return True self._app.before_turn(on_before_turn) From 9783031a9170815dda21c59bf575a0ea0298331d Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 30 Sep 2026 15:14:14 -0700 Subject: [PATCH 4/6] Typo in docstring --- .../microsoft_agents/hosting/msteams/_teams_api_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py index 40590987a..86b2b3147 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py @@ -30,7 +30,7 @@ def _get_ssl_context() -> ssl.SSLContext: """Get or create the SSL context for verifying HTTPS requests. - httpx by default creates a new SSL context for each new client instance. This is can be + httpx by default creates a new SSL context for each new client instance. This can be inefficient. For example, at the time of writing this, without caching the SSL context, the hosting_msteams unit tests took 32 seconds to complete. With caching, the total time for the unit tests dropped to 7 seconds. For an agent with lots of traffic, From 375f4d2d3fce1caa9dc34beb250e9fc25c75a893 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 30 Sep 2026 15:37:28 -0700 Subject: [PATCH 5/6] Addressing PR feedback --- .../hosting/core/channel_service_adapter.py | 32 +++++++++++-------- .../hosting/msteams/_teams_api_client.py | 1 - .../hosting/msteams/teams_agent_extension.py | 3 +- .../test_teams_agent_extension.py | 7 ++++ 4 files changed, 28 insertions(+), 15 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index cd57a5c82..2bd5ea02f 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -309,10 +309,12 @@ async def create_conversation( # pylint: disable=arguments-differ ) # Run the pipeline - await self.run_pipeline(context, callback) - - await connector_client.close() - await user_token_client.close() + try: + await self.run_pipeline(context, callback) + finally: + await connector_client.close() + await user_token_client.close() + await context._aclose() async def process_proactive( self, @@ -357,10 +359,12 @@ async def process_proactive( ) # Run the pipeline - await self.run_pipeline(context, callback) - - await connector_client.close() - await user_token_client.close() + try: + await self.run_pipeline(context, callback) + finally: + await connector_client.close() + await user_token_client.close() + await context._aclose() def _resolve_if_connector_client_is_needed(self, activity: Activity) -> bool: """Determine if a connector client is needed based on the activity's delivery mode and service URL. @@ -449,11 +453,13 @@ async def process_activity( connector_client # for back-compat ) - await self.run_pipeline(context, callback) - - if connector_client: - await connector_client.close() - await user_token_client.close() + try: + await self.run_pipeline(context, callback) + finally: + if connector_client: + await connector_client.close() + await user_token_client.close() + await context._aclose() # If there are any results they will have been left on the TurnContext. return self._process_turn_results(context) diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py index 86b2b3147..ea6cdc115 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/_teams_api_client.py @@ -9,7 +9,6 @@ """ import ssl -from time import time import certifi from typing import Callable, Awaitable diff --git a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py index cccc0bdbe..8d48690ce 100644 --- a/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py +++ b/libraries/microsoft-agents-hosting-msteams/microsoft_agents/hosting/msteams/teams_agent_extension.py @@ -132,9 +132,10 @@ async def on_close_turn(context: TurnContext) -> None: async def on_before_turn(context: TurnContext, state: StateT) -> bool: if context.activity.channel_id == Channels.ms_teams: _set_teams_api_client(context, self._app.connection_manager) + context._on_aclose(on_close_turn) + # caches the deserialized version of ChannelData context.activity.channel_data = _try_get_channel_data(context.activity) - context._on_aclose(on_close_turn) return True self._app.before_turn(on_before_turn) diff --git a/tests/hosting_msteams/test_teams_agent_extension.py b/tests/hosting_msteams/test_teams_agent_extension.py index c8da5a822..3fa20e616 100644 --- a/tests/hosting_msteams/test_teams_agent_extension.py +++ b/tests/hosting_msteams/test_teams_agent_extension.py @@ -46,6 +46,10 @@ def __init__(self, activity, identity=None): self.activity = activity self.identity = identity self.services = _FakeServiceSet() + self.on_aclose_handlers = [] + + def _on_aclose(self, handler): + self.on_aclose_handlers.append(handler) class TestTeamsAgentExtensionProperties: @@ -109,6 +113,7 @@ async def test_non_teams_channel_is_untouched(self): # channel_data left as the raw dict; no Teams API client cached assert activity.channel_data == {"channel": {"id": "c"}} assert not ctx.services.has(ApiClient) + assert ctx.on_aclose_handlers == [] @pytest.mark.asyncio async def test_teams_channel_deserializes_channel_data(self): @@ -126,6 +131,7 @@ async def test_teams_channel_deserializes_channel_data(self): assert isinstance(activity.channel_data, ChannelData) assert activity.channel_data.channel.id == "c1" assert ctx.services.has(ApiClient) + assert len(ctx.on_aclose_handlers) == 1 @pytest.mark.asyncio async def test_teams_channel_without_channel_data_sets_none(self): @@ -140,3 +146,4 @@ async def test_teams_channel_without_channel_data_sets_none(self): assert result is True assert activity.channel_data is None + assert len(ctx.on_aclose_handlers) == 1 From 5d61f7c6cb48f09f80eb37a29364829eed4dae25 Mon Sep 17 00:00:00 2001 From: Rodrigo Brandao Date: Wed, 30 Sep 2026 15:51:29 -0700 Subject: [PATCH 6/6] Simplifying/improving cleanup logic with AsyncExitStack --- .../hosting/core/channel_service_adapter.py | 34 +++++++++---------- .../test_channel_service_adapter.py | 32 +++++++++++++++++ .../test_teams_agent_extension.py | 23 +++++++++++-- 3 files changed, 69 insertions(+), 20 deletions(-) diff --git a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py index 2bd5ea02f..4e11c86ec 100644 --- a/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py +++ b/libraries/microsoft-agents-hosting-core/microsoft_agents/hosting/core/channel_service_adapter.py @@ -4,6 +4,7 @@ from __future__ import annotations from abc import ABC +from contextlib import AsyncExitStack from http import HTTPStatus from typing import Awaitable, Callable, Optional, cast from uuid import uuid4 @@ -308,13 +309,12 @@ async def create_conversation( # pylint: disable=arguments-differ user_token_client # for back-compat ) - # Run the pipeline - try: + # Run the pipeline and release resources in reverse registration order. + async with AsyncExitStack() as cleanup: + cleanup.push_async_callback(context._aclose) + cleanup.push_async_callback(user_token_client.close) + cleanup.push_async_callback(connector_client.close) await self.run_pipeline(context, callback) - finally: - await connector_client.close() - await user_token_client.close() - await context._aclose() async def process_proactive( self, @@ -358,13 +358,12 @@ async def process_proactive( connector_client # for back-compat ) - # Run the pipeline - try: + # Run the pipeline and release resources in reverse registration order. + async with AsyncExitStack() as cleanup: + cleanup.push_async_callback(context._aclose) + cleanup.push_async_callback(user_token_client.close) + cleanup.push_async_callback(connector_client.close) await self.run_pipeline(context, callback) - finally: - await connector_client.close() - await user_token_client.close() - await context._aclose() def _resolve_if_connector_client_is_needed(self, activity: Activity) -> bool: """Determine if a connector client is needed based on the activity's delivery mode and service URL. @@ -453,13 +452,12 @@ async def process_activity( connector_client # for back-compat ) - try: - await self.run_pipeline(context, callback) - finally: + async with AsyncExitStack() as cleanup: + cleanup.push_async_callback(context._aclose) + cleanup.push_async_callback(user_token_client.close) if connector_client: - await connector_client.close() - await user_token_client.close() - await context._aclose() + cleanup.push_async_callback(connector_client.close) + await self.run_pipeline(context, callback) # If there are any results they will have been left on the TurnContext. return self._process_turn_results(context) diff --git a/tests/hosting_core/test_channel_service_adapter.py b/tests/hosting_core/test_channel_service_adapter.py index 4dd678f6f..85097efb4 100644 --- a/tests/hosting_core/test_channel_service_adapter.py +++ b/tests/hosting_core/test_channel_service_adapter.py @@ -263,6 +263,38 @@ async def callback(context: TurnContext): assert context_arg.services.get(UserTokenClientBase) is user_token_client assert context_arg.services.get(ConnectorClientBase) is connector_client + @pytest.mark.asyncio + async def test_process_proactive_attempts_all_cleanup_when_connector_close_fails( + self, mocker, user_token_client, connector_client, adapter + ): + connector_error = RuntimeError("connector close failed") + connector_client.close = mocker.AsyncMock(side_effect=connector_error) + user_token_client.close = mocker.AsyncMock() + context_cleanup = mocker.AsyncMock() + + async def run_pipeline(context, callback): + context._on_aclose(context_cleanup) + + adapter.run_pipeline = mocker.AsyncMock(side_effect=run_pipeline) + activity = Activity( + type="message", + conversation={"id": "conversation123"}, + channel_id="channel_id", + service_url="service_url", + ) + + with pytest.raises(RuntimeError, match="connector close failed"): + await adapter.process_proactive( + ClaimsIdentity(), + activity, + "audience", + mocker.AsyncMock(), + ) + + connector_client.close.assert_awaited_once_with() + user_token_client.close.assert_awaited_once_with() + context_cleanup.assert_awaited_once() + @pytest.mark.asyncio async def test_process_proactive_uses_anonymous_clients(self, mocker): factory = mocker.Mock(spec=ChannelServiceClientFactoryBase) diff --git a/tests/hosting_msteams/test_teams_agent_extension.py b/tests/hosting_msteams/test_teams_agent_extension.py index 3fa20e616..df47a80d3 100644 --- a/tests/hosting_msteams/test_teams_agent_extension.py +++ b/tests/hosting_msteams/test_teams_agent_extension.py @@ -35,6 +35,9 @@ def __init__(self): def has(self, key): return key in self._state + def get(self, key): + return self._state.get(key) + def set(self, key, value): self._state[key] = value @@ -51,6 +54,10 @@ def __init__(self, activity, identity=None): def _on_aclose(self, handler): self.on_aclose_handlers.append(handler) + async def _aclose(self): + for handler in self.on_aclose_handlers: + await handler(self) + class TestTeamsAgentExtensionProperties: @@ -116,7 +123,7 @@ async def test_non_teams_channel_is_untouched(self): assert ctx.on_aclose_handlers == [] @pytest.mark.asyncio - async def test_teams_channel_deserializes_channel_data(self): + async def test_teams_channel_deserializes_channel_data(self, mocker): activity = Activity( type="conversationUpdate", channel_id=Channels.ms_teams, @@ -132,9 +139,15 @@ async def test_teams_channel_deserializes_channel_data(self): assert activity.channel_data.channel.id == "c1" assert ctx.services.has(ApiClient) assert len(ctx.on_aclose_handlers) == 1 + async_client = ctx.services.get(ApiClient).http.http + close_spy = mocker.spy(async_client, "aclose") + + await ctx._aclose() + + close_spy.assert_awaited_once_with() @pytest.mark.asyncio - async def test_teams_channel_without_channel_data_sets_none(self): + async def test_teams_channel_without_channel_data_sets_none(self, mocker): activity = Activity( type="conversationUpdate", channel_id=Channels.ms_teams, @@ -147,3 +160,9 @@ async def test_teams_channel_without_channel_data_sets_none(self): assert result is True assert activity.channel_data is None assert len(ctx.on_aclose_handlers) == 1 + async_client = ctx.services.get(ApiClient).http.http + close_spy = mocker.spy(async_client, "aclose") + + await ctx._aclose() + + close_spy.assert_awaited_once_with()