-
Notifications
You must be signed in to change notification settings - Fork 185
Commit
## Linked issues closes: #1053 #1110 ## Details 1. Create an adapter for MSAL library to make the classes testable with mock 2. Update existing class implementation to use the MSAL library adapter 3. Do not check token cache in TeamsSsoPrompt because the Authentication handler already checks token cache before invoking the prompt 4. Add unit tests ## Attestation Checklist - [x] My code follows the style guidelines of this project - I have checked for/fixed spelling, linting, and other errors - I have commented my code for clarity - I have made corresponding changes to the documentation (we use [TypeDoc](https://typedoc.org/) to document our code) - My changes generate no new warnings - I have added tests that validates my changes, and provides sufficient test coverage. I have tested with: - Local testing - E2E testing in Teams - New and existing unit tests pass locally with my changes --------- Co-authored-by: Alex Acebo <[email protected]>
- Loading branch information
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
using Microsoft.Identity.Client; | ||
using System.Security.Cryptography.X509Certificates; | ||
|
||
namespace Microsoft.Teams.AI.Tests.Application.Authentication | ||
{ | ||
internal class AppConfig : IAppConfig | ||
Check warning on line 6 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/AppConfig.cs GitHub Actions / Analyze
Check warning on line 6 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/AppConfig.cs GitHub Actions / Build/Test/Lint / Build/Test/Lint (7.0)
Check warning on line 6 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/AppConfig.cs GitHub Actions / Build/Test/Lint / Build/Test/Lint (6.0)
Check warning on line 6 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/AppConfig.cs GitHub Actions / Build/Test/Lint (7.0)
Check warning on line 6 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/AppConfig.cs GitHub Actions / Build/Test/Lint (7.0)
Check warning on line 6 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/AppConfig.cs GitHub Actions / Build/Test/Lint (6.0)
|
||
{ | ||
#pragma warning disable CS8618 // This class is for test purpose only | ||
public AppConfig(string clientId, string tenantId) | ||
#pragma warning restore CS8618 | ||
{ | ||
ClientId = clientId; | ||
TenantId = tenantId; | ||
} | ||
|
||
public string ClientId { get; } | ||
|
||
public bool EnablePiiLogging { get; } | ||
|
||
public IMsalHttpClientFactory HttpClientFactory { get; } | ||
|
||
public LogLevel LogLevel { get; } | ||
|
||
public bool IsDefaultPlatformLoggingEnabled { get; } | ||
|
||
public string RedirectUri { get; } | ||
|
||
public string TenantId { get; } | ||
|
||
public LogCallback LoggingCallback { get; } | ||
|
||
public IDictionary<string, string> ExtraQueryParameters { get; } | ||
|
||
public bool IsBrokerEnabled { get; } | ||
|
||
public string ClientName { get; } | ||
|
||
public string ClientVersion { get; } | ||
|
||
[Obsolete] | ||
public ITelemetryConfig TelemetryConfig { get; } | ||
|
||
public bool ExperimentalFeaturesEnabled { get; } | ||
|
||
public IEnumerable<string> ClientCapabilities { get; } | ||
|
||
public bool LegacyCacheCompatibilityEnabled { get; } | ||
|
||
public string ClientSecret { get; } | ||
|
||
public X509Certificate2 ClientCredentialCertificate { get; } | ||
|
||
public Func<object> ParentActivityOrWindowFunc { get; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,204 @@ | ||
using Microsoft.Bot.Builder; | ||
using Microsoft.Bot.Builder.Dialogs; | ||
using Microsoft.Bot.Schema; | ||
using Microsoft.Identity.Client; | ||
using Microsoft.Teams.AI.State; | ||
using Microsoft.Teams.AI.Tests.TestUtils; | ||
using Moq; | ||
using Newtonsoft.Json.Linq; | ||
|
||
namespace Microsoft.Teams.AI.Tests.Application.Authentication.Bot | ||
{ | ||
public class TeamsSsoBotAuthenticationTests | ||
{ | ||
internal class MockTeamsSsoBotAuthentication<TState> : TeamsSsoBotAuthentication<TState> | ||
Check warning on line 14 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/TeamsSsoBotAuthenticationTests.cs GitHub Actions / Analyze
Check warning on line 14 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/TeamsSsoBotAuthenticationTests.cs GitHub Actions / Build/Test/Lint / Build/Test/Lint (7.0)
Check warning on line 14 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/TeamsSsoBotAuthenticationTests.cs GitHub Actions / Build/Test/Lint / Build/Test/Lint (6.0)
Check warning on line 14 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/TeamsSsoBotAuthenticationTests.cs GitHub Actions / Build/Test/Lint / Build/Test/Lint (7.0)
Check warning on line 14 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/TeamsSsoBotAuthenticationTests.cs GitHub Actions / Build/Test/Lint / Build/Test/Lint (6.0)
Check warning on line 14 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/TeamsSsoBotAuthenticationTests.cs GitHub Actions / Build/Test/Lint (7.0)
Check warning on line 14 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/TeamsSsoBotAuthenticationTests.cs GitHub Actions / Build/Test/Lint (7.0)
Check warning on line 14 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/TeamsSsoBotAuthenticationTests.cs GitHub Actions / Build/Test/Lint (6.0)
Check warning on line 14 in dotnet/packages/Microsoft.TeamsAI/Microsoft.TeamsAI.Tests/Application/Authentication/Bot/TeamsSsoBotAuthenticationTests.cs GitHub Actions / Build/Test/Lint (6.0)
|
||
where TState : TurnState, new() | ||
{ | ||
public MockTeamsSsoBotAuthentication(Application<TState> app, string name, TeamsSsoSettings settings, TeamsSsoPrompt? mockPrompt = null) : base(app, name, settings, null) | ||
{ | ||
if (mockPrompt != null) | ||
{ | ||
_prompt = mockPrompt; | ||
} | ||
} | ||
|
||
public async Task<bool> TokenExchangeRouteSelectorPublic(ITurnContext context, CancellationToken cancellationToken) | ||
{ | ||
return await base.TokenExchangeRouteSelector(context, cancellationToken); | ||
} | ||
} | ||
|
||
|
||
[Fact] | ||
public async void Test_RunDialog_BeginNew() | ||
{ | ||
// arrange | ||
var app = new Application<TurnState>(new ApplicationOptions<TurnState>()); | ||
var msal = ConfidentialClientApplicationBuilder.Create("clientId").WithClientSecret("clientSecret").Build(); | ||
var settings = new TeamsSsoSettings(new string[] { "User.Read" }, "https://localhost/auth-start.html", msal); | ||
var mockedPrompt = CreateTeamsSsoPromptMock(settings); | ||
var botAuthentication = new MockTeamsSsoBotAuthentication<TurnState>(app, "TokenName", settings, mockedPrompt.Object); | ||
var messageContext = MockTurnContext(); | ||
var turnState = await TurnStateConfig.GetTurnStateWithConversationStateAsync(messageContext); | ||
|
||
// act | ||
var result = await botAuthentication.RunDialog(messageContext, turnState, "dialogStateProperty"); | ||
|
||
// assert | ||
Assert.Equal(DialogTurnStatus.Waiting, result.Status); | ||
} | ||
|
||
[Fact] | ||
public async void Test_RunDialog_ContinueExisting() | ||
{ | ||
// arrange | ||
var app = new Application<TurnState>(new ApplicationOptions<TurnState>()); | ||
var msal = ConfidentialClientApplicationBuilder.Create("clientId").WithClientSecret("clientSecret").Build(); | ||
var settings = new TeamsSsoSettings(new string[] { "User.Read" }, "https://localhost/auth-start.html", msal); | ||
var mockedPrompt = CreateTeamsSsoPromptMock(settings); | ||
var botAuthentication = new MockTeamsSsoBotAuthentication<TurnState>(app, "TokenName", settings, mockedPrompt.Object); | ||
var messageContext = MockTurnContext(); | ||
var turnState = await TurnStateConfig.GetTurnStateWithConversationStateAsync(messageContext); | ||
await botAuthentication.RunDialog(messageContext, turnState, "dialogStateProperty"); // Begin new dialog first | ||
|
||
// act | ||
var tokenExchangeContext = MockTokenExchangeContext(); | ||
var result = await botAuthentication.RunDialog(tokenExchangeContext, turnState, "dialogStateProperty"); | ||
|
||
// assert | ||
Assert.Equal(DialogTurnStatus.Complete, result.Status); | ||
} | ||
|
||
|
||
[Fact] | ||
public async void Test_ContinueDialog() | ||
{ | ||
// arrange | ||
var app = new Application<TurnState>(new ApplicationOptions<TurnState>()); | ||
var msal = ConfidentialClientApplicationBuilder.Create("clientId").WithClientSecret("clientSecret").Build(); | ||
var settings = new TeamsSsoSettings(new string[] { "User.Read" }, "https://localhost/auth-start.html", msal); | ||
var mockedPrompt = CreateTeamsSsoPromptMock(settings); | ||
var botAuthentication = new MockTeamsSsoBotAuthentication<TurnState>(app, "TokenName", settings, mockedPrompt.Object); | ||
var messageContext = MockTurnContext(); | ||
var turnState = await TurnStateConfig.GetTurnStateWithConversationStateAsync(messageContext); | ||
await botAuthentication.RunDialog(messageContext, turnState, "dialogStateProperty"); // Begin new dialog first | ||
|
||
// act | ||
var tokenExchangeContext = MockTokenExchangeContext(); | ||
var result = await botAuthentication.ContinueDialog(tokenExchangeContext, turnState, "dialogStateProperty"); | ||
|
||
// assert | ||
Assert.Equal(DialogTurnStatus.Complete, result.Status); | ||
} | ||
|
||
[Fact] | ||
public async void Test_TokenExchangeRouteSelector_NameMatched() | ||
{ | ||
// arrange | ||
var app = new Application<TurnState>(new ApplicationOptions<TurnState>()); | ||
var msal = ConfidentialClientApplicationBuilder.Create("clientId").WithClientSecret("clientSecret").Build(); | ||
var settings = new TeamsSsoSettings(new string[] { "User.Read" }, "https://localhost/auth-start.html", msal); | ||
var turnContext = MockTokenExchangeContext("test"); | ||
|
||
var botAuthentication = new MockTeamsSsoBotAuthentication<TurnState>(app, "test", settings); | ||
|
||
// act | ||
var result = await botAuthentication.TokenExchangeRouteSelectorPublic(turnContext, CancellationToken.None); | ||
|
||
// assert | ||
Assert.True(result); | ||
} | ||
|
||
[Fact] | ||
public async void Test_TokenExchangeRouteSelector_NameNotMatch() | ||
{ | ||
// arrange | ||
var app = new Application<TurnState>(new ApplicationOptions<TurnState>()); | ||
var msal = ConfidentialClientApplicationBuilder.Create("clientId").WithClientSecret("clientSecret").Build(); | ||
var settings = new TeamsSsoSettings(new string[] { "User.Read" }, "https://localhost/auth-start.html", msal); | ||
var turnContext = MockTokenExchangeContext("AnotherTokenName"); | ||
|
||
var botAuthentication = new MockTeamsSsoBotAuthentication<TurnState>(app, "test", settings); | ||
|
||
// act | ||
var result = await botAuthentication.TokenExchangeRouteSelectorPublic(turnContext, CancellationToken.None); | ||
|
||
// assert | ||
Assert.False(result); | ||
} | ||
|
||
[Fact] | ||
public async void Test_Dedupe() | ||
{ | ||
// arrange | ||
var app = new Application<TurnState>(new ApplicationOptions<TurnState>()); | ||
var msal = ConfidentialClientApplicationBuilder.Create("clientId").WithClientSecret("clientSecret").Build(); | ||
var settings = new TeamsSsoSettings(new string[] { "User.Read" }, "https://localhost/auth-start.html", msal); | ||
var mockedPrompt = CreateTeamsSsoPromptMock(settings); | ||
var botAuthentication = new MockTeamsSsoBotAuthentication<TurnState>(app, "TokenName", settings, mockedPrompt.Object); | ||
|
||
// act | ||
var messageContext = MockTurnContext(); | ||
var turnState = await TurnStateConfig.GetTurnStateWithConversationStateAsync(messageContext); | ||
await botAuthentication.RunDialog(messageContext, turnState, "dialogStateProperty"); | ||
var tokenExchangeContext = MockTokenExchangeContext(); | ||
var tokenExchangeResult = await botAuthentication.ContinueDialog(tokenExchangeContext, turnState, "dialogStateProperty"); | ||
|
||
// assert | ||
Assert.NotNull(tokenExchangeResult.Result); | ||
Assert.Equal("test token", ((TokenResponse)tokenExchangeResult.Result).Token); | ||
|
||
// act - simulate processing duplicate request | ||
await botAuthentication.RunDialog(messageContext, turnState, "dialogStateProperty"); | ||
tokenExchangeResult = await botAuthentication.ContinueDialog(tokenExchangeContext, turnState, "dialogStateProperty"); | ||
|
||
// assert | ||
Assert.Equal(DialogTurnStatus.Waiting, tokenExchangeResult.Status); | ||
} | ||
|
||
private static Mock<TeamsSsoPrompt> CreateTeamsSsoPromptMock(TeamsSsoSettings settings) | ||
{ | ||
var mockedPrompt = new Mock<TeamsSsoPrompt>("TeamsSsoPrompt", "TokenName", settings); | ||
mockedPrompt | ||
.Setup(mock => mock.BeginDialogAsync(It.IsAny<DialogContext>(), It.IsAny<object>(), It.IsAny<CancellationToken>())) | ||
.ReturnsAsync(new DialogTurnResult(DialogTurnStatus.Waiting)); | ||
mockedPrompt | ||
.Setup(mock => mock.ContinueDialogAsync(It.IsAny<DialogContext>(), It.IsAny<CancellationToken>())) | ||
.Returns(async (DialogContext dc, CancellationToken cancellationToken) => | ||
{ | ||
return await dc.EndDialogAsync(new TokenResponse(token: "test token")); | ||
}); | ||
return mockedPrompt; | ||
} | ||
|
||
private static TurnContext MockTurnContext(string type = ActivityTypes.Message, string? name = null) | ||
{ | ||
return new TurnContext(new SimpleAdapter(), new Activity() | ||
{ | ||
Type = type, | ||
Recipient = new() { Id = "recipientId" }, | ||
Conversation = new() { Id = "conversationId" }, | ||
From = new() { Id = "fromId" }, | ||
ChannelId = "channelId", | ||
Name = name | ||
}); | ||
} | ||
|
||
private static TurnContext MockTokenExchangeContext(string settingName = "test") | ||
{ | ||
JObject activityValue = new(); | ||
activityValue["id"] = $"{Guid.NewGuid()}-{settingName}"; | ||
|
||
return new TurnContext(new SimpleAdapter(), new Activity() | ||
{ | ||
Type = ActivityTypes.Invoke, | ||
Name = SignInConstants.TokenExchangeOperationName, | ||
Recipient = new() { Id = "recipientId" }, | ||
Conversation = new() { Id = "conversationId" }, | ||
From = new() { Id = "fromId" }, | ||
ChannelId = "channelId", | ||
Value = activityValue | ||
}); | ||
} | ||
} | ||
} |