From df6b25c74a904ea8c985a749b93ea90fcfb31f8b Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 3 Mar 2025 12:14:22 -0800 Subject: [PATCH 1/2] ActivityHandler unit tests --- .../agents/botbuilder/activity_handler.py | 28 ++-- .../tests/test_activity_handler.py | 157 ++++++++++++++++++ .../agents/core/models/agents_model.py | 6 +- .../microsoft/agents/core/models/entity.py | 5 +- 4 files changed, 175 insertions(+), 21 deletions(-) create mode 100644 libraries/Botbuilder/microsoft-agents-botbuilder/tests/test_activity_handler.py diff --git a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/activity_handler.py b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/activity_handler.py index 1410329c..b28053a8 100644 --- a/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/activity_handler.py +++ b/libraries/Botbuilder/microsoft-agents-botbuilder/microsoft/agents/botbuilder/activity_handler.py @@ -159,21 +159,15 @@ async def on_conversation_update_activity(self, turn_context: TurnContextProtoco - In a derived class, override this method to add logic that applies to all conversation update activities. Add logic to apply before the member added or removed logic before the call to this base class method. """ - if ( - turn_context.activity.members_added is not None - and turn_context.activity.members_added - ): - return await self.on_members_added_activity( + # TODO: confirm behavior of added and removed at the same time as C# doesn't support it + if turn_context.activity.members_added: + await self.on_members_added_activity( turn_context.activity.members_added, turn_context ) - if ( - turn_context.activity.members_removed is not None - and turn_context.activity.members_removed - ): - return await self.on_members_removed_activity( + if turn_context.activity.members_removed: + await self.on_members_removed_activity( turn_context.activity.members_removed, turn_context ) - return async def on_members_added_activity( self, members_added: list[ChannelAccount], turn_context: TurnContextProtocol @@ -534,9 +528,15 @@ async def on_adaptive_card_invoke( @staticmethod def _create_invoke_response(body: BaseModel = None) -> InvokeResponse: + serialized_body = ( + body.model_dump(mode="json", by_alias=True, exclude_none=True) + if body + else None + ) + return InvokeResponse( status=int(HTTPStatus.OK), - body=body.model_dump(mode="json", by_alias=True, exclude_none=True), + body=serialized_body, ) def _get_adaptive_card_invoke_value(self, activity: Activity): @@ -563,11 +563,11 @@ def _get_adaptive_card_invoke_value(self, activity: Activity): ) raise _InvokeResponseException(HTTPStatus.BAD_REQUEST, response) - if invoke_value.action.get("type") != "Action.Execute": + if invoke_value.action.type != "Action.Execute": response = self._create_adaptive_card_invoke_error_response( HTTPStatus.BAD_REQUEST, "NotSupported", - f"The action '{invoke_value.action.get('type')}' is not supported.", + f"The action '{invoke_value.action.type}' is not supported.", ) raise _InvokeResponseException(HTTPStatus.BAD_REQUEST, response) diff --git a/libraries/Botbuilder/microsoft-agents-botbuilder/tests/test_activity_handler.py b/libraries/Botbuilder/microsoft-agents-botbuilder/tests/test_activity_handler.py new file mode 100644 index 00000000..7227675d --- /dev/null +++ b/libraries/Botbuilder/microsoft-agents-botbuilder/tests/test_activity_handler.py @@ -0,0 +1,157 @@ +import pytest +from unittest.mock import AsyncMock +from microsoft.agents.botbuilder import ActivityHandler, TurnContext +from microsoft.agents.core.models import ( + Activity, + ActivityTypes, + ChannelAccount, + MessageReaction, + SignInConstants, + InvokeResponse, + AdaptiveCardInvokeValue, +) + + +class TestActivityHandler: + @pytest.fixture + def handler(self): + return ActivityHandler() + + @pytest.fixture + def turn_context(self): + return AsyncMock(spec=TurnContext) + + @pytest.mark.asyncio + async def test_on_turn_message_activity( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.type = ActivityTypes.message + handler.on_message_activity = AsyncMock() + await handler.on_turn(turn_context) + handler.on_message_activity.assert_called_once_with(turn_context) + + @pytest.mark.asyncio + async def test_on_turn_conversation_update_activity( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.type = ActivityTypes.conversation_update + handler.on_conversation_update_activity = AsyncMock() + await handler.on_turn(turn_context) + handler.on_conversation_update_activity.assert_called_once_with(turn_context) + + @pytest.mark.asyncio + async def test_on_turn_event_activity( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.type = ActivityTypes.event + handler.on_event_activity = AsyncMock() + await handler.on_turn(turn_context) + handler.on_event_activity.assert_called_once_with(turn_context) + + @pytest.mark.asyncio + async def test_on_turn_unrecognized_activity_type( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.type = "unknown" + handler.on_unrecognized_activity_type = AsyncMock() + await handler.on_turn(turn_context) + handler.on_unrecognized_activity_type.assert_called_once_with(turn_context) + + @pytest.mark.asyncio + async def test_on_message_activity( + self, handler: ActivityHandler, turn_context: TurnContext + ): + await handler.on_message_activity(turn_context) + # No exception means the test passed + + @pytest.mark.asyncio + async def test_on_conversation_update_activity_members_added( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.members_added = [ChannelAccount(id="user1")] + handler.on_members_added_activity = AsyncMock() + await handler.on_conversation_update_activity(turn_context) + handler.on_members_added_activity.assert_called_once_with( + turn_context.activity.members_added, turn_context + ) + + @pytest.mark.asyncio + async def test_on_conversation_update_activity_members_removed( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.members_removed = [ChannelAccount(id="user1")] + handler.on_members_removed_activity = AsyncMock() + await handler.on_conversation_update_activity(turn_context) + handler.on_members_removed_activity.assert_called_once_with( + turn_context.activity.members_removed, turn_context + ) + + @pytest.mark.asyncio + async def test_on_message_reaction_activity_reactions_added( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.reactions_added = [MessageReaction(type="like")] + handler.on_reactions_added = AsyncMock() + await handler.on_message_reaction_activity(turn_context) + handler.on_reactions_added.assert_called_once_with( + turn_context.activity.reactions_added, turn_context + ) + + @pytest.mark.asyncio + async def test_on_message_reaction_activity_reactions_removed( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.reactions_removed = [MessageReaction(type="like")] + handler.on_reactions_removed = AsyncMock() + await handler.on_message_reaction_activity(turn_context) + handler.on_reactions_removed.assert_called_once_with( + turn_context.activity.reactions_removed, turn_context + ) + + @pytest.mark.asyncio + async def test_on_event_activity_token_response( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.name = SignInConstants.token_response_event_name + handler.on_token_response_event = AsyncMock() + await handler.on_event_activity(turn_context) + handler.on_token_response_event.assert_called_once_with(turn_context) + + @pytest.mark.asyncio + async def test_on_event_activity_other_event( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.name = "other_event" + handler.on_event = AsyncMock() + await handler.on_event_activity(turn_context) + handler.on_event.assert_called_once_with(turn_context) + + @pytest.mark.asyncio + async def test_on_invoke_activity_sign_in( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.name = SignInConstants.verify_state_operation_name + handler.on_sign_in_invoke = AsyncMock() + response = await handler.on_invoke_activity(turn_context) + handler.on_sign_in_invoke.assert_called_once_with(turn_context) + assert isinstance(response, InvokeResponse) + + @pytest.mark.asyncio + async def test_on_invoke_activity_adaptive_card( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.name = "adaptiveCard/action" + turn_context.activity.value = {"action": {"type": "Action.Execute"}} + handler.on_adaptive_card_invoke = AsyncMock(return_value=None) + response = await handler.on_invoke_activity(turn_context) + handler.on_adaptive_card_invoke.assert_called_once() + assert isinstance(response, InvokeResponse) + + @pytest.mark.asyncio + async def test_on_invoke_activity_not_implemented( + self, handler: ActivityHandler, turn_context: TurnContext + ): + turn_context.activity.name = "unknown" + response = await handler.on_invoke_activity(turn_context) + + assert response.status == 501 diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/agents_model.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/agents_model.py index e6d8bf4f..32ac17be 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/agents_model.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/agents_model.py @@ -1,11 +1,9 @@ -from pydantic import BaseModel +from pydantic import BaseModel, ConfigDict from pydantic.alias_generators import to_camel class AgentsModel(BaseModel): - class Config: - alias_generator = to_camel - populate_by_name = True + model_config = ConfigDict(alias_generator=to_camel, populate_by_name=True) """ @model_serializer diff --git a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/entity.py b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/entity.py index 9636980b..260086f2 100644 --- a/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/entity.py +++ b/libraries/Core/microsoft-agents-core/microsoft/agents/core/models/entity.py @@ -1,4 +1,4 @@ -from .agents_model import AgentsModel +from .agents_model import AgentsModel, ConfigDict from ._type_aliases import NonEmptyString @@ -9,7 +9,6 @@ class Entity(AgentsModel): :type type: str """ - class Config: - extra = "allow" + model_config = ConfigDict(extra="allow") type: NonEmptyString From 2791432925ab5f24ef7dfde3255e0095fe520651 Mon Sep 17 00:00:00 2001 From: Axel Suarez Date: Mon, 3 Mar 2025 14:38:26 -0800 Subject: [PATCH 2/2] Packaging for CI --- .azdo/ci-pr.yaml | 27 ++++++++++++++++++- .github/workflows/python-package.yml | 25 ++++++++++++++++- .../tests/test_activity_handler.py | 2 -- 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/.azdo/ci-pr.yaml b/.azdo/ci-pr.yaml index 5c0de986..64d19eb9 100644 --- a/.azdo/ci-pr.yaml +++ b/.azdo/ci-pr.yaml @@ -22,7 +22,7 @@ steps: - script: | python -m pip install --upgrade pip - python -m pip install flake8 pytest black + python -m pip install flake8 pytest black pytest-asyncio build if [ -f requirements.txt ]; then pip install -r requirements.txt; fi displayName: 'Install dependencies' @@ -34,6 +34,31 @@ steps: flake8 . --count --exit-zero --show-source --statistics displayName: 'Lint with flake8' +- script: | + mkdir -p dist + for dir in libraries/*; do + if [ -d "$dir" ]; then + for subdir in "$dir"/*; do + if [ -f "$subdir/pyproject.toml" ]; then + (cd "$subdir" && python -m build --outdir ../../../dist) + fi + done + fi + done + displayName: 'Build packages' + +- script: | + python -m pip install ./dist/microsoft_agents_core*.whl + python -m pip install ./dist/microsoft_agents_authentication*.whl + python -m pip install ./dist/microsoft_agents_connector*.whl + python -m pip install ./dist/microsoft_agents_client*.whl + python -m pip install ./dist/microsoft_agents_botbuilder*.whl + python -m pip install ./dist/microsoft_agents_authorization_msal*.whl + python -m pip install ./dist/microsoft_agents_copilotstudio_client*.whl + python -m pip install ./dist/microsoft_agents_hosting_aiohttp*.whl + python -m pip install ./dist/microsoft_agents_storage*.whl + displayName: 'Install wheels' + - script: | pytest displayName: 'Test with pytest' diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 66c4539e..bd0b37bc 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -27,7 +27,7 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - python -m pip install flake8 pytest black + python -m pip install flake8 pytest black pytest-asyncio build if [ -f requirements.txt ]; then pip install -r requirements.txt; fi - name: Check format with black run: | @@ -37,6 +37,29 @@ jobs: run: | # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide flake8 . --count --exit-zero --show-source --statistics + - name: Build packages + run: | + mkdir -p dist + for dir in libraries/*; do + if [ -d "$dir" ]; then + for subdir in "$dir"/*; do + if [ -f "$subdir/pyproject.toml" ]; then + (cd "$subdir" && python -m build --outdir ../../../dist) + fi + done + fi + done + - name: Install wheels + run: | + python -m pip install ./dist/microsoft_agents_core*.whl + python -m pip install ./dist/microsoft_agents_authentication*.whl + python -m pip install ./dist/microsoft_agents_connector*.whl + python -m pip install ./dist/microsoft_agents_client*.whl + python -m pip install ./dist/microsoft_agents_botbuilder*.whl + python -m pip install ./dist/microsoft_agents_authorization_msal*.whl + python -m pip install ./dist/microsoft_agents_copilotstudio_client*.whl + python -m pip install ./dist/microsoft_agents_hosting_aiohttp*.whl + python -m pip install ./dist/microsoft_agents_storage*.whl - name: Test with pytest run: | pytest diff --git a/libraries/Botbuilder/microsoft-agents-botbuilder/tests/test_activity_handler.py b/libraries/Botbuilder/microsoft-agents-botbuilder/tests/test_activity_handler.py index 7227675d..102e7af1 100644 --- a/libraries/Botbuilder/microsoft-agents-botbuilder/tests/test_activity_handler.py +++ b/libraries/Botbuilder/microsoft-agents-botbuilder/tests/test_activity_handler.py @@ -2,13 +2,11 @@ from unittest.mock import AsyncMock from microsoft.agents.botbuilder import ActivityHandler, TurnContext from microsoft.agents.core.models import ( - Activity, ActivityTypes, ChannelAccount, MessageReaction, SignInConstants, InvokeResponse, - AdaptiveCardInvokeValue, )