1
1
# Copyright (c) Microsoft Corporation. All rights reserved.
2
2
# Licensed under the MIT License.
3
3
4
+ import json
4
5
import 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
+ )
5
17
from botframework .connector import Channels
6
18
7
19
from botbuilder .core import TurnContext , MessageFactory
@@ -234,13 +246,62 @@ async def test_get_meeting_info(self):
234
246
handler = TeamsActivityHandler ()
235
247
await handler .on_turn (turn_context )
236
248
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
+
237
296
238
297
class TestTeamsActivityHandler (TeamsActivityHandler ):
239
298
async def on_turn (self , turn_context : TurnContext ):
240
299
await super ().on_turn (turn_context )
241
300
242
301
if turn_context .activity .text == "test_send_message_to_teams_channel" :
243
302
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 )
244
305
245
306
async def call_send_message_to_teams (self , turn_context : TurnContext ):
246
307
msg = MessageFactory .text ("call_send_message_to_teams" )
@@ -251,3 +312,71 @@ async def call_send_message_to_teams(self, turn_context: TurnContext):
251
312
252
313
assert reference [0 ].activity_id == "new_conversation_id"
253
314
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