Skip to content

Commit

Permalink
Merge branch 'master' into master
Browse files Browse the repository at this point in the history
  • Loading branch information
jennyf19 authored Feb 25, 2024
2 parents 24aa229 + b6cc9eb commit 85d516f
Show file tree
Hide file tree
Showing 12 changed files with 699 additions and 593 deletions.
2 changes: 1 addition & 1 deletion build/template-run-unit-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ steps:
- task: PublishBuildArtifacts@1
displayName: 'Publish traces after test'
inputs:
PathtoPublish: '$(Build.SourcesDirectory)/tests/IntegrationTests/PlaywrightTraces/'
PathtoPublish: '$(Build.SourcesDirectory)/tests/E2E Tests/PlaywrightTraces/'
ArtifactName: 'traces-after-tests-$(Build.BuildNumber)'
condition: failed()
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System;
using System.Linq;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
Expand Down Expand Up @@ -44,20 +45,20 @@ public static IServiceCollection AddTokenAcquisition(
bool forceSdk = !services.Any(s => s.ServiceType.FullName == "Microsoft.AspNetCore.Authentication.IAuthenticationService");
#endif

if (services.FirstOrDefault(s => s.ImplementationType == typeof(DefaultCertificateLoader)) == null)
if (!HasImplementationType(services, typeof(DefaultCertificateLoader)))
{
services.TryAddSingleton<ICredentialsLoader, DefaultCertificateLoader>();
}

if (services.FirstOrDefault(s => s.ImplementationType == typeof(MicrosoftIdentityOptionsMerger)) == null)
if (!HasImplementationType(services, typeof(MicrosoftIdentityOptionsMerger)))
{
services.TryAddSingleton<IPostConfigureOptions<MicrosoftIdentityOptions>, MicrosoftIdentityOptionsMerger>();
}
if (services.FirstOrDefault(s => s.ImplementationType == typeof(MicrosoftIdentityApplicationOptionsMerger)) == null)
if (!HasImplementationType(services, typeof(MicrosoftIdentityApplicationOptionsMerger)))
{
services.TryAddSingleton<IPostConfigureOptions<MicrosoftIdentityApplicationOptions>, MicrosoftIdentityApplicationOptionsMerger>();
}
if (services.FirstOrDefault(s => s.ImplementationType == typeof(ConfidentialClientApplicationOptionsMerger)) == null)
if (!HasImplementationType(services, typeof(ConfidentialClientApplicationOptionsMerger)))
{
services.TryAddSingleton<IPostConfigureOptions<ConfidentialClientApplicationOptions>, ConfidentialClientApplicationOptionsMerger>();
}
Expand Down Expand Up @@ -139,5 +140,14 @@ public static IServiceCollection AddTokenAcquisition(
services.AddSingleton<IMergedOptionsStore, MergedOptionsStore>();
return services;
}

private static bool HasImplementationType(IServiceCollection services, Type implementationType)
{
return services.Any(s =>
#if NET8_0_OR_GREATER
s.ServiceKey is null &&
#endif
s.ImplementationType == implementationType);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,11 @@ private static void AddMicrosoftIdentityWebApiImplementation(
builder.Services.AddRequiredScopeAuthorization();
builder.Services.AddRequiredScopeOrAppPermissionAuthorization();
builder.Services.AddOptions<AadIssuerValidatorOptions>();
if (builder.Services.FirstOrDefault(s => s.ImplementationType == typeof(MicrosoftIdentityOptionsMerger)) == null)
if (!HasImplementationType(builder.Services, typeof(MicrosoftIdentityOptionsMerger)))
{
builder.Services.TryAddSingleton<IPostConfigureOptions<MicrosoftIdentityOptions>, MicrosoftIdentityOptionsMerger>();
}
if (builder.Services.FirstOrDefault(s => s.ImplementationType == typeof(JwtBearerOptionsMerger)) == null)
if (!HasImplementationType(builder.Services, typeof(JwtBearerOptionsMerger)))
{
builder.Services.TryAddSingleton<IPostConfigureOptions<JwtBearerOptions>, JwtBearerOptionsMerger>();
}
Expand Down Expand Up @@ -308,5 +308,14 @@ internal static void ChainOnTokenValidatedEventForClaimsValidation(JwtBearerEven
await tokenValidatedHandler(context).ConfigureAwait(false);
};
}

private static bool HasImplementationType(IServiceCollection services, Type implementationType)
{
return services.Any(s =>
#if NET8_0_OR_GREATER
s.ServiceKey is null &&
#endif
s.ImplementationType == implementationType);
}
}
}

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion tests/E2E Tests/TokenAcquirerTests/TokenAcquirer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -313,7 +313,6 @@ public async Task AcquireTokenWithManagedIdentity_UserAssigned()
const string baseUrl = "https://vault.azure.net";
const string clientId = "9c5896db-a74a-4b1a-a259-74c5080a3a6a";
TokenAcquirerFactory tokenAcquirerFactory = TokenAcquirerFactory.GetDefaultInstance();
_ = tokenAcquirerFactory.Services;
IServiceProvider serviceProvider = tokenAcquirerFactory.Build();

// Act: Get the authorization header provider and add the options to tell it to use Managed Identity
Expand Down
42 changes: 29 additions & 13 deletions tests/E2E Tests/WebAppUiTests/B2CWebAppCallsWebApiLocally.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,7 @@ namespace WebAppUiTests
#if !FROM_GITHUB_ACTION
{
// since these tests change environment variables we'd prefer it not run at the same time as other tests
[CollectionDefinition(nameof(B2CWebAppCallsWebApiLocally), DisableParallelization = true)]
[Collection("WebAppUiTests")]
[CollectionDefinition(nameof(UiTestNoParallelization), DisableParallelization = true)]
public class B2CWebAppCallsWebApiLocally : IClassFixture<InstallPlaywrightBrowserFixture>
{
private const string KeyvaultEmailName = "IdWeb-B2C-user";
Expand All @@ -30,7 +29,7 @@ public class B2CWebAppCallsWebApiLocally : IClassFixture<InstallPlaywrightBrowse
private const uint TodoListClientPort = 5000;
private const uint TodoListServicePort = 44332;
private const string TraceClassName = "B2CWebAppCallsWebApiLocally";
private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 15000 };
private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 };
private readonly string _devAppPath = Path.Join("DevApps", "B2CWebAppCallsWebApi");
private readonly Uri _keyvaultUri = new("https://webappsapistests.vault.azure.net");
private readonly ITestOutputHelper _output;
Expand Down Expand Up @@ -60,12 +59,6 @@ public async Task Susi_B2C_LocalAccount_TodoAppFucntionsCorrectly()
{TC.KestrelEndpointEnvVar, TC.HttpsStarColon + TodoListClientPort}
};

// Start the web app and api processes.
// Five second delay prevents transient issue where client fails to load on devbox the first time the test is run in VS after rebuilding.
Process serviceProcess = UiTestHelpers.StartProcessLocally(_testAssemblyPath, _devAppPath + TC.s_todoListServicePath, TC.s_todoListServiceExe, serviceEnvVars);
Thread.Sleep(5000);
Process clientProcess = UiTestHelpers.StartProcessLocally(_testAssemblyPath, _devAppPath + TC.s_todoListClientPath, TC.s_todoListClientExe, clientEnvVars);

// Get email and password from keyvault.
string email = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultEmailName, azureCred);
string password = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultPasswordName, azureCred);
Expand All @@ -77,16 +70,39 @@ public async Task Susi_B2C_LocalAccount_TodoAppFucntionsCorrectly()
IBrowserContext context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true });
await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true });

Process? serviceProcess= null;
Process? clientProcess = null;

try
{
// Start the web app and api processes.
// The delay before starting client prevents transient devbox issue where the client fails to load the first time after rebuilding.
serviceProcess = UiTestHelpers.StartProcessLocally(_testAssemblyPath, _devAppPath + TC.s_todoListServicePath, TC.s_todoListServiceExe, serviceEnvVars);
await Task.Delay(3000);
clientProcess = UiTestHelpers.StartProcessLocally(_testAssemblyPath, _devAppPath + TC.s_todoListClientPath, TC.s_todoListClientExe, clientEnvVars);

if (!UiTestHelpers.ProcessesAreAlive(new List<Process>() { clientProcess, serviceProcess }))
{
Assert.Fail(TC.WebAppCrashedString);
}

// Navigate to web app
// Navigate to web app the retry logic ensures the web app has time to start up to establish a connection.
IPage page = await context.NewPageAsync();
await page.GotoAsync(TC.LocalhostUrl + TodoListClientPort);
uint InitialConnectionRetryCount = 5;
while (InitialConnectionRetryCount > 0)
{
try
{
await page.GotoAsync(TC.LocalhostUrl + TodoListClientPort);
break;
}
catch (PlaywrightException ex)
{
await Task.Delay(1000);
InitialConnectionRetryCount--;
if (InitialConnectionRetryCount == 0) { throw ex; }
}
}
LabResponse labResponse = await LabUserHelper.GetB2CLocalAccountAsync().ConfigureAwait(false);

// Initial sign in
Expand Down Expand Up @@ -150,8 +166,8 @@ public async Task Susi_B2C_LocalAccount_TodoAppFucntionsCorrectly()
{
// Add the following to make sure all processes and their children are stopped.
Queue<Process> processes = new Queue<Process>();
processes.Enqueue(serviceProcess!);
processes.Enqueue(clientProcess!);
if (serviceProcess != null) { processes.Enqueue(serviceProcess); }
if (clientProcess != null) { processes.Enqueue(clientProcess); }
UiTestHelpers.KillProcessTrees(processes);

// Stop tracing and export it into a zip archive.
Expand Down
40 changes: 30 additions & 10 deletions tests/E2E Tests/WebAppUiTests/TestingWebAppLocally.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,14 @@
using Microsoft.Playwright;
using Xunit;
using Xunit.Abstractions;
using System.Threading;

namespace WebAppUiTests;

#if !FROM_GITHUB_ACTION && !AZURE_DEVOPS_BUILD

// since this test changes environment variables we'd prefer it not run at the same time as other tests
[CollectionDefinition(nameof(TestingWebAppLocally), DisableParallelization = true)]
[Collection("WebAppUiTests")]
// Since this test affects Kestrel environment variables it can cause a race condition when run in parallel with other UI tests.
[CollectionDefinition(nameof(UiTestNoParallelization), DisableParallelization = true)]
public class TestingWebAppLocally : IClassFixture<InstallPlaywrightBrowserFixture>
{
private const string UrlString = "https://localhost:5001/MicrosoftIdentity/Account/signin";
Expand All @@ -28,8 +28,9 @@ public class TestingWebAppLocally : IClassFixture<InstallPlaywrightBrowserFixtur
private readonly string _devAppExecutable = Path.DirectorySeparatorChar.ToString() + "WebAppCallsMicrosoftGraph.exe";
private readonly string _devAppPath = "DevApps" + Path.DirectorySeparatorChar.ToString() + "WebAppCallsMicrosoftGraph";
private readonly string _uiTestAssemblyLocation = typeof(TestingWebAppLocally).Assembly.Location;
private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 15000 };

public TestingWebAppLocally(ITestOutputHelper output)
public TestingWebAppLocally(ITestOutputHelper output)
{
_output = output;
}
Expand All @@ -39,7 +40,7 @@ public TestingWebAppLocally(ITestOutputHelper output)
public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPassword()
{
// Arrange
Process p = UiTestHelpers.StartProcessLocally(_uiTestAssemblyLocation, _devAppPath, _devAppExecutable);
Process? process = null;
const string TraceFileName = TraceFileClassName + "_ValidEmailPassword";
using IPlaywright playwright = await Playwright.CreateAsync();
IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = true });
Expand All @@ -48,10 +49,29 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPassword()

try
{
if (!UiTestHelpers.ProcessIsAlive(p)) { Assert.Fail(TC.WebAppCrashedString); }
process = UiTestHelpers.StartProcessLocally(_uiTestAssemblyLocation, _devAppPath, _devAppExecutable);

if (!UiTestHelpers.ProcessIsAlive(process)) { Assert.Fail(TC.WebAppCrashedString); }

IPage page = await browser.NewPageAsync();
await page.GotoAsync(UrlString);

// The retry logic ensures the web app has time to start up to establish a connection.
uint InitialConnectionRetryCount = 5;
while (InitialConnectionRetryCount > 0)
{
try
{
await page.GotoAsync(UrlString);
break;
}
catch (PlaywrightException ex)
{
await Task.Delay(1000);
InitialConnectionRetryCount--;
if (InitialConnectionRetryCount == 0) { throw ex; }
}
}

LabResponse labResponse = await LabUserHelper.GetDefaultUserAsync().ConfigureAwait(false);

// Act
Expand All @@ -60,8 +80,8 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPassword()
await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPassword(page, email, labResponse.User.GetOrFetchPassword(), _output);

// Assert
await Assertions.Expect(page.GetByText("Welcome")).ToBeVisibleAsync();
await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync();
await Assertions.Expect(page.GetByText("Welcome")).ToBeVisibleAsync(_assertVisibleOptions);
await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync(_assertVisibleOptions);
}
catch (Exception ex)
{
Expand All @@ -71,7 +91,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPassword()
{
// Cleanup the web app process and any child processes
Queue<Process> processes = new();
processes.Enqueue(p);
if (process != null) { processes.Enqueue(process); }
UiTestHelpers.KillProcessTrees(processes);

// Cleanup Playwright
Expand Down
26 changes: 13 additions & 13 deletions tests/E2E Tests/WebAppUiTests/UiTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,12 +80,12 @@ private static async Task SelectKnownAccountByEmail_MicrosoftIdFlow(IPage page,
}

/// <summary>
///
/// The set of steps to take when given a password to enter and submit when logging in via Microsoft.
/// </summary>
/// <param name="page"></param>
/// <param name="password"></param>
/// <param name="staySignedInText"></param>
/// <param name="output"></param>
/// <param name="page">The browser page instance.</param>
/// <param name="password">The password for the account you're logging into.</param>
/// <param name="staySignedInText">"Yes" or "No" to stay signed in for the given browsing session.</param>
/// <param name="output">The writer for output to the test's console.</param>
public static async Task EnterPassword_MicrosoftIdFlow_ValidPassword(IPage page, string password, string staySignedInText, ITestOutputHelper? output = null)
{
// If using an account that has other non-password validation options, the below code should be uncommented
Expand Down Expand Up @@ -120,9 +120,9 @@ private static void WriteLine(ITestOutputHelper? output, string message)

/// <summary>
/// This starts the recording of playwright trace files. The corresponsing EndAndWritePlaywrightTrace method will also need to be used.
/// This is not used anywhere by default and will need to be added to the code if desired
/// This is not used anywhere by default and will need to be added to the code if desired.
/// </summary>
/// <param name="page">The page object whose context the trace will record</param>
/// <param name="page">The page object whose context the trace will record.</param>
public static async Task StartPlaywrightTrace(IPage page)
{
await page.Context.Tracing.StartAsync(new()
Expand All @@ -134,14 +134,14 @@ await page.Context.Tracing.StartAsync(new()
}

/// <summary>
/// Starts a process from an executable, sets its working directory, and redirects its output to the test's output
/// Starts a process from an executable, sets its working directory, and redirects its output to the test's output.
/// </summary>
/// <param name="testAssemblyLocation">The path to the test's directory</param>
/// <param name="appLocation">The path to the processes directory</param>
/// <param name="executableName">The name of the executable that launches the process</param>
/// <param name="portNumber">The port for the process to listen on</param>
/// <param name="testAssemblyLocation">The path to the test's directory.</param>
/// <param name="appLocation">The path to the processes directory.</param>
/// <param name="executableName">The name of the executable that launches the process.</param>
/// <param name="portNumber">The port for the process to listen on.</param>
/// <param name="isHttp">If the launch URL is http or https. Default is https.</param>
/// <returns>The started process</returns>
/// <returns>The started process.</returns>
public static Process StartProcessLocally(string testAssemblyLocation, string appLocation, string executableName, Dictionary<string, string>? environmentVariables = null)
{
string applicationWorkingDirectory = GetApplicationWorkingDirectory(testAssemblyLocation, appLocation);
Expand Down
12 changes: 12 additions & 0 deletions tests/E2E Tests/WebAppUiTests/UiTestNoParallelization.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Xunit;

namespace WebAppUiTests
{
[CollectionDefinition(nameof(UiTestNoParallelization), DisableParallelization = true)]
public class UiTestNoParallelization
{
}
}
Loading

0 comments on commit 85d516f

Please sign in to comment.