11# Copyright (c) Microsoft Corporation. All rights reserved.
22# Licensed under the MIT License.
33
4+ import json
45import aiounittest
6+ from botbuilder .schema .teams ._models_py3 import (
7+ ContentType ,
8+ MeetingNotificationChannelData ,
9+ MeetingStageSurface ,
10+ MeetingTabIconSurface ,
11+ OnBehalfOf ,
12+ TargetedMeetingNotification ,
13+ TargetedMeetingNotificationValue ,
14+ TaskModuleContinueResponse ,
15+ TaskModuleTaskInfo ,
16+ )
517from botframework .connector import Channels
618
719from botbuilder .core import TurnContext , MessageFactory
@@ -234,13 +246,62 @@ async def test_get_meeting_info(self):
234246 handler = TeamsActivityHandler ()
235247 await handler .on_turn (turn_context )
236248
249+ async def test_send_meeting_notificationt (self ):
250+ test_cases = [
251+ ("202" , "accepted" ),
252+ (
253+ "207" ,
254+ "if the notifications are sent only to parital number of recipients\
255+ because the validation on some recipients' ids failed or some\
256+ recipients were not found in the roster. In this case, \
257+ SMBA will return the user MRIs of those failed recipients\
258+ in a format that was given to a bot (ex: if a bot sent \
259+ encrypted user MRIs, return encrypted one)." ,
260+ ),
261+ (
262+ "400" ,
263+ "when Meeting Notification request payload validation fails. For instance,\
264+ Recipients: # of recipients is greater than what the API allows ||\
265+ all of recipients' user ids were invalid, Surface: Surface list\
266+ is empty or null, Surface type is invalid, Duplicative \
267+ surface type exists in one payload" ,
268+ ),
269+ (
270+ "403" ,
271+ "if the bot is not allowed to send the notification. In this case,\
272+ the payload should contain more detail error message. \
273+ There can be many reasons: bot disabled by tenant admin,\
274+ blocked during live site mitigation, the bot does not\
275+ have a correct RSC permission for a specific surface type, etc" ,
276+ ),
277+ ]
278+ for status_code , expected_message in test_cases :
279+ adapter = SimpleAdapterWithCreateConversation ()
280+
281+ activity = Activity (
282+ type = "targetedMeetingNotification" ,
283+ text = "Test-send_meeting_notificationt" ,
284+ channel_id = Channels .ms_teams ,
285+ from_property = ChannelAccount (
286+ aad_object_id = "participantId-1" , name = status_code
287+ ),
288+ service_url = "https://test.coffee" ,
289+ conversation = ConversationAccount (id = "conversation-id" ),
290+ )
291+
292+ turn_context = TurnContext (adapter , activity )
293+ handler = TeamsActivityHandler ()
294+ await handler .on_turn (turn_context )
295+
237296
238297class TestTeamsActivityHandler (TeamsActivityHandler ):
239298 async def on_turn (self , turn_context : TurnContext ):
240299 await super ().on_turn (turn_context )
241300
242301 if turn_context .activity .text == "test_send_message_to_teams_channel" :
243302 await self .call_send_message_to_teams (turn_context )
303+ elif turn_context .activity .text == "test_send_meeting_notification" :
304+ await self .call_send_meeting_notification (turn_context )
244305
245306 async def call_send_message_to_teams (self , turn_context : TurnContext ):
246307 msg = MessageFactory .text ("call_send_message_to_teams" )
@@ -251,3 +312,71 @@ async def call_send_message_to_teams(self, turn_context: TurnContext):
251312
252313 assert reference [0 ].activity_id == "new_conversation_id"
253314 assert reference [1 ] == "reference123"
315+
316+ async def call_send_meeting_notification (self , turn_context : TurnContext ):
317+ from_property = turn_context .activity .from_property
318+ try :
319+ # Send the meeting notification asynchronously
320+ failed_participants = await TeamsInfo .send_meeting_notification (
321+ turn_context ,
322+ self .get_targeted_meeting_notification (from_property ),
323+ "meeting-id" ,
324+ )
325+
326+ # Handle based on the 'from_property.name'
327+ if from_property .name == "207" :
328+ self .assertEqual (
329+ "failingid" ,
330+ failed_participants .recipients_failure_info [0 ].recipient_mri ,
331+ )
332+ elif from_property .name == "202" :
333+ assert failed_participants is None
334+ else :
335+ raise TypeError (
336+ f"Expected HttpOperationException with response status code { from_property .name } ."
337+ )
338+
339+ except ValueError as ex :
340+ # Assert that the response status code matches the from_property.name
341+ assert from_property .name == str (int (ex .response .status_code ))
342+
343+ # Deserialize the error response content to an ErrorResponse object
344+ error_response = json .loads (ex .response .content )
345+
346+ # Handle based on error codes
347+ if from_property .name == "400" :
348+ assert error_response ["error" ]["code" ] == "BadSyntax"
349+ elif from_property .name == "403" :
350+ assert error_response ["error" ]["code" ] == "BotNotInConversationRoster"
351+ else :
352+ raise TypeError (
353+ f"Expected HttpOperationException with response status code { from_property .name } ."
354+ )
355+
356+ def get_targeted_meeting_notification (self , from_account : ChannelAccount ):
357+ recipients = [from_account .id ]
358+
359+ if from_account .name == "207" :
360+ recipients .append ("failingid" )
361+
362+ meeting_stage_surface = MeetingStageSurface (
363+ content = TaskModuleContinueResponse (
364+ value = TaskModuleTaskInfo (title = "title here" , height = 3 , width = 2 )
365+ ),
366+ content_type = ContentType .Task ,
367+ )
368+
369+ meeting_tab_icon_surface = MeetingTabIconSurface (
370+ tab_entity_id = "test tab entity id"
371+ )
372+
373+ value = TargetedMeetingNotificationValue (
374+ recipients = recipients ,
375+ surfaces = [meeting_stage_surface , meeting_tab_icon_surface ],
376+ )
377+
378+ obo = OnBehalfOf (display_name = from_account .name , mri = from_account .id )
379+
380+ channel_data = MeetingNotificationChannelData (on_behalf_of_list = [obo ])
381+
382+ return TargetedMeetingNotification (value = value , channel_data = channel_data )
0 commit comments