microsoft/teams.net

Public

mirrored fromhttps://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
next/oauth-card-null-ref-bug

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

core/test/Microsoft.Teams.Bot.Compat.UnitTests/CompatActivityTests.cs

606lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using System.Text.Json;
5using System.Text.Json.Nodes;
6using AdaptiveCards;
7using Microsoft.Bot.Schema;
8using Microsoft.Teams.Bot.Core.Schema;
9using Newtonsoft.Json;
10
11namespace Microsoft.Teams.Bot.Compat.UnitTests
12{
13 public class CompatActivityTests
14 {
15 #region Core Properties Tests
16
17 [Fact]
18 public void FromCompatActivity_PreservesCoreProperties()
19 {
20 Activity activity = new()
21 {
22 Type = ActivityTypes.Message,
23 ServiceUrl = "https://smba.trafficmanager.net/teams",
24 ChannelId = "msteams",
25 Id = "test-id-123",
26 From = new ChannelAccount { Id = "user-123", Name = "Test User" },
27 Recipient = new ChannelAccount { Id = "bot-456", Name = "Test Bot" },
28 Conversation = new Microsoft.Bot.Schema.ConversationAccount { Id = "conv-789", Name = "Test Conversation" }
29 };
30
31 CoreActivity coreActivity = activity.FromCompatActivity();
32
33 Assert.NotNull(coreActivity);
34 Assert.Equal(activity.Type, coreActivity.Type);
35 Assert.Equal(activity.ServiceUrl, coreActivity.ServiceUrl?.ToString());
36 Assert.Equal(activity.ChannelId, coreActivity.ChannelId);
37 Assert.Equal(activity.Id, coreActivity.Id);
38 Assert.Equal(activity.From?.Id, coreActivity.From?.Id);
39 Assert.Equal(activity.From?.Name, coreActivity.From?.Name);
40 Assert.Equal(activity.Recipient?.Id, coreActivity.Recipient?.Id);
41 Assert.Equal(activity.Conversation?.Id, coreActivity.Conversation?.Id);
42 }
43
44 [Fact]
45 public void FromCompatActivity_PreservesTextAndMetadata()
46 {
47 Activity activity = new()
48 {
49 Type = ActivityTypes.Message,
50 Text = "Hello, this is a test message",
51 TextFormat = "plain",
52 Locale = "en-US",
53 InputHint = "acceptingInput",
54 ReplyToId = "reply-to-123"
55 };
56
57 CoreActivity coreActivity = activity.FromCompatActivity();
58
59 Assert.NotNull(coreActivity);
60 Assert.Equal(activity.Text, coreActivity.Properties["text"]?.ToString());
61 Assert.Equal(activity.InputHint, coreActivity.Properties["inputHint"]?.ToString());
62 Assert.Equal(activity.ReplyToId, coreActivity.ReplyToId);
63 Assert.Equal(activity.Locale, coreActivity.Properties["locale"]?.ToString());
64 }
65
66 #endregion
67
68 #region Attachments Tests
69
70 [Fact]
71 public void FromCompatActivity_PreservesAdaptiveCardAttachment()
72 {
73 string json = LoadTestData("AdaptiveCardActivity.json");
74 Activity botActivity = JsonConvert.DeserializeObject<Activity>(json)!;
75 Assert.NotNull(botActivity);
76 Assert.Single(botActivity.Attachments);
77
78 CoreActivity coreActivity = botActivity.FromCompatActivity();
79
80 Assert.NotNull(coreActivity);
81 Assert.NotNull(coreActivity.Attachments);
82 Assert.Single(coreActivity.Attachments);
83
84 JsonNode? attachmentNode = coreActivity.Attachments[0];
85 Assert.NotNull(attachmentNode);
86 JsonObject attachmentObj = attachmentNode.AsObject();
87
88 string? contentType = attachmentObj["contentType"]?.GetValue<string>();
89 Assert.Equal("application/vnd.microsoft.card.adaptive", contentType);
90
91 JsonNode? content = attachmentObj["content"];
92 Assert.NotNull(content);
93 AdaptiveCard card = AdaptiveCard.FromJson(content.ToJsonString()).Card;
94 Assert.Equal(2, card.Body?.Count);
95 AdaptiveTextBlock? firstTextBlock = card?.Body?[0] as AdaptiveTextBlock;
96 Assert.NotNull(firstTextBlock);
97 Assert.Equal("Mention a user by User Principle Name: Hello <at>Test User UPN</at>", firstTextBlock.Text);
98 }
99
100 [Fact]
101 public void FromCompatActivity_PreservesMultipleAttachments()
102 {
103 Activity activity = new()
104 {
105 Type = ActivityTypes.Message,
106 Attachments = new List<Attachment>
107 {
108 new() { ContentType = "text/plain", Content = "First attachment" },
109 new() { ContentType = "image/png", ContentUrl = "https://example.com/image.png" }
110 }
111 };
112
113 CoreActivity coreActivity = activity.FromCompatActivity();
114
115 Assert.NotNull(coreActivity.Attachments);
116 Assert.Equal(2, coreActivity.Attachments?.Count);
117 Assert.Equal("text/plain", coreActivity.Attachments?[0]?["contentType"]?.GetValue<string>());
118 Assert.Equal("image/png", coreActivity.Attachments?[1]?["contentType"]?.GetValue<string>());
119 }
120
121 #endregion
122
123 #region Entities Tests
124
125 [Fact]
126 public void FromCompatActivity_PreservesEntities()
127 {
128 string json = LoadTestData("AdaptiveCardActivity.json");
129 Activity botActivity = JsonConvert.DeserializeObject<Activity>(json)!;
130
131 CoreActivity coreActivity = botActivity.FromCompatActivity();
132
133 Assert.NotNull(coreActivity.Entities);
134 Assert.Single(coreActivity.Entities);
135
136 JsonObject? entity = coreActivity.Entities[0]?.AsObject();
137 Assert.NotNull(entity);
138 Assert.Equal("https://schema.org/Message", entity["type"]?.GetValue<string>());
139 }
140
141 [Fact]
142 public void FromCompatActivity_PreservesMultipleEntities()
143 {
144 string json = LoadTestData("SuggestedActionsActivity.json");
145 Activity botActivity = JsonConvert.DeserializeObject<Activity>(json)!;
146
147 CoreActivity coreActivity = botActivity.FromCompatActivity();
148
149 Assert.NotNull(coreActivity.Entities);
150 Assert.Equal(2, coreActivity.Entities?.Count);
151
152 JsonObject? firstEntity = coreActivity.Entities?[0]?.AsObject();
153 Assert.Equal("https://schema.org/Message", firstEntity?["type"]?.GetValue<string>());
154
155 JsonObject? secondEntity = coreActivity.Entities?[1]?.AsObject();
156 Assert.Equal("BotMessageMetadata", secondEntity?["type"]?.GetValue<string>());
157 }
158
159 #endregion
160
161 #region SuggestedActions Tests
162
163 [Fact]
164 public void FromCompatActivity_PreservesSuggestedActions()
165 {
166 string json = LoadTestData("SuggestedActionsActivity.json");
167 Activity botActivity = JsonConvert.DeserializeObject<Activity>(json)!;
168 Assert.NotNull(botActivity.SuggestedActions);
169 Assert.Equal(3, botActivity.SuggestedActions.Actions.Count);
170
171 CoreActivity coreActivity = botActivity.FromCompatActivity();
172
173 Assert.True(coreActivity.Properties.ContainsKey("suggestedActions"));
174
175 string coreActivityJson = coreActivity.ToJson();
176 JsonNode coreActivityNode = JsonNode.Parse(coreActivityJson)!;
177
178 JsonNode? suggestedActions = coreActivityNode["suggestedActions"];
179 Assert.NotNull(suggestedActions);
180
181 JsonArray? actions = suggestedActions["actions"]?.AsArray();
182 Assert.NotNull(actions);
183 Assert.Equal(3, actions.Count);
184 }
185
186 [Fact]
187 public void FromCompatActivity_PreservesSuggestedActionDetails()
188 {
189 string json = LoadTestData("SuggestedActionsActivity.json");
190 Activity botActivity = JsonConvert.DeserializeObject<Activity>(json)!;
191
192 CoreActivity coreActivity = botActivity.FromCompatActivity();
193 string coreActivityJson = coreActivity.ToJson();
194 JsonNode coreActivityNode = JsonNode.Parse(coreActivityJson)!;
195
196 JsonArray? actions = coreActivityNode["suggestedActions"]?["actions"]?.AsArray();
197 Assert.NotNull(actions);
198
199 // Verify Action.Odsl actions
200 Assert.Equal("Action.Odsl", actions[0]?["type"]?.GetValue<string>());
201 Assert.Equal("Add reviewers", actions[0]?["title"]?.GetValue<string>());
202 Assert.NotNull(actions[0]?["value"]);
203
204 Assert.Equal("Action.Odsl", actions[1]?["type"]?.GetValue<string>());
205 Assert.Equal("Open agent settings", actions[1]?["title"]?.GetValue<string>());
206
207 // Verify Action.Compose action
208 Assert.Equal("Action.Compose", actions[2]?["type"]?.GetValue<string>());
209 Assert.Equal("Ask me a question", actions[2]?["title"]?.GetValue<string>());
210 Assert.NotNull(actions[2]?["value"]);
211 }
212
213 #endregion
214
215 #region ChannelData Tests
216
217 [Fact]
218 public void FromCompatActivity_PreservesChannelData()
219 {
220 Activity activity = new()
221 {
222 Type = ActivityTypes.Message,
223 ChannelData = new { customProperty = "customValue", nestedObject = new { key = "value" } }
224 };
225
226 CoreActivity coreActivity = activity.FromCompatActivity();
227
228 Assert.NotNull(coreActivity.ChannelData);
229 Assert.True(coreActivity.ChannelData.Properties.ContainsKey("customProperty"));
230 Assert.Equal("customValue", coreActivity.ChannelData.Properties["customProperty"]?.ToString());
231 }
232
233 [Fact]
234 public void FromCompatActivity_PreservesComplexChannelData()
235 {
236 string json = LoadTestData("SuggestedActionsActivity.json");
237 Activity botActivity = JsonConvert.DeserializeObject<Activity>(json)!;
238
239 CoreActivity coreActivity = botActivity.FromCompatActivity();
240
241 Assert.NotNull(coreActivity.ChannelData);
242 Assert.True(coreActivity.ChannelData.Properties.ContainsKey("feedbackLoopEnabled"));
243
244 JsonElement feedbackLoopValue = (JsonElement)coreActivity.ChannelData.Properties["feedbackLoopEnabled"]!;
245 Assert.True(feedbackLoopValue.GetBoolean());
246 }
247
248 #endregion
249
250 #region Integration Tests
251
252 [Fact]
253 public void FromCompatActivity_CompleteRoundTrip_AdaptiveCard()
254 {
255 // Verify the complete adaptive card payload round-trips successfully
256 string originalJson = LoadTestData("AdaptiveCardActivity.json");
257 Activity botActivity = JsonConvert.DeserializeObject<Activity>(originalJson)!;
258
259 CoreActivity coreActivity = botActivity.FromCompatActivity();
260 string coreActivityJson = coreActivity.ToJson();
261
262 // Use JsonNode.DeepEquals to verify structural equality
263 JsonNode originalNode = JsonNode.Parse(originalJson)!;
264 JsonNode coreNode = JsonNode.Parse(coreActivityJson)!;
265
266 Assert.True(JsonNode.DeepEquals(originalNode, coreNode));
267 }
268
269 [Fact]
270 public void FromCompatActivity_CompleteRoundTrip_SuggestedActions()
271 {
272 // Verify the complete suggested actions payload round-trips successfully
273 string originalJson = LoadTestData("SuggestedActionsActivity.json");
274 Activity botActivity = JsonConvert.DeserializeObject<Activity>(originalJson)!;
275
276 CoreActivity coreActivity = botActivity.FromCompatActivity();
277 string coreActivityJson = coreActivity.ToJson();
278
279 // Use JsonNode.DeepEquals to verify structural equality
280 JsonNode originalNode = JsonNode.Parse(originalJson)!;
281 JsonNode coreNode = JsonNode.Parse(coreActivityJson)!;
282
283 Assert.True(JsonNode.DeepEquals(originalNode, coreNode));
284 }
285
286 #endregion
287
288 #region MessageFactory.Attachment Scenario Tests
289
290 [Fact]
291 public void FromCompatActivity_MessageFactoryAttachment_ReturnsNonNull()
292 {
293 // Mimic what MessageFactory.Attachment() creates - Activity with an attachment but minimal properties
294 Activity activity = new()
295 {
296 Type = ActivityTypes.Message,
297 Attachments = new List<Attachment>
298 {
299 new()
300 {
301 ContentType = "application/vnd.microsoft.card.oauth",
302 Content = new
303 {
304 buttons = new[]
305 {
306 new { type = "signin", title = "Sign in", value = "https://example.com/signin" }
307 }
308 }
309 }
310 },
311 // Properties set by ProjectAgentBot after MessageFactory.Attachment()
312 Recipient = new ChannelAccount { Id = "user-123", Name = "Test User" },
313 Conversation = new Microsoft.Bot.Schema.ConversationAccount { Id = "conv-456" }
314 };
315
316 // This is what fails in SendToConversationWithHttpMessagesAsync
317 CoreActivity? coreActivity = activity.FromCompatActivity();
318
319 // Verify it doesn't return null
320 Assert.NotNull(coreActivity);
321 Assert.Equal(activity.Type, coreActivity.Type);
322 Assert.Equal(activity.Recipient?.Id, coreActivity.Recipient?.Id);
323 Assert.Equal(activity.Conversation?.Id, coreActivity.Conversation?.Id);
324 }
325
326 [Fact]
327 public void FromCompatActivity_MinimalActivity_ReturnsNonNull()
328 {
329 // Test even more minimal activity - what if it's missing ServiceUrl, From, etc?
330 Activity activity = new()
331 {
332 Type = ActivityTypes.Message,
333 Recipient = new ChannelAccount { Id = "user-123" },
334 Conversation = new Microsoft.Bot.Schema.ConversationAccount { Id = "conv-456" }
335 };
336
337 CoreActivity? coreActivity = activity.FromCompatActivity();
338
339 // Should not return null even for minimal activity
340 Assert.NotNull(coreActivity);
341 }
342
343 [Fact]
344 public void FromCompatActivity_ActivityWithOnlyAttachment_ReturnsNonNull()
345 {
346 // Test if activity with ONLY attachment and no other properties returns null
347 Activity activity = new()
348 {
349 Type = ActivityTypes.Message,
350 Attachments = new List<Attachment>
351 {
352 new() { ContentType = "text/plain", Content = "test" }
353 }
354 };
355
356 CoreActivity? coreActivity = activity.FromCompatActivity();
357
358 Assert.NotNull(coreActivity);
359 }
360
361 [Fact]
362 public void FromCompatActivity_ExactProjectAgentScenario_ConversationNotNull()
363 {
364 // EXACT scenario from ProjectAgentBot - create activity with MessageFactory pattern,
365 // then set Recipient and Conversation (which is what ProjectAgent does)
366 Activity activity = new()
367 {
368 Type = ActivityTypes.Message,
369 Attachments = new List<Attachment>
370 {
371 new()
372 {
373 ContentType = "application/vnd.microsoft.card.oauth",
374 Content = new { buttons = new[] { new { type = "signin" } } }
375 }
376 }
377 };
378
379 // These lines mimic ProjectAgentBot.cs lines 1255-1256
380 activity.Recipient = new ChannelAccount { Id = "user-123", Name = "Test User" };
381 activity.Conversation = new Microsoft.Bot.Schema.ConversationAccount { Id = "test-conv-id" };
382
383 // Convert to CoreActivity
384 CoreActivity? coreActivity = activity.FromCompatActivity();
385
386 // Check that coreActivity is not null
387 Assert.NotNull(coreActivity);
388
389 // Check that coreActivity.Conversation is accessible (this is line 318 in CompatConversations)
390 // The ??= operator would try to access coreActivity.Conversation
391 var conversation = coreActivity.Conversation;
392
393 // Log what we got
394 Console.WriteLine($"coreActivity.Conversation is null: {conversation == null}");
395 Console.WriteLine($"coreActivity.Conversation.Id: {conversation?.Id}");
396 }
397
398 [Fact]
399 public void FromCompatActivity_PreservesFromAccountProperties()
400 {
401 // Test that From account's Properties dictionary is properly initialized after conversion
402 Activity activity = new()
403 {
404 Type = ActivityTypes.Message,
405 From = new ChannelAccount { Id = "user-123", Name = "Test User" },
406 Recipient = new ChannelAccount { Id = "bot-456", Name = "Test Bot" },
407 Conversation = new Microsoft.Bot.Schema.ConversationAccount { Id = "conv-789" }
408 };
409
410 CoreActivity coreActivity = activity.FromCompatActivity();
411
412 Assert.NotNull(coreActivity);
413 Assert.NotNull(coreActivity.From);
414
415 // The critical check - is Properties initialized?
416 Assert.NotNull(coreActivity.From.Properties);
417
418 // Try calling GetAgenticIdentity - this should not throw NullReferenceException
419 var identity = coreActivity.From.GetAgenticIdentity();
420 Assert.Null(identity); // Should be null since no agentic properties were set
421 }
422
423 [Fact]
424 public void FromCompatActivity_WithoutFromProperty_DoesNotCrash()
425 {
426 // EXACT ProjectAgent scenario - MessageFactory.Attachment() doesn't set From
427 Activity activity = new()
428 {
429 Type = ActivityTypes.Message,
430 Attachments = new List<Attachment>
431 {
432 new()
433 {
434 ContentType = "application/vnd.microsoft.card.oauth",
435 Content = new { buttons = new[] { new { type = "signin" } } }
436 }
437 },
438 Recipient = new ChannelAccount { Id = "user-123", Name = "Test User" },
439 Conversation = new Microsoft.Bot.Schema.ConversationAccount { Id = "conv-456" }
440 // NOTE: From is NOT set - this is what MessageFactory.Attachment() creates
441 };
442
443 // This is line 309 in CompatConversations
444 CoreActivity? coreActivity = activity.FromCompatActivity();
445
446 // These checks mimic what happens in SendToConversationWithHttpMessagesAsync
447 Assert.NotNull(coreActivity);
448
449 // Check if From is null after conversion
450 Console.WriteLine($"coreActivity.From is null: {coreActivity.From == null}");
451
452 // If From is null, GetAgenticIdentity() would be called on null with ?. operator (safe)
453 // But let's check if other code paths could fail
454 if (coreActivity.From != null)
455 {
456 Console.WriteLine($"coreActivity.From.Properties is null: {coreActivity.From.Properties == null}");
457 }
458 }
459
460 #endregion
461
462 private static string LoadTestData(string fileName)
463 {
464 string testDataPath = Path.Combine(AppContext.BaseDirectory, "TestData", fileName);
465 return File.ReadAllText(testDataPath);
466 }
467 }
468
469 public class FromCompatChannelAccountTests
470 {
471 [Fact]
472 public void FromCompatChannelAccount_MapsIdAndName()
473 {
474 Microsoft.Bot.Schema.ChannelAccount account = new() { Id = "user-1", Name = "Alice" };
475
476 Microsoft.Teams.Bot.Core.Schema.ConversationAccount result = account.FromCompatChannelAccount();
477
478 Assert.Equal("user-1", result.Id);
479 Assert.Equal("Alice", result.Name);
480 }
481
482 [Fact]
483 public void FromCompatChannelAccount_MapsAadObjectIdToProperties()
484 {
485 Microsoft.Bot.Schema.ChannelAccount account = new() { Id = "user-1", AadObjectId = "aad-123" };
486
487 Microsoft.Teams.Bot.Core.Schema.ConversationAccount result = account.FromCompatChannelAccount();
488
489 Assert.True(result.Properties.TryGetValue("aadObjectId", out object? val));
490 Assert.Equal("aad-123", val?.ToString());
491 }
492
493 [Fact]
494 public void FromCompatChannelAccount_MapsRoleToUserRoleInProperties()
495 {
496 Microsoft.Bot.Schema.ChannelAccount account = new() { Id = "user-1", Role = "owner" };
497
498 Microsoft.Teams.Bot.Core.Schema.ConversationAccount result = account.FromCompatChannelAccount();
499
500 Assert.True(result.Properties.TryGetValue("userRole", out object? val));
501 Assert.Equal("owner", val?.ToString());
502 }
503
504 [Fact]
505 public void FromCompatChannelAccount_SkipsNullAadObjectIdAndRole()
506 {
507 Microsoft.Bot.Schema.ChannelAccount account = new() { Id = "user-1" };
508
509 Microsoft.Teams.Bot.Core.Schema.ConversationAccount result = account.FromCompatChannelAccount();
510
511 Assert.False(result.Properties.ContainsKey("aadObjectId"));
512 Assert.False(result.Properties.ContainsKey("userRole"));
513 }
514
515 [Fact]
516 public void FromCompatChannelAccount_ThrowsOnNull()
517 {
518 Microsoft.Bot.Schema.ChannelAccount? account = null;
519 Assert.Throws<ArgumentNullException>(() => account!.FromCompatChannelAccount());
520 }
521 }
522
523 public class FromCompatConversationParametersTests
524 {
525 [Fact]
526 public void FromCompatConversationParameters_MapsAllScalarFields()
527 {
528 Microsoft.Bot.Schema.ConversationParameters parameters = new()
529 {
530 IsGroup = true,
531 TopicName = "Test Topic",
532 TenantId = "tenant-abc",
533 ChannelData = new { custom = "data" },
534 };
535
536 Microsoft.Teams.Bot.Core.ConversationParameters result = parameters.FromCompatConversationParameters();
537
538 Assert.True(result.IsGroup);
539 Assert.Equal("Test Topic", result.TopicName);
540 Assert.Equal("tenant-abc", result.TenantId);
541 Assert.NotNull(result.ChannelData);
542 }
543
544 [Fact]
545 public void FromCompatConversationParameters_MapsBotAccount()
546 {
547 Microsoft.Bot.Schema.ConversationParameters parameters = new()
548 {
549 Bot = new Microsoft.Bot.Schema.ChannelAccount { Id = "bot-1", Name = "MyBot" }
550 };
551
552 Microsoft.Teams.Bot.Core.ConversationParameters result = parameters.FromCompatConversationParameters();
553
554 Assert.NotNull(result.Bot);
555 Assert.Equal("bot-1", result.Bot.Id);
556 Assert.Equal("MyBot", result.Bot.Name);
557 }
558
559 [Fact]
560 public void FromCompatConversationParameters_MapsMembers()
561 {
562 Microsoft.Bot.Schema.ConversationParameters parameters = new()
563 {
564 Members =
565 [
566 new Microsoft.Bot.Schema.ChannelAccount { Id = "user-1", Name = "Alice" },
567 new Microsoft.Bot.Schema.ChannelAccount { Id = "user-2", Name = "Bob" },
568 ]
569 };
570
571 Microsoft.Teams.Bot.Core.ConversationParameters result = parameters.FromCompatConversationParameters();
572
573 Assert.NotNull(result.Members);
574 Assert.Equal(2, result.Members.Count);
575 Assert.Equal("user-1", result.Members[0].Id);
576 Assert.Equal("user-2", result.Members[1].Id);
577 }
578
579 [Fact]
580 public void FromCompatConversationParameters_NullActivityProducesNullActivity()
581 {
582 Microsoft.Bot.Schema.ConversationParameters parameters = new() { Activity = null };
583
584 Microsoft.Teams.Bot.Core.ConversationParameters result = parameters.FromCompatConversationParameters();
585
586 Assert.Null(result.Activity);
587 }
588
589 [Fact]
590 public void FromCompatConversationParameters_NullBotProducesNullBot()
591 {
592 Microsoft.Bot.Schema.ConversationParameters parameters = new() { Bot = null };
593
594 Microsoft.Teams.Bot.Core.ConversationParameters result = parameters.FromCompatConversationParameters();
595
596 Assert.Null(result.Bot);
597 }
598
599 [Fact]
600 public void FromCompatConversationParameters_ThrowsOnNull()
601 {
602 Microsoft.Bot.Schema.ConversationParameters? parameters = null;
603 Assert.Throws<ArgumentNullException>(() => parameters!.FromCompatConversationParameters());
604 }
605 }
606}
607