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/CompatConversationsTests.cs

464lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using Microsoft.Bot.Schema;
5using Microsoft.Teams.Bot.Core;
6using Microsoft.Teams.Bot.Core.Schema;
7using Moq;
8
9namespace Microsoft.Teams.Bot.Compat.UnitTests
10{
11 public class CompatConversationsTests
12 {
13 private const string TestServiceUrl = "https://smba.trafficmanager.net/amer/";
14 private const string TestConversationId = "test-conversation-id";
15 private const string TestActivityId = "test-activity-id";
16
17 [Fact]
18 public async Task SendToConversationWithHttpMessagesAsync_SetsServiceUrlFromProperty_WhenActivityServiceUrlIsNull()
19 {
20 // Arrange
21 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
22 CompatConversations compatConversations = new(mockConversationClient.Object)
23 {
24 ServiceUrl = TestServiceUrl
25 };
26
27 Activity activity = new()
28 {
29 Type = ActivityTypes.Message,
30 Text = "Test message"
31 };
32
33 CoreActivity? capturedActivity = null;
34 mockConversationClient
35 .Setup(c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()))
36 .Callback<CoreActivity, Dictionary<string, string>?, CancellationToken>((act, _, _) => capturedActivity = act)
37 .ReturnsAsync(new SendActivityResponse { Id = TestActivityId });
38
39 // Act
40 await compatConversations.SendToConversationWithHttpMessagesAsync(TestConversationId, activity);
41
42 // Assert
43 Assert.NotNull(capturedActivity);
44 Assert.NotNull(capturedActivity.ServiceUrl);
45 Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/'));
46 mockConversationClient.Verify(
47 c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()),
48 Times.Once);
49 }
50
51 [Fact]
52 public async Task SendToConversationWithHttpMessagesAsync_DoesNotOverrideServiceUrl_WhenActivityServiceUrlIsSet()
53 {
54 // Arrange
55 const string activityServiceUrl = "https://custom.service.url/";
56 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
57 CompatConversations compatConversations = new(mockConversationClient.Object)
58 {
59 ServiceUrl = TestServiceUrl
60 };
61
62 Activity activity = new()
63 {
64 Type = ActivityTypes.Message,
65 Text = "Test message",
66 ServiceUrl = activityServiceUrl
67 };
68
69 CoreActivity? capturedActivity = null;
70 mockConversationClient
71 .Setup(c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()))
72 .Callback<CoreActivity, Dictionary<string, string>?, CancellationToken>((act, _, _) => capturedActivity = act)
73 .ReturnsAsync(new SendActivityResponse { Id = TestActivityId });
74
75 // Act
76 await compatConversations.SendToConversationWithHttpMessagesAsync(TestConversationId, activity);
77
78 // Assert
79 Assert.NotNull(capturedActivity);
80 Assert.NotNull(capturedActivity.ServiceUrl);
81 Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/'));
82 mockConversationClient.Verify(
83 c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()),
84 Times.Once);
85 }
86
87 [Fact]
88 public async Task ReplyToActivityWithHttpMessagesAsync_SetsServiceUrlFromProperty_WhenActivityServiceUrlIsNull()
89 {
90 // Arrange
91 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
92 CompatConversations compatConversations = new(mockConversationClient.Object)
93 {
94 ServiceUrl = TestServiceUrl
95 };
96
97 Activity activity = new()
98 {
99 Type = ActivityTypes.Message,
100 Text = "Test reply"
101 };
102
103 CoreActivity? capturedActivity = null;
104 mockConversationClient
105 .Setup(c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()))
106 .Callback<CoreActivity, Dictionary<string, string>?, CancellationToken>((act, _, _) => capturedActivity = act)
107 .ReturnsAsync(new SendActivityResponse { Id = TestActivityId });
108
109 // Act
110 await compatConversations.ReplyToActivityWithHttpMessagesAsync(TestConversationId, TestActivityId, activity);
111
112 // Assert
113 Assert.NotNull(capturedActivity);
114 Assert.NotNull(capturedActivity.ServiceUrl);
115 Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/'));
116 Assert.Equal(TestActivityId, capturedActivity.Properties["replyToId"]);
117 mockConversationClient.Verify(
118 c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()),
119 Times.Once);
120 }
121
122 [Fact]
123 public async Task ReplyToActivityWithHttpMessagesAsync_DoesNotOverrideServiceUrl_WhenActivityServiceUrlIsSet()
124 {
125 // Arrange
126 const string activityServiceUrl = "https://custom.service.url/";
127 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
128 CompatConversations compatConversations = new(mockConversationClient.Object)
129 {
130 ServiceUrl = TestServiceUrl
131 };
132
133 Activity activity = new()
134 {
135 Type = ActivityTypes.Message,
136 Text = "Test reply",
137 ServiceUrl = activityServiceUrl
138 };
139
140 CoreActivity? capturedActivity = null;
141 mockConversationClient
142 .Setup(c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()))
143 .Callback<CoreActivity, Dictionary<string, string>?, CancellationToken>((act, _, _) => capturedActivity = act)
144 .ReturnsAsync(new SendActivityResponse { Id = TestActivityId });
145
146 // Act
147 await compatConversations.ReplyToActivityWithHttpMessagesAsync(TestConversationId, TestActivityId, activity);
148
149 // Assert
150 Assert.NotNull(capturedActivity);
151 Assert.NotNull(capturedActivity.ServiceUrl);
152 Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/'));
153 mockConversationClient.Verify(
154 c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()),
155 Times.Once);
156 }
157
158 [Fact]
159 public async Task UpdateActivityWithHttpMessagesAsync_SetsServiceUrlFromProperty_WhenActivityServiceUrlIsNull()
160 {
161 // Arrange
162 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
163 CompatConversations compatConversations = new(mockConversationClient.Object)
164 {
165 ServiceUrl = TestServiceUrl
166 };
167
168 Activity activity = new()
169 {
170 Type = ActivityTypes.Message,
171 Text = "Updated message"
172 };
173
174 CoreActivity? capturedActivity = null;
175 mockConversationClient
176 .Setup(c => c.UpdateActivityAsync(
177 It.IsAny<string>(),
178 It.IsAny<string>(),
179 It.IsAny<CoreActivity>(),
180 It.IsAny<Dictionary<string, string>>(),
181 It.IsAny<CancellationToken>()))
182 .Callback<string, string, CoreActivity, Dictionary<string, string>?, CancellationToken>((_, _, act, _, _) => capturedActivity = act)
183 .ReturnsAsync(new UpdateActivityResponse { Id = TestActivityId });
184
185 // Act
186 await compatConversations.UpdateActivityWithHttpMessagesAsync(TestConversationId, TestActivityId, activity);
187
188 // Assert
189 Assert.NotNull(capturedActivity);
190 Assert.NotNull(capturedActivity.ServiceUrl);
191 Assert.Equal(TestServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/'));
192 mockConversationClient.Verify(
193 c => c.UpdateActivityAsync(
194 TestConversationId,
195 TestActivityId,
196 It.IsAny<CoreActivity>(),
197 It.IsAny<Dictionary<string, string>>(),
198 It.IsAny<CancellationToken>()),
199 Times.Once);
200 }
201
202 [Fact]
203 public async Task UpdateActivityWithHttpMessagesAsync_DoesNotOverrideServiceUrl_WhenActivityServiceUrlIsSet()
204 {
205 // Arrange
206 const string activityServiceUrl = "https://custom.service.url/";
207 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
208 CompatConversations compatConversations = new(mockConversationClient.Object)
209 {
210 ServiceUrl = TestServiceUrl
211 };
212
213 Activity activity = new()
214 {
215 Type = ActivityTypes.Message,
216 Text = "Updated message",
217 ServiceUrl = activityServiceUrl
218 };
219
220 CoreActivity? capturedActivity = null;
221 mockConversationClient
222 .Setup(c => c.UpdateActivityAsync(
223 It.IsAny<string>(),
224 It.IsAny<string>(),
225 It.IsAny<CoreActivity>(),
226 It.IsAny<Dictionary<string, string>>(),
227 It.IsAny<CancellationToken>()))
228 .Callback<string, string, CoreActivity, Dictionary<string, string>?, CancellationToken>((_, _, act, _, _) => capturedActivity = act)
229 .ReturnsAsync(new UpdateActivityResponse { Id = TestActivityId });
230
231 // Act
232 await compatConversations.UpdateActivityWithHttpMessagesAsync(TestConversationId, TestActivityId, activity);
233
234 // Assert
235 Assert.NotNull(capturedActivity);
236 Assert.NotNull(capturedActivity.ServiceUrl);
237 Assert.Equal(activityServiceUrl.TrimEnd('/'), capturedActivity.ServiceUrl.ToString().TrimEnd('/'));
238 mockConversationClient.Verify(
239 c => c.UpdateActivityAsync(
240 TestConversationId,
241 TestActivityId,
242 It.IsAny<CoreActivity>(),
243 It.IsAny<Dictionary<string, string>>(),
244 It.IsAny<CancellationToken>()),
245 Times.Once);
246 }
247
248 [Fact]
249 public async Task SendToConversationWithHttpMessagesAsync_EnsuresConversationIdIsSet()
250 {
251 // Arrange
252 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
253 CompatConversations compatConversations = new(mockConversationClient.Object)
254 {
255 ServiceUrl = TestServiceUrl
256 };
257
258 Activity activity = new()
259 {
260 Type = ActivityTypes.Message,
261 Text = "Test message"
262 };
263
264 CoreActivity? capturedActivity = null;
265 mockConversationClient
266 .Setup(c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()))
267 .Callback<CoreActivity, Dictionary<string, string>?, CancellationToken>((act, _, _) => capturedActivity = act)
268 .ReturnsAsync(new SendActivityResponse { Id = TestActivityId });
269
270 // Act
271 await compatConversations.SendToConversationWithHttpMessagesAsync(TestConversationId, activity);
272
273 // Assert
274 Assert.NotNull(capturedActivity);
275 Assert.NotNull(capturedActivity.Conversation);
276 Assert.Equal(TestConversationId, capturedActivity.Conversation.Id);
277 }
278
279 [Fact]
280 public async Task ReplyToActivityWithHttpMessagesAsync_SetsReplyToIdProperty()
281 {
282 // Arrange
283 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
284 CompatConversations compatConversations = new(mockConversationClient.Object)
285 {
286 ServiceUrl = TestServiceUrl
287 };
288
289 Activity activity = new()
290 {
291 Type = ActivityTypes.Message,
292 Text = "Test reply"
293 };
294
295 CoreActivity? capturedActivity = null;
296 mockConversationClient
297 .Setup(c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()))
298 .Callback<CoreActivity, Dictionary<string, string>?, CancellationToken>((act, _, _) => capturedActivity = act)
299 .ReturnsAsync(new SendActivityResponse { Id = TestActivityId });
300
301 // Act
302 await compatConversations.ReplyToActivityWithHttpMessagesAsync(TestConversationId, "parent-activity-id", activity);
303
304 // Assert
305 Assert.NotNull(capturedActivity);
306 Assert.True(capturedActivity.Properties.ContainsKey("replyToId"));
307 Assert.Equal("parent-activity-id", capturedActivity.Properties["replyToId"]);
308 Assert.NotNull(capturedActivity.Conversation);
309 Assert.Equal(TestConversationId, capturedActivity.Conversation.Id);
310 }
311
312 [Fact]
313 public async Task SendToConversationWithHttpMessagesAsync_WithNullServiceUrl_ThrowsArgumentNullException()
314 {
315 // This test reproduces the ProjectAgent scenario where ServiceUrl might not be set
316 // Arrange
317 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
318
319 // Make the mock behave like the real ConversationClient - validate ServiceUrl
320 mockConversationClient
321 .Setup(c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()))
322 .Callback<CoreActivity, Dictionary<string, string>?, CancellationToken>((act, _, _) =>
323 {
324 // Mimic the real ConversationClient.SendActivityAsync validation (lines 46-49)
325 ArgumentNullException.ThrowIfNull(act);
326 ArgumentNullException.ThrowIfNull(act.Conversation);
327 ArgumentException.ThrowIfNullOrWhiteSpace(act.Conversation.Id);
328 ArgumentNullException.ThrowIfNull(act.ServiceUrl); // This should throw!
329 })
330 .ReturnsAsync(new SendActivityResponse { Id = TestActivityId });
331
332 CompatConversations compatConversations = new(mockConversationClient.Object);
333 // NOTE: ServiceUrl is NOT set on compatConversations
334
335 Activity activity = new()
336 {
337 Type = ActivityTypes.Message,
338 Attachments = new List<Attachment>
339 {
340 new()
341 {
342 ContentType = "application/vnd.microsoft.card.oauth",
343 Content = new { buttons = new[] { new { type = "signin" } } }
344 }
345 },
346 Recipient = new ChannelAccount { Id = "user-123", Name = "Test User" },
347 Conversation = new Microsoft.Bot.Schema.ConversationAccount { Id = TestConversationId }
348 // NOTE: ServiceUrl is also NOT set on activity
349 };
350
351 // Act & Assert
352 // ConversationClient.SendActivityAsync should throw ArgumentNullException when it validates activity.ServiceUrl
353 var exception = await Assert.ThrowsAsync<ArgumentNullException>(async () =>
354 await compatConversations.SendToConversationWithHttpMessagesAsync(TestConversationId, activity)
355 );
356
357 // Verify it's about the ServiceUrl
358 Console.WriteLine($"Exception message: {exception.Message}");
359 Console.WriteLine($"Parameter name: {exception.ParamName}");
360 }
361
362 [Fact]
363 public async Task SendToConversationWithHttpMessagesAsync_WithNullConversationId_DoesNotThrow()
364 {
365 // Test what happens if conversationId is null (even though user said it's not)
366 // Arrange
367 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
368 mockConversationClient
369 .Setup(c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()))
370 .Callback<CoreActivity, Dictionary<string, string>?, CancellationToken>((act, _, _) =>
371 {
372 Console.WriteLine($"Conversation: {act.Conversation}");
373 Console.WriteLine($"Conversation.Id: {act.Conversation?.Id ?? "NULL"}");
374 })
375 .ReturnsAsync(new SendActivityResponse { Id = TestActivityId });
376
377 CompatConversations compatConversations = new(mockConversationClient.Object)
378 {
379 ServiceUrl = TestServiceUrl
380 };
381
382 Activity activity = new()
383 {
384 Type = ActivityTypes.Message,
385 Text = "Test"
386 };
387
388 // Act - pass null as conversationId
389 await compatConversations.SendToConversationWithHttpMessagesAsync(null!, activity);
390
391 // This should succeed - the null conversationId gets assigned to Conversation.Id
392 Assert.True(true);
393 }
394
395 [Fact]
396 public async Task SendToConversationWithHttpMessagesAsync_WhenSendActivityReturnsNull_ThrowsNullReferenceException()
397 {
398 // CRITICAL TEST: If APX returns null response, this should reproduce the NullReferenceException!
399 // Arrange
400 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
401 mockConversationClient
402 .Setup(c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()))
403 .ReturnsAsync((SendActivityResponse?)null!); // Return null despite non-nullable return type
404
405 CompatConversations compatConversations = new(mockConversationClient.Object)
406 {
407 ServiceUrl = TestServiceUrl
408 };
409
410 Activity activity = new()
411 {
412 Type = ActivityTypes.Message,
413 Text = "Test"
414 };
415
416 // Act & Assert
417 // This should throw NullReferenceException when trying to access response.Id on line 324
418 var exception = await Assert.ThrowsAsync<NullReferenceException>(async () =>
419 await compatConversations.SendToConversationWithHttpMessagesAsync(TestConversationId, activity)
420 );
421
422 Console.WriteLine($"SUCCESS! Reproduced NullReferenceException: {exception.Message}");
423 }
424
425 [Fact]
426 public async Task SendToConversationWithHttpMessagesAsync_WhenResponseIdIsNull_Succeeds()
427 {
428 // Test if response.Id being null causes issues (it shouldn't - Id is nullable)
429 // Arrange
430 Mock<ConversationClient> mockConversationClient = CreateMockConversationClient();
431 mockConversationClient
432 .Setup(c => c.SendActivityAsync(It.IsAny<CoreActivity>(), It.IsAny<Dictionary<string, string>>(), It.IsAny<CancellationToken>()))
433 .ReturnsAsync(new SendActivityResponse { Id = null }); // Id is null
434
435 CompatConversations compatConversations = new(mockConversationClient.Object)
436 {
437 ServiceUrl = TestServiceUrl
438 };
439
440 Activity activity = new()
441 {
442 Type = ActivityTypes.Message,
443 Text = "Test"
444 };
445
446 // Act
447 var result = await compatConversations.SendToConversationWithHttpMessagesAsync(TestConversationId, activity);
448
449 // Assert - Should succeed, Id will just be null
450 Assert.NotNull(result);
451 Assert.NotNull(result.Body);
452 Assert.Null(result.Body.Id);
453 }
454
455 private static Mock<ConversationClient> CreateMockConversationClient()
456 {
457 Mock<ConversationClient> mock = new(
458 Mock.Of<HttpClient>(),
459 null!);
460
461 return mock;
462 }
463 }
464}
465