diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/__init__.py b/packages/api/src/microsoft_teams/api/clients/conversation/__init__.py index dc421bc9..8dba1468 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/__init__.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/__init__.py @@ -6,13 +6,11 @@ from .activity import ConversationActivityClient from .client import ConversationClient from .member import ConversationMemberClient -from .params import CreateConversationParams, GetConversationsParams, GetConversationsResponse +from .params import CreateConversationParams __all__ = [ "ConversationActivityClient", "ConversationClient", "ConversationMemberClient", "CreateConversationParams", - "GetConversationsParams", - "GetConversationsResponse", ] diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/activity.py b/packages/api/src/microsoft_teams/api/clients/conversation/activity.py index e72d1104..a1ee86ef 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/activity.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/activity.py @@ -9,7 +9,7 @@ from microsoft_teams.common.http import Client from ...activities import ActivityParams, SentActivity -from ...models import Account +from ...models import TeamsChannelAccount from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -111,7 +111,7 @@ async def delete(self, conversation_id: str, activity_id: str) -> None: """ await self.http.delete(f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}") - async def get_members(self, conversation_id: str, activity_id: str) -> List[Account]: + async def get_members(self, conversation_id: str, activity_id: str) -> List[TeamsChannelAccount]: """ Get the members associated with an activity. @@ -120,12 +120,12 @@ async def get_members(self, conversation_id: str, activity_id: str) -> List[Acco activity_id: The ID of the activity Returns: - List of Account objects representing the activity members + List of TeamsChannelAccount objects representing the activity members """ response = await self.http.get( f"{self.service_url}/v3/conversations/{conversation_id}/activities/{activity_id}/members" ) - return [Account.model_validate(member) for member in response.json()] + return [TeamsChannelAccount.model_validate(member) for member in response.json()] @experimental("ExperimentalTeamsTargeted") async def create_targeted(self, conversation_id: str, activity: ActivityParams) -> SentActivity: diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/client.py b/packages/api/src/microsoft_teams/api/clients/conversation/client.py index 7ad5aa01..1c42a103 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/client.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/client.py @@ -3,7 +3,7 @@ Licensed under the MIT License. """ -from typing import Dict, Optional, Union +from typing import Optional, Union from microsoft_teams.common.http import Client, ClientOptions @@ -12,11 +12,7 @@ from ..base_client import BaseClient from .activity import ActivityParams, ConversationActivityClient from .member import ConversationMemberClient -from .params import ( - CreateConversationParams, - GetConversationsParams, - GetConversationsResponse, -) +from .params import CreateConversationParams class ConversationOperations: @@ -64,12 +60,12 @@ class MemberOperations(ConversationOperations): async def get_all(self): return await self._client.members_client.get(self._conversation_id) + async def get_paged(self, page_size: Optional[int] = None, continuation_token: Optional[str] = None): + return await self._client.members_client.get_paged(self._conversation_id, page_size, continuation_token) + async def get(self, member_id: str): return await self._client.members_client.get_by_id(self._conversation_id, member_id) - async def delete(self, member_id: str) -> None: - await self._client.members_client.delete(self._conversation_id, member_id) - class ConversationClient(BaseClient): """Client for managing Teams conversations.""" @@ -137,25 +133,6 @@ def members(self, conversation_id: str) -> MemberOperations: """ return MemberOperations(self, conversation_id) - async def get(self, params: Optional[GetConversationsParams] = None) -> GetConversationsResponse: - """Get a list of conversations. - - Args: - params: Optional parameters for getting conversations. - - Returns: - A response containing the list of conversations and a continuation token. - """ - query_params: Dict[str, str] = {} - if params and params.continuation_token: - query_params["continuationToken"] = params.continuation_token - - response = await self.http.get( - f"{self.service_url}/v3/conversations", - params=query_params, - ) - return GetConversationsResponse.model_validate(response.json()) - async def create(self, params: CreateConversationParams) -> ConversationResource: """Create a new conversation. diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/member.py b/packages/api/src/microsoft_teams/api/clients/conversation/member.py index f46692d3..78a7a958 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/member.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/member.py @@ -8,6 +8,7 @@ from microsoft_teams.common.http import Client from ...models import TeamsChannelAccount +from ...models.conversation import PagedMembersResult from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -47,6 +48,28 @@ async def get(self, conversation_id: str) -> List[TeamsChannelAccount]: response = await self.http.get(f"{self.service_url}/v3/conversations/{conversation_id}/members") return [TeamsChannelAccount.model_validate(member) for member in response.json()] + async def get_paged( + self, + conversation_id: str, + page_size: Optional[int] = None, + continuation_token: Optional[str] = None, + ) -> PagedMembersResult: + """ + Get a page of members in a conversation. + + Args: + conversation_id: The ID of the conversation. + page_size: Optional maximum number of members to return per page. + continuation_token: Optional token from a previous call to fetch the next page. + + Returns: + PagedMembersResult containing the members and an optional continuation token + for fetching subsequent pages. + """ + url = f"{self.service_url}/v3/conversations/{conversation_id}/pagedMembers" + response = await self.http.get(url, params={"pageSize": page_size, "continuationToken": continuation_token}) + return PagedMembersResult.model_validate(response.json()) + async def get_by_id(self, conversation_id: str, member_id: str) -> TeamsChannelAccount: """ Get a specific member in a conversation. @@ -60,13 +83,3 @@ async def get_by_id(self, conversation_id: str, member_id: str) -> TeamsChannelA """ response = await self.http.get(f"{self.service_url}/v3/conversations/{conversation_id}/members/{member_id}") return TeamsChannelAccount.model_validate(response.json()) - - async def delete(self, conversation_id: str, member_id: str) -> None: - """ - Remove a member from a conversation. - - Args: - conversation_id: The ID of the conversation - member_id: The ID of the member to remove - """ - await self.http.delete(f"{self.service_url}/v3/conversations/{conversation_id}/members/{member_id}") diff --git a/packages/api/src/microsoft_teams/api/clients/conversation/params.py b/packages/api/src/microsoft_teams/api/clients/conversation/params.py index c1f3c380..f750e0b6 100644 --- a/packages/api/src/microsoft_teams/api/clients/conversation/params.py +++ b/packages/api/src/microsoft_teams/api/clients/conversation/params.py @@ -5,16 +5,10 @@ from typing import Any, Dict, List, Optional -from ...models import Account, Conversation, CustomBaseModel +from ...models import Account, CustomBaseModel from .activity import ActivityParams -class GetConversationsParams(CustomBaseModel): - """Parameters for getting conversations.""" - - continuation_token: Optional[str] = None - - class CreateConversationParams(CustomBaseModel): """Parameters for creating a conversation.""" @@ -22,18 +16,10 @@ class CreateConversationParams(CustomBaseModel): """ Whether this is a group conversation. """ - bot: Optional[Account] = None - """ - The bot account to add to the conversation. - """ members: Optional[List[Account]] = None """ The members to add to the conversation. """ - topic_name: Optional[str] = None - """ - The topic name for the conversation. - """ tenant_id: Optional[str] = None """ The tenant ID for the conversation. @@ -46,16 +32,3 @@ class CreateConversationParams(CustomBaseModel): """ The channel-specific data for the conversation. """ - - -class GetConversationsResponse(CustomBaseModel): - """Response from getting conversations.""" - - continuation_token: Optional[str] = None - """ - Token for getting the next page of conversations. - """ - conversations: List[Conversation] = [] - """ - List of conversations. - """ diff --git a/packages/api/src/microsoft_teams/api/clients/meeting/client.py b/packages/api/src/microsoft_teams/api/clients/meeting/client.py index 7b467fbd..cea7277b 100644 --- a/packages/api/src/microsoft_teams/api/clients/meeting/client.py +++ b/packages/api/src/microsoft_teams/api/clients/meeting/client.py @@ -8,6 +8,7 @@ from microsoft_teams.common.http import Client, ClientOptions from ...models import MeetingInfo, MeetingParticipant +from ...models.meetings.meeting_notification import MeetingNotificationParams, MeetingNotificationResponse from ..api_client_settings import ApiClientSettings from ..base_client import BaseClient @@ -57,7 +58,31 @@ async def get_participant(self, meeting_id: str, id: str, tenant_id: str) -> Mee Returns: MeetingParticipant: The meeting participant information. """ - url = f"{self.service_url}/v1/meetings/{meeting_id}/participants/{id}?tenantId={tenant_id}" response = await self.http.get(url) return MeetingParticipant.model_validate(response.json()) + + async def send_notification( + self, meeting_id: str, params: MeetingNotificationParams + ) -> Optional[MeetingNotificationResponse]: + """ + Send a targeted meeting notification to participants. + + Returns None on full success (HTTP 202). Returns a MeetingNotificationResponse + with failure details on partial success (HTTP 207). + + Args: + meeting_id: The BASE64-encoded meeting ID. + params: The notification parameters including recipients and surfaces. + + Returns: + None if all notifications were sent successfully, or a MeetingNotificationResponse + with per-recipient failure details on partial success. + """ + response = await self.http.post( + f"{self.service_url}/v1/meetings/{meeting_id}/notification", + json=params.model_dump(by_alias=True, exclude_none=True), + ) + if not response.text: + return None + return MeetingNotificationResponse.model_validate(response.json()) diff --git a/packages/api/src/microsoft_teams/api/clients/team/client.py b/packages/api/src/microsoft_teams/api/clients/team/client.py index ddda3b10..7ff53140 100644 --- a/packages/api/src/microsoft_teams/api/clients/team/client.py +++ b/packages/api/src/microsoft_teams/api/clients/team/client.py @@ -56,4 +56,4 @@ async def get_conversations(self, id: str) -> List[ChannelInfo]: List of channel information. """ response = await self.http.get(f"{self.service_url}/v3/teams/{id}/conversations") - return [ChannelInfo.model_validate(channel) for channel in response.json()] + return [ChannelInfo.model_validate(channel) for channel in response.json()["conversations"]] diff --git a/packages/api/src/microsoft_teams/api/models/account.py b/packages/api/src/microsoft_teams/api/models/account.py index 5f3100c5..604a509e 100644 --- a/packages/api/src/microsoft_teams/api/models/account.py +++ b/packages/api/src/microsoft_teams/api/models/account.py @@ -5,6 +5,8 @@ from typing import Any, Dict, Literal, Optional +from pydantic import AliasChoices, Field + from .custom_base_model import CustomBaseModel AccountRole = Literal["user", "bot"] @@ -59,13 +61,17 @@ class TeamsChannelAccount(CustomBaseModel): """ Display-friendly name of the user or bot. """ - aad_object_id: Optional[str] = None + aad_object_id: Optional[str] = Field( + default=None, + validation_alias=AliasChoices("aadObjectId", "objectId"), + serialization_alias="aadObjectId", + ) """ The user's Object ID in Azure Active Directory (AAD). """ - role: Optional[AccountRole] = None + role: Optional[str] = Field(default=None, alias="userRole") """ - Role of the user (e.g., 'user' or 'bot'). + Role of the user in the conversation. """ given_name: Optional[str] = None """ diff --git a/packages/api/src/microsoft_teams/api/models/conversation/__init__.py b/packages/api/src/microsoft_teams/api/models/conversation/__init__.py index ccd35b43..fc336f5c 100644 --- a/packages/api/src/microsoft_teams/api/models/conversation/__init__.py +++ b/packages/api/src/microsoft_teams/api/models/conversation/__init__.py @@ -6,5 +6,6 @@ from .conversation import Conversation, ConversationType from .conversation_reference import ConversationReference from .conversation_resource import ConversationResource +from .paged_members_result import PagedMembersResult -__all__ = ["Conversation", "ConversationReference", "ConversationResource", "ConversationType"] +__all__ = ["Conversation", "ConversationReference", "ConversationResource", "ConversationType", "PagedMembersResult"] diff --git a/packages/api/src/microsoft_teams/api/models/conversation/paged_members_result.py b/packages/api/src/microsoft_teams/api/models/conversation/paged_members_result.py new file mode 100644 index 00000000..89aa7afc --- /dev/null +++ b/packages/api/src/microsoft_teams/api/models/conversation/paged_members_result.py @@ -0,0 +1,23 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import List, Optional + +from pydantic import Field + +from ..account import TeamsChannelAccount +from ..custom_base_model import CustomBaseModel + + +class PagedMembersResult(CustomBaseModel): + """ + Result of a paged members request. + """ + + members: List[TeamsChannelAccount] = Field(default_factory=list[TeamsChannelAccount]) + "The members in this page." + + continuation_token: Optional[str] = None + "Token to fetch the next page of members. None if this is the last page." diff --git a/packages/api/src/microsoft_teams/api/models/meetings/__init__.py b/packages/api/src/microsoft_teams/api/models/meetings/__init__.py index 8e718367..1a07032e 100644 --- a/packages/api/src/microsoft_teams/api/models/meetings/__init__.py +++ b/packages/api/src/microsoft_teams/api/models/meetings/__init__.py @@ -6,11 +6,23 @@ from .meeting import Meeting from .meeting_details import MeetingDetails from .meeting_info import MeetingInfo +from .meeting_notification import ( + MeetingNotificationParams, + MeetingNotificationRecipientFailure, + MeetingNotificationResponse, + MeetingNotificationSurface, + MeetingNotificationValue, +) from .meeting_participant import MeetingParticipant __all__ = [ "Meeting", "MeetingDetails", "MeetingInfo", + "MeetingNotificationParams", + "MeetingNotificationRecipientFailure", + "MeetingNotificationResponse", + "MeetingNotificationSurface", + "MeetingNotificationValue", "MeetingParticipant", ] diff --git a/packages/api/src/microsoft_teams/api/models/meetings/meeting_notification.py b/packages/api/src/microsoft_teams/api/models/meetings/meeting_notification.py new file mode 100644 index 00000000..71d2effd --- /dev/null +++ b/packages/api/src/microsoft_teams/api/models/meetings/meeting_notification.py @@ -0,0 +1,75 @@ +""" +Copyright (c) Microsoft Corporation. All rights reserved. +Licensed under the MIT License. +""" + +from typing import Any, Dict, List, Optional + +from ..custom_base_model import CustomBaseModel + + +class MeetingNotificationSurface(CustomBaseModel): + """ + A surface target for a meeting notification. + """ + + surface: str + "The surface type. E.g. 'meetingStage', 'meetingTabIcon', 'meetingCopilotPane'." + + content_type: Optional[str] = None + "The content type for surfaces that carry content. E.g. 'task'." + + content: Optional[Dict[str, Any]] = None + "The content payload for the surface." + + tab_entity_id: Optional[str] = None + "The tab entity ID, required for 'meetingTabIcon' surfaces." + + +class MeetingNotificationValue(CustomBaseModel): + """ + The value of a targeted meeting notification. + """ + + recipients: List[str] + "AAD object IDs of the meeting participants to notify." + + surfaces: List[MeetingNotificationSurface] + "The surfaces to send the notification to." + + +class MeetingNotificationParams(CustomBaseModel): + """ + Parameters for sending a meeting notification. + """ + + type: str = "targetedMeetingNotification" + "The notification type." + + value: MeetingNotificationValue + "The notification value containing recipients and surfaces." + + +class MeetingNotificationRecipientFailure(CustomBaseModel): + """ + Information about a recipient that failed to receive a meeting notification. + """ + + recipient_mri: Optional[str] = None + "The MRI of the recipient." + + error_code: Optional[str] = None + "The error code." + + failure_reason: Optional[str] = None + "The reason for the failure." + + +class MeetingNotificationResponse(CustomBaseModel): + """ + Response from a meeting notification request when some or all recipients failed (HTTP 207). + None is returned when all notifications were sent successfully (HTTP 202). + """ + + recipients_failure_info: Optional[List[MeetingNotificationRecipientFailure]] = None + "Information about recipients that failed to receive the notification." diff --git a/packages/api/src/microsoft_teams/api/models/team_details.py b/packages/api/src/microsoft_teams/api/models/team_details.py index 70d30ba0..527cbaa4 100644 --- a/packages/api/src/microsoft_teams/api/models/team_details.py +++ b/packages/api/src/microsoft_teams/api/models/team_details.py @@ -30,3 +30,6 @@ class TeamDetails(CustomBaseModel): member_count: Optional[int] = None "Count of members in the team." + + tenant_id: Optional[str] = None + "Azure Active Directory (AAD) tenant ID for the team." diff --git a/packages/api/tests/conftest.py b/packages/api/tests/conftest.py index 70196944..90efb2ff 100644 --- a/packages/api/tests/conftest.py +++ b/packages/api/tests/conftest.py @@ -86,18 +86,41 @@ def handler(request: httpx.Request) -> httpx.Response: "tokenExchangeResource": {"id": "mock_resource_id"}, } elif "/v3/teams/" in str(request.url) and "/conversations" in str(request.url): - response_data = [ - { - "id": "mock_channel_id_1", - "name": "General", - "type": "standard", - }, - { - "id": "mock_channel_id_2", - "name": "Random", - "type": "standard", - }, - ] + response_data = { + "conversations": [ + { + "id": "mock_channel_id_1", + "name": "General", + "type": "standard", + }, + { + "id": "mock_channel_id_2", + "name": "Random", + "type": "standard", + }, + ] + } + elif "/conversations/" in str(request.url) and "/pagedMembers" in str(request.url): + response_data = { + "members": [ + { + "id": "mock_member_id", + "name": "Mock Member", + "aadObjectId": "mock_aad_object_id", + } + ], + "continuationToken": "mock_continuation_token", + } + elif "/notification" in str(request.url) and request.method == "POST": + response_data = { + "recipientsFailureInfo": [ + { + "recipientMri": "8:orgid:mock_recipient", + "errorCode": "BadArgument", + "failureReason": "Invalid recipient", + } + ] + } elif "/conversations/" in str(request.url) and str(request.url).endswith("/members"): response_data = [ { @@ -112,17 +135,6 @@ def handler(request: httpx.Request) -> httpx.Response: "name": "Mock Member", "aadObjectId": "mock_aad_object_id", } - elif "/conversations" in str(request.url) and request.method == "GET": - response_data = { - "conversations": [ - { - "id": "mock_conversation_id", - "conversationType": "personal", - "isGroup": True, - } - ], - "continuationToken": "mock_continuation_token", - } elif "/conversations" in str(request.url) and request.method == "POST": # Parse request body to check if activity is present try: diff --git a/packages/api/tests/unit/test_conversation_client.py b/packages/api/tests/unit/test_conversation_client.py index 86053b3d..bc468e20 100644 --- a/packages/api/tests/unit/test_conversation_client.py +++ b/packages/api/tests/unit/test_conversation_client.py @@ -4,13 +4,13 @@ """ # pyright: basic +from unittest.mock import AsyncMock, patch + +import httpx import pytest from microsoft_teams.api.clients.conversation import ConversationClient -from microsoft_teams.api.clients.conversation.params import ( - CreateConversationParams, - GetConversationsParams, -) -from microsoft_teams.api.models import ConversationResource, TeamsChannelAccount +from microsoft_teams.api.clients.conversation.params import CreateConversationParams +from microsoft_teams.api.models import ConversationResource, PagedMembersResult, TeamsChannelAccount from microsoft_teams.common.http import Client, ClientOptions @@ -47,30 +47,6 @@ def test_conversation_client_initialization_with_options(self): assert client.http is not None assert client.service_url == service_url - @pytest.mark.asyncio - async def test_get_conversations(self, mock_http_client): - """Test getting conversations.""" - service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) - - params = GetConversationsParams(continuation_token="test_token") - response = await client.get(params) - - assert response.conversations is not None - assert isinstance(response.conversations, list) - assert response.continuation_token is not None - - @pytest.mark.asyncio - async def test_get_conversations_without_params(self, mock_http_client): - """Test getting conversations without parameters.""" - service_url = "https://test.service.url" - client = ConversationClient(service_url, mock_http_client) - - response = await client.get() - - assert response.conversations is not None - assert isinstance(response.conversations, list) - @pytest.mark.asyncio async def test_create_conversation(self, mock_http_client, mock_account, mock_activity): """Test creating a conversation with an activity.""" @@ -79,9 +55,7 @@ async def test_create_conversation(self, mock_http_client, mock_account, mock_ac params = CreateConversationParams( is_group=True, - bot=mock_account, members=[mock_account], - topic_name="Test Conversation", tenant_id="test_tenant_id", activity=mock_activity, channel_data={"custom": "data"}, @@ -101,9 +75,7 @@ async def test_create_conversation_without_activity(self, mock_http_client, mock params = CreateConversationParams( is_group=True, - bot=mock_account, members=[mock_account], - topic_name="Test Conversation", tenant_id="test_tenant_id", ) @@ -292,17 +264,41 @@ async def test_member_get(self, mock_http_client): assert result.name == "Mock Member" assert result.aad_object_id == "mock_aad_object_id" - async def test_member_delete(self, mock_http_client): - """Test deleting a member.""" + async def test_member_get_paged(self, mock_http_client): + """Test getting a page of members returns PagedMembersResult.""" + service_url = "https://test.service.url" client = ConversationClient(service_url, mock_http_client) conversation_id = "test_conversation_id" - member_id = "test_member_id" members = client.members(conversation_id) - # Should not raise an exception - await members.delete(member_id) + result = await members.get_paged() + + assert isinstance(result, PagedMembersResult) + assert len(result.members) == 1 + assert isinstance(result.members[0], TeamsChannelAccount) + assert result.members[0].id == "mock_member_id" + assert result.continuation_token == "mock_continuation_token" + + async def test_member_get_paged_with_token(self, mock_http_client): + """Test get_paged passes continuation_token and page_size.""" + + service_url = "https://test.service.url" + client = ConversationClient(service_url, mock_http_client) + members = client.members("test_conversation_id") + + mock_response = httpx.Response( + 200, + json={"members": [], "continuationToken": None}, + headers={"content-type": "application/json"}, + ) + with patch.object(mock_http_client, "get", new_callable=AsyncMock, return_value=mock_response) as mock_get: + await members.get_paged(page_size=50, continuation_token="some_token") + + called_params = mock_get.call_args.kwargs.get("params", {}) + assert called_params.get("pageSize") == 50 + assert called_params.get("continuationToken") == "some_token" @pytest.mark.unit diff --git a/packages/api/tests/unit/test_meeting_client.py b/packages/api/tests/unit/test_meeting_client.py index 7a2701e8..4f4e7d18 100644 --- a/packages/api/tests/unit/test_meeting_client.py +++ b/packages/api/tests/unit/test_meeting_client.py @@ -4,9 +4,19 @@ """ # pyright: basic +from unittest.mock import AsyncMock, patch + +import httpx import pytest from microsoft_teams.api.clients.meeting import MeetingClient -from microsoft_teams.api.models import MeetingInfo, MeetingParticipant +from microsoft_teams.api.models import ( + MeetingInfo, + MeetingNotificationParams, + MeetingNotificationResponse, + MeetingNotificationSurface, + MeetingNotificationValue, + MeetingParticipant, +) from microsoft_teams.common.http import Client, ClientOptions @@ -57,3 +67,59 @@ def test_meeting_client_strips_trailing_slash(self, mock_http_client): client = MeetingClient(service_url, mock_http_client) assert client.service_url == "https://test.service.url" + + @pytest.mark.asyncio + async def test_send_notification_partial_failure(self, mock_http_client): + """Test send_notification returns MeetingNotificationResponse on partial failure (HTTP 207).""" + + service_url = "https://test.service.url" + client = MeetingClient(service_url, mock_http_client) + + params = MeetingNotificationParams( + value=MeetingNotificationValue( + recipients=["mock_aad_oid"], + surfaces=[MeetingNotificationSurface(surface="meetingTabIcon", tab_entity_id="test")], + ) + ) + + partial_failure_response = httpx.Response( + 207, + json={ + "recipientsFailureInfo": [ + { + "recipientMri": "8:orgid:mock_recipient", + "errorCode": "BadArgument", + "failureReason": "Invalid recipient", + } + ] + }, + headers={"content-type": "application/json"}, + ) + with patch.object(mock_http_client, "post", new_callable=AsyncMock, return_value=partial_failure_response): + result = await client.send_notification("mock_meeting_id", params) + + assert isinstance(result, MeetingNotificationResponse) + assert result.recipients_failure_info is not None + assert len(result.recipients_failure_info) == 1 + assert result.recipients_failure_info[0].error_code == "BadArgument" + + @pytest.mark.asyncio + async def test_send_notification_full_success(self, mock_http_client): + """Test send_notification returns None on full success (HTTP 202, empty body).""" + import httpx + + service_url = "https://test.service.url" + client = MeetingClient(service_url, mock_http_client) + + params = MeetingNotificationParams( + value=MeetingNotificationValue( + recipients=["mock_aad_oid"], + surfaces=[MeetingNotificationSurface(surface="meetingTabIcon", tab_entity_id="test")], + ) + ) + + empty_response = httpx.Response(202, content=b"", headers={"content-type": "application/json"}) + with patch.object(mock_http_client, "post", new_callable=AsyncMock, return_value=empty_response): + result = await client.send_notification("mock_meeting_id", params) + + assert result is None diff --git a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py index eb209ec5..8d369478 100644 --- a/packages/apps/src/microsoft_teams/apps/contexts/function_context.py +++ b/packages/apps/src/microsoft_teams/apps/contexts/function_context.py @@ -120,7 +120,6 @@ async def _resolve_conversation_id(self, activity: str | ActivityParams | Adapti or return a pre-existing one.""" try: conversation_params = CreateConversationParams( - bot=Account(id=self.id, name=self.name, role="bot"), # type: ignore members=[Account(id=self.user_id, role="user", name=self.user_name)], tenant_id=self.tenant_id, is_group=False, diff --git a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py index 243463b3..4c50f26f 100644 --- a/packages/apps/src/microsoft_teams/apps/routing/activity_context.py +++ b/packages/apps/src/microsoft_teams/apps/routing/activity_context.py @@ -274,7 +274,6 @@ async def sign_in(self, options: Optional[SignInOptions] = None) -> Optional[str CreateConversationParams( tenant_id=self.activity.conversation.tenant_id, is_group=False, - bot=self.activity.recipient, members=[self.activity.from_], ) )