microsoft/teams.net

Public

mirrored from https://github.com/microsoft/teams.netAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
cg/sovereign-cloud-nextcore

Branches

Tags

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

Clone

HTTPS

Download ZIP

core/test/Microsoft.Teams.Bot.Core.UnitTests/Hosting/AddBotApplicationExtensionsTests.cs

572lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4using Microsoft.Extensions.Configuration;
5using Microsoft.Extensions.DependencyInjection;
6using Microsoft.Extensions.Options;
7using Microsoft.Identity.Abstractions;
8using Microsoft.Teams.Bot.Core.Hosting;
9
10namespace Microsoft.Teams.Bot.Core.UnitTests.Hosting;
11
12public class AddBotApplicationExtensionsTests
13{
14 private static ServiceProvider BuildServiceProvider(Dictionary<string, string?> configData, string? aadConfigSectionName = null)
15 {
16 IConfigurationRoot configuration = new ConfigurationBuilder()
17 .AddInMemoryCollection(configData)
18 .Build();
19
20 ServiceCollection services = new();
21 services.AddSingleton<IConfiguration>(configuration);
22 services.AddLogging();
23
24 if (aadConfigSectionName is null)
25 {
26 services.AddConversationClient();
27 }
28 else
29 {
30 services.AddConversationClient(aadConfigSectionName);
31 }
32
33 return services.BuildServiceProvider();
34 }
35
36 private static void AssertMsalOptions(ServiceProvider serviceProvider, string expectedClientId, string expectedTenantId, string expectedInstance = "https://login.microsoftonline.com/")
37 {
38 MicrosoftIdentityApplicationOptions msalOptions = serviceProvider
39 .GetRequiredService<IOptionsMonitor<MicrosoftIdentityApplicationOptions>>()
40 .Get(MsalConfigurationExtensions.MsalConfigKey);
41 Assert.Equal(expectedClientId, msalOptions.ClientId);
42 Assert.Equal(expectedTenantId, msalOptions.TenantId);
43 Assert.Equal(expectedInstance, msalOptions.Instance);
44 }
45
46 [Fact]
47 public void AddConversationClient_WithBotFrameworkConfig_ConfiguresClientSecret()
48 {
49 // Arrange
50 Dictionary<string, string?> configData = new()
51 {
52 ["MicrosoftAppId"] = "test-app-id",
53 ["MicrosoftAppTenantId"] = "test-tenant-id",
54 ["MicrosoftAppPassword"] = "test-secret"
55 };
56
57 // Act
58 ServiceProvider serviceProvider = BuildServiceProvider(configData);
59
60 // Assert
61 AssertMsalOptions(serviceProvider, "test-app-id", "test-tenant-id");
62 MicrosoftIdentityApplicationOptions msalOptions = serviceProvider
63 .GetRequiredService<IOptionsMonitor<MicrosoftIdentityApplicationOptions>>()
64 .Get(MsalConfigurationExtensions.MsalConfigKey);
65 Assert.NotNull(msalOptions.ClientCredentials);
66 Assert.Single(msalOptions.ClientCredentials);
67 CredentialDescription credential = msalOptions.ClientCredentials.First();
68 Assert.Equal(CredentialSource.ClientSecret, credential.SourceType);
69 Assert.Equal("test-secret", credential.ClientSecret);
70 }
71
72 [Fact]
73 public void AddConversationClient_WithCoreConfigAndClientSecret_ConfiguresClientSecret()
74 {
75 // Arrange
76 Dictionary<string, string?> configData = new()
77 {
78 ["CLIENT_ID"] = "test-client-id",
79 ["TENANT_ID"] = "test-tenant-id",
80 ["CLIENT_SECRET"] = "test-client-secret"
81 };
82
83 // Act
84 ServiceProvider serviceProvider = BuildServiceProvider(configData);
85
86 // Assert
87 AssertMsalOptions(serviceProvider, "test-client-id", "test-tenant-id");
88 MicrosoftIdentityApplicationOptions msalOptions = serviceProvider
89 .GetRequiredService<IOptionsMonitor<MicrosoftIdentityApplicationOptions>>()
90 .Get(MsalConfigurationExtensions.MsalConfigKey);
91 Assert.NotNull(msalOptions.ClientCredentials);
92 Assert.Single(msalOptions.ClientCredentials);
93 CredentialDescription credential = msalOptions.ClientCredentials.First();
94 Assert.Equal(CredentialSource.ClientSecret, credential.SourceType);
95 Assert.Equal("test-client-secret", credential.ClientSecret);
96 }
97
98 [Fact]
99 public void AddConversationClient_WithCoreConfigAndSystemAssignedMI_ConfiguresSystemAssignedFIC()
100 {
101 // Arrange
102 Dictionary<string, string?> configData = new()
103 {
104 ["CLIENT_ID"] = "test-client-id",
105 ["TENANT_ID"] = "test-tenant-id",
106 ["MANAGED_IDENTITY_CLIENT_ID"] = "system"
107 };
108
109 // Act
110 ServiceProvider serviceProvider = BuildServiceProvider(configData);
111
112 // Assert
113 AssertMsalOptions(serviceProvider, "test-client-id", "test-tenant-id");
114 MicrosoftIdentityApplicationOptions msalOptions = serviceProvider
115 .GetRequiredService<IOptionsMonitor<MicrosoftIdentityApplicationOptions>>()
116 .Get(MsalConfigurationExtensions.MsalConfigKey);
117 Assert.NotNull(msalOptions.ClientCredentials);
118 Assert.Single(msalOptions.ClientCredentials);
119 CredentialDescription credential = msalOptions.ClientCredentials.First();
120 Assert.Equal(CredentialSource.SignedAssertionFromManagedIdentity, credential.SourceType);
121 Assert.Null(credential.ManagedIdentityClientId); // System-assigned
122
123 ManagedIdentityOptions managedIdentityOptions = serviceProvider.GetRequiredService<IOptions<ManagedIdentityOptions>>().Value;
124 Assert.Null(managedIdentityOptions.UserAssignedClientId);
125 }
126
127 [Fact]
128 public void AddConversationClient_WithCoreConfigAndUserAssignedMI_ConfiguresUserAssignedFIC()
129 {
130 // Arrange
131 Dictionary<string, string?> configData = new()
132 {
133 ["CLIENT_ID"] = "test-client-id",
134 ["TENANT_ID"] = "test-tenant-id",
135 ["MANAGED_IDENTITY_CLIENT_ID"] = "umi-client-id" // Different from CLIENT_ID means FIC
136 };
137
138 // Act
139 ServiceProvider serviceProvider = BuildServiceProvider(configData);
140
141 // Assert
142 AssertMsalOptions(serviceProvider, "test-client-id", "test-tenant-id");
143 MicrosoftIdentityApplicationOptions msalOptions = serviceProvider
144 .GetRequiredService<IOptionsMonitor<MicrosoftIdentityApplicationOptions>>()
145 .Get(MsalConfigurationExtensions.MsalConfigKey);
146 Assert.NotNull(msalOptions.ClientCredentials);
147 Assert.Single(msalOptions.ClientCredentials);
148 CredentialDescription credential = msalOptions.ClientCredentials.First();
149 Assert.Equal(CredentialSource.SignedAssertionFromManagedIdentity, credential.SourceType);
150 Assert.Equal("umi-client-id", credential.ManagedIdentityClientId);
151
152 ManagedIdentityOptions managedIdentityOptions = serviceProvider.GetRequiredService<IOptions<ManagedIdentityOptions>>().Value;
153 Assert.Null(managedIdentityOptions.UserAssignedClientId);
154 }
155
156 [Fact]
157 public void AddConversationClient_WithCoreConfigAndNoManagedIdentity_ConfiguresUMIWithClientId()
158 {
159 // Arrange
160 Dictionary<string, string?> configData = new()
161 {
162 ["CLIENT_ID"] = "test-client-id",
163 ["TENANT_ID"] = "test-tenant-id"
164 };
165
166 // Act
167 ServiceProvider serviceProvider = BuildServiceProvider(configData);
168
169 // Assert
170 AssertMsalOptions(serviceProvider, "test-client-id", "test-tenant-id");
171 MicrosoftIdentityApplicationOptions msalOptions = serviceProvider
172 .GetRequiredService<IOptionsMonitor<MicrosoftIdentityApplicationOptions>>()
173 .Get(MsalConfigurationExtensions.MsalConfigKey);
174 Assert.Null(msalOptions.ClientCredentials);
175
176 ManagedIdentityOptions managedIdentityOptions = serviceProvider.GetRequiredService<IOptions<ManagedIdentityOptions>>().Value;
177 Assert.Equal("test-client-id", managedIdentityOptions.UserAssignedClientId);
178 }
179
180 [Fact]
181 public void AddConversationClient_WithDefaultSection_ConfiguresFromSection()
182 {
183 // AzureAd is the default Section Name
184 // Arrange
185 Dictionary<string, string?> configData = new()
186 {
187 ["AzureAd:ClientId"] = "azuread-client-id",
188 ["AzureAd:TenantId"] = "azuread-tenant-id",
189 ["AzureAd:Instance"] = "https://login.microsoftonline.com/"
190 };
191
192 // Act
193 ServiceProvider serviceProvider = BuildServiceProvider(configData);
194
195 // Assert
196 AssertMsalOptions(serviceProvider, "azuread-client-id", "azuread-tenant-id");
197 }
198
199 [Fact]
200 public void AddConversationClient_WithCustomSectionName_ConfiguresFromCustomSection()
201 {
202 // Arrange
203 Dictionary<string, string?> configData = new()
204 {
205 ["CustomAuth:ClientId"] = "custom-client-id",
206 ["CustomAuth:TenantId"] = "custom-tenant-id",
207 ["CustomAuth:Instance"] = "https://login.microsoftonline.com/"
208 };
209
210 // Act
211 ServiceProvider serviceProvider = BuildServiceProvider(configData, "CustomAuth");
212
213 // Assert
214 AssertMsalOptions(serviceProvider, "custom-client-id", "custom-tenant-id");
215 }
216
217 // --- BotApplicationOptions (AppId) tests ---
218
219 private static ServiceProvider BuildServiceProviderForBotApp(Dictionary<string, string?> configData, string? sectionName = null)
220 {
221 IConfigurationRoot configuration = new ConfigurationBuilder()
222 .AddInMemoryCollection(configData)
223 .Build();
224
225 ServiceCollection services = new();
226 services.AddSingleton<IConfiguration>(configuration);
227 services.AddLogging();
228
229 if (sectionName is null)
230 services.AddBotApplication();
231 else
232 services.AddBotApplication(sectionName);
233
234 return services.BuildServiceProvider();
235 }
236
237 private static string GetAppId(ServiceProvider serviceProvider) =>
238 serviceProvider.GetRequiredService<BotApplicationOptions>().AppId;
239
240 [Fact]
241 public void AddBotApplication_WithMicrosoftAppId_SetsAppIdFromMicrosoftAppId()
242 {
243 // Arrange
244 Dictionary<string, string?> configData = new()
245 {
246 ["MicrosoftAppId"] = "bf-app-id",
247 ["MicrosoftAppTenantId"] = "bf-tenant-id"
248 };
249
250 // Act
251 ServiceProvider serviceProvider = BuildServiceProviderForBotApp(configData);
252
253 // Assert
254 Assert.Equal("bf-app-id", GetAppId(serviceProvider));
255 }
256
257 [Fact]
258 public void AddBotApplication_WithClientId_SetsAppIdFromClientId()
259 {
260 // Arrange
261 Dictionary<string, string?> configData = new()
262 {
263 ["CLIENT_ID"] = "core-client-id",
264 ["TENANT_ID"] = "core-tenant-id"
265 };
266
267 // Act
268 ServiceProvider serviceProvider = BuildServiceProviderForBotApp(configData);
269
270 // Assert
271 Assert.Equal("core-client-id", GetAppId(serviceProvider));
272 }
273
274 [Fact]
275 public void AddBotApplication_WithAzureAdSection_SetsAppIdFromSection()
276 {
277 // Arrange
278 Dictionary<string, string?> configData = new()
279 {
280 ["AzureAd:ClientId"] = "azuread-client-id",
281 ["AzureAd:TenantId"] = "azuread-tenant-id"
282 };
283
284 // Act
285 ServiceProvider serviceProvider = BuildServiceProviderForBotApp(configData);
286
287 // Assert
288 Assert.Equal("azuread-client-id", GetAppId(serviceProvider));
289 }
290
291 [Fact]
292 public void AddBotApplication_WithCustomSection_SetsAppIdFromCustomSection()
293 {
294 // Arrange
295 Dictionary<string, string?> configData = new()
296 {
297 ["CustomAuth:ClientId"] = "custom-client-id",
298 ["CustomAuth:TenantId"] = "custom-tenant-id"
299 };
300
301 // Act
302 ServiceProvider serviceProvider = BuildServiceProviderForBotApp(configData, "CustomAuth");
303
304 // Assert
305 Assert.Equal("custom-client-id", GetAppId(serviceProvider));
306 }
307
308 [Fact]
309 public void AddBotApplication_ClientIdTakesPrecedenceOverMicrosoftAppId()
310 {
311 // Arrange — both keys present; CLIENT_ID is highest priority
312 Dictionary<string, string?> configData = new()
313 {
314 ["MicrosoftAppId"] = "bf-app-id",
315 ["MicrosoftAppTenantId"] = "bf-tenant-id",
316 ["CLIENT_ID"] = "core-client-id",
317 ["TENANT_ID"] = "core-tenant-id"
318 };
319
320 // Act
321 ServiceProvider serviceProvider = BuildServiceProviderForBotApp(configData);
322
323 // Assert
324 Assert.Equal("core-client-id", GetAppId(serviceProvider));
325 }
326
327 // --- MSAL Instance / Authority validation + Cloud fallback ---
328
329 [Fact]
330 public void AddConversationClient_SectionInstanceMatchesCloud_Succeeds()
331 {
332 Dictionary<string, string?> configData = new()
333 {
334 ["AzureAd:ClientId"] = "cid",
335 ["AzureAd:TenantId"] = "tid",
336 ["AzureAd:Cloud"] = "USGov",
337 ["AzureAd:Instance"] = "https://login.microsoftonline.us/"
338 };
339
340 ServiceProvider serviceProvider = BuildServiceProvider(configData);
341
342 AssertMsalOptions(serviceProvider, "cid", "tid", "https://login.microsoftonline.us/");
343 }
344
345 [Fact]
346 public void AddConversationClient_SectionInstanceConflictsWithCloud_Throws()
347 {
348 Dictionary<string, string?> configData = new()
349 {
350 ["AzureAd:ClientId"] = "cid",
351 ["AzureAd:TenantId"] = "tid",
352 ["AzureAd:Cloud"] = "USGov",
353 ["AzureAd:Instance"] = "https://login.microsoftonline.com/"
354 };
355
356 InvalidOperationException ex = Assert.Throws<InvalidOperationException>(
357 () => BuildServiceProvider(configData));
358 Assert.Contains("AzureAd:Instance", ex.Message);
359 Assert.Contains("https://login.microsoftonline.com/", ex.Message);
360 Assert.Contains("https://login.microsoftonline.us", ex.Message);
361 }
362
363 [Fact]
364 public void AddConversationClient_SectionInstanceMissing_FallsBackToCloud()
365 {
366 Dictionary<string, string?> configData = new()
367 {
368 ["AzureAd:ClientId"] = "cid",
369 ["AzureAd:TenantId"] = "tid",
370 ["AzureAd:Cloud"] = "USGov"
371 };
372
373 ServiceProvider serviceProvider = BuildServiceProvider(configData);
374
375 AssertMsalOptions(serviceProvider, "cid", "tid", "https://login.microsoftonline.us/");
376 }
377
378 [Fact]
379 public void AddConversationClient_SectionAuthorityMatchesCloud_Succeeds()
380 {
381 Dictionary<string, string?> configData = new()
382 {
383 ["AzureAd:ClientId"] = "cid",
384 ["AzureAd:TenantId"] = "tid",
385 ["AzureAd:Cloud"] = "USGov",
386 ["AzureAd:Authority"] = "https://login.microsoftonline.us/tid/v2.0"
387 };
388
389 ServiceProvider serviceProvider = BuildServiceProvider(configData);
390
391 MicrosoftIdentityApplicationOptions msalOptions = serviceProvider
392 .GetRequiredService<IOptionsMonitor<MicrosoftIdentityApplicationOptions>>()
393 .Get(MsalConfigurationExtensions.MsalConfigKey);
394 Assert.Equal("cid", msalOptions.ClientId);
395 }
396
397 [Fact]
398 public void AddConversationClient_SectionAuthorityConflictsWithCloud_Throws()
399 {
400 Dictionary<string, string?> configData = new()
401 {
402 ["AzureAd:ClientId"] = "cid",
403 ["AzureAd:TenantId"] = "tid",
404 ["AzureAd:Cloud"] = "USGov",
405 ["AzureAd:Authority"] = "https://login.microsoftonline.com/tid/v2.0"
406 };
407
408 InvalidOperationException ex = Assert.Throws<InvalidOperationException>(
409 () => BuildServiceProvider(configData));
410 Assert.Contains("AzureAd:Authority", ex.Message);
411 Assert.Contains("https://login.microsoftonline.com/tid/v2.0", ex.Message);
412 }
413
414 [Fact]
415 public void AddConversationClient_DefaultCloud_InstanceDefaultsToPublic()
416 {
417 Dictionary<string, string?> configData = new()
418 {
419 ["AzureAd:ClientId"] = "cid",
420 ["AzureAd:TenantId"] = "tid"
421 };
422
423 ServiceProvider serviceProvider = BuildServiceProvider(configData);
424
425 AssertMsalOptions(serviceProvider, "cid", "tid", "https://login.microsoftonline.com/");
426 }
427
428 [Fact]
429 public void AddConversationClient_RawCredentials_ConfiguresCloudAwareInstance()
430 {
431 // Core-config path (no MSAL section) should set MSAL Instance from Cloud.
432 Dictionary<string, string?> configData = new()
433 {
434 ["CLIENT_ID"] = "cid",
435 ["TENANT_ID"] = "tid",
436 ["CLIENT_SECRET"] = "secret",
437 ["CLOUD"] = "USGov"
438 };
439
440 ServiceProvider serviceProvider = BuildServiceProvider(configData);
441
442 AssertMsalOptions(serviceProvider, "cid", "tid", "https://login.microsoftonline.us/");
443 }
444
445 // --- Per-endpoint override binding (BotConfig.ResolveCloud) ---
446
447 [Fact]
448 public void BotConfig_FromMsalConfig_AppliesTokenIssuerOverrideFromSection()
449 {
450 IConfiguration config = new ConfigurationBuilder()
451 .AddInMemoryCollection(new Dictionary<string, string?>
452 {
453 ["AzureAd:ClientId"] = "cid",
454 ["AzureAd:Cloud"] = "USGov",
455 ["AzureAd:TokenIssuer"] = "https://custom.issuer"
456 })
457 .Build();
458
459 BotConfig result = BotConfig.FromMsalConfig(config);
460
461 Assert.Equal("https://custom.issuer", result.Cloud.TokenIssuer);
462 Assert.Equal(CloudEnvironment.USGov.LoginEndpoint, result.Cloud.LoginEndpoint);
463 }
464
465 [Fact]
466 public void BotConfig_FromMsalConfig_AppliesMultipleEndpointOverrides()
467 {
468 IConfiguration config = new ConfigurationBuilder()
469 .AddInMemoryCollection(new Dictionary<string, string?>
470 {
471 ["AzureAd:ClientId"] = "cid",
472 ["AzureAd:Cloud"] = "USGov",
473 ["AzureAd:TokenIssuer"] = "https://iss",
474 ["AzureAd:TokenServiceUrl"] = "https://tsu",
475 ["AzureAd:GraphScope"] = "https://graph.override/.default"
476 })
477 .Build();
478
479 BotConfig result = BotConfig.FromMsalConfig(config);
480
481 Assert.Equal("https://iss", result.Cloud.TokenIssuer);
482 Assert.Equal("https://tsu", result.Cloud.TokenServiceUrl);
483 Assert.Equal("https://graph.override/.default", result.Cloud.GraphScope);
484 }
485
486 [Fact]
487 public void BotConfig_FromMsalConfig_SectionOverrideWinsOverRoot()
488 {
489 IConfiguration config = new ConfigurationBuilder()
490 .AddInMemoryCollection(new Dictionary<string, string?>
491 {
492 ["AzureAd:ClientId"] = "cid",
493 ["AzureAd:Cloud"] = "USGov",
494 ["AzureAd:TokenIssuer"] = "https://section",
495 ["TokenIssuer"] = "https://root"
496 })
497 .Build();
498
499 BotConfig result = BotConfig.FromMsalConfig(config);
500
501 Assert.Equal("https://section", result.Cloud.TokenIssuer);
502 }
503
504 [Fact]
505 public void BotConfig_FromMsalConfig_FallsBackToRootOverride()
506 {
507 IConfiguration config = new ConfigurationBuilder()
508 .AddInMemoryCollection(new Dictionary<string, string?>
509 {
510 ["AzureAd:ClientId"] = "cid",
511 ["AzureAd:Cloud"] = "USGov",
512 ["TokenIssuer"] = "https://root"
513 })
514 .Build();
515
516 BotConfig result = BotConfig.FromMsalConfig(config);
517
518 Assert.Equal("https://root", result.Cloud.TokenIssuer);
519 }
520
521 [Fact]
522 public void BotConfig_FromMsalConfig_WhitespaceOverrideIgnored()
523 {
524 IConfiguration config = new ConfigurationBuilder()
525 .AddInMemoryCollection(new Dictionary<string, string?>
526 {
527 ["AzureAd:ClientId"] = "cid",
528 ["AzureAd:Cloud"] = "USGov",
529 ["AzureAd:TokenIssuer"] = " "
530 })
531 .Build();
532
533 BotConfig result = BotConfig.FromMsalConfig(config);
534
535 Assert.Equal(CloudEnvironment.USGov.TokenIssuer, result.Cloud.TokenIssuer);
536 }
537
538 [Fact]
539 public void BotConfig_FromBFConfig_AppliesEndpointOverrides()
540 {
541 IConfiguration config = new ConfigurationBuilder()
542 .AddInMemoryCollection(new Dictionary<string, string?>
543 {
544 ["MicrosoftAppId"] = "cid",
545 ["Cloud"] = "USGov",
546 ["TokenIssuer"] = "https://bf-custom"
547 })
548 .Build();
549
550 BotConfig result = BotConfig.FromBFConfig(config);
551
552 Assert.Equal("https://bf-custom", result.Cloud.TokenIssuer);
553 }
554
555 [Fact]
556 public void BotConfig_FromCoreConfig_AppliesEndpointOverrides()
557 {
558 IConfiguration config = new ConfigurationBuilder()
559 .AddInMemoryCollection(new Dictionary<string, string?>
560 {
561 ["CLIENT_ID"] = "cid",
562 ["CLOUD"] = "China",
563 ["TokenIssuer"] = "https://core-custom"
564 })
565 .Build();
566
567 BotConfig result = BotConfig.FromCoreConfig(config);
568
569 Assert.Equal("https://core-custom", result.Cloud.TokenIssuer);
570 Assert.Equal(CloudEnvironment.China.LoginEndpoint, result.Cloud.LoginEndpoint);
571 }
572}
573