diff --git a/3-WebApp-multi-APIs/appsettings.json b/3-WebApp-multi-APIs/appsettings.json index 24c97a37..919da2d1 100644 --- a/3-WebApp-multi-APIs/appsettings.json +++ b/3-WebApp-multi-APIs/appsettings.json @@ -20,4 +20,4 @@ }, "AllowedHosts": "*", "GraphApiUrl": "https://graph.microsoft.com" -} \ No newline at end of file +} diff --git a/4-WebApp-your-API/4-2-B2C/Client/appsettings.json b/4-WebApp-your-API/4-2-B2C/Client/appsettings.json index 05413392..fff2eed7 100644 --- a/4-WebApp-your-API/4-2-B2C/Client/appsettings.json +++ b/4-WebApp-your-API/4-2-B2C/Client/appsettings.json @@ -6,7 +6,6 @@ "SignedOutCallbackPath": "/signout/B2C_1_susi_reset_v2", "SignUpSignInPolicyId": "B2C_1_susi_reset_v2", "EditProfilePolicyId": "B2C_1_edit_profile_v2", // Optional profile editing policy - "ClientSecret": "X330F3#92!z614M4" //"CallbackPath": "/signin/B2C_1_sign_up_in" // defaults to /signin-oidc }, "TodoList": { diff --git a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs index e7aeddbd..2e0993df 100644 --- a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs +++ b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs @@ -52,7 +52,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // Arrange Playwright setup, to see the browser UI set Headless = false. const string TraceFileName = TraceFileClassName + "_LoginLogout"; using IPlaywright playwright = await Playwright.CreateAsync(); - IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = false }); + IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = true }); IBrowserContext context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true }); await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true }); IPage page = await context.NewPageAsync(); @@ -61,7 +61,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds try { // Build the sample app with correct appsettings file. - UiTestHelpers.BuildSampleWithTestAppsettings(_testAssemblyLocation, _sampleAppPath, _testAppsettingsPath, SampleSlnFileName); + UiTestHelpers.BuildSampleUsingTestAppsettings(_testAssemblyLocation, _sampleAppPath, _testAppsettingsPath, SampleSlnFileName); // 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 diff --git a/UiTests/B2CUiTest/B2CUiTest.cs b/UiTests/B2CUiTest/B2CUiTest.cs new file mode 100644 index 00000000..10ca6c03 --- /dev/null +++ b/UiTests/B2CUiTest/B2CUiTest.cs @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Azure.Identity; +using Common; +using Microsoft.Identity.Lab.Api; +using Microsoft.Playwright; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.Versioning; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using TC = Common.TestConstants; + +namespace B2CUiTest +{ + public class B2CUiTest : IClassFixture + { + private const string KeyvaultEmailName = "IdWeb-B2C-user"; + private const string KeyvaultPasswordName = "IdWeb-B2C-password"; + private const string KeyvaultClientSecretName = "IdWeb-B2C-Client-ClientSecret"; + private const string NameOfUser = "unknown"; + private const uint ProcessStartupRetryNum = 3; + private const string SampleSolutionFileName = "4-2-B2C-Secured-API.sln"; + private const uint TodoListClientPort = 5000; + private const uint TodoListServicePort = 44332; + private const string TraceClassName = "B2C-Login"; + + private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 }; + private readonly string _sampleClientAppPath; + private readonly string _samplePath = Path.Join("4-WebApp-your-API", "4-2-B2C"); + private readonly string _sampleServiceAppPath; + private readonly Uri _keyvaultUri = new("https://webappsapistests.vault.azure.net"); + private readonly ITestOutputHelper _output; + private readonly string _testAssemblyLocation = typeof(B2CUiTest).Assembly.Location; + + public B2CUiTest(ITestOutputHelper output) + { + _output = output; + _sampleClientAppPath = Path.Join(_samplePath, TC.s_todoListClientPath); + _sampleServiceAppPath = Path.Join(_samplePath, TC.s_todoListServicePath); + } + + [Fact] + [SupportedOSPlatform("windows")] + public async Task B2C_ValidCreds_LoginLogout() + { + // Web app and api environmental variable setup. + Dictionary? processes = null; + DefaultAzureCredential azureCred = new(); + string clientSecret = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultClientSecretName, azureCred); + var serviceEnvVars = new Dictionary + { + {"ASPNETCORE_ENVIRONMENT", "Development" }, + {TC.KestrelEndpointEnvVar, TC.HttpStarColon + TodoListServicePort} + }; + var clientEnvVars = new Dictionary + { + {"ASPNETCORE_ENVIRONMENT", "Development"}, + {"AzureAdB2C__ClientSecret", clientSecret}, + {TC.KestrelEndpointEnvVar, TC.HttpsStarColon + TodoListClientPort} + }; + + // Get email and password from keyvault. + string email = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultEmailName, azureCred); + string password = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultPasswordName, azureCred); + + // Playwright setup. To see browser UI, set 'Headless = false'. + const string TraceFileName = TraceClassName + "_TodoAppFunctionsCorrectly"; + using IPlaywright playwright = await Playwright.CreateAsync(); + IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = true }); + IBrowserContext context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true }); + await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true }); + + try + { + UiTestHelpers.BuildSampleUsingSampleAppsettings(_testAssemblyLocation, _samplePath, SampleSolutionFileName); + + // 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. + var clientProcessOptions = new ProcessStartOptions(_testAssemblyLocation, _sampleClientAppPath, TC.s_todoListClientExe, clientEnvVars); // probs need to add client specific path + var serviceProcessOptions = new ProcessStartOptions(_testAssemblyLocation, _sampleServiceAppPath, TC.s_todoListServiceExe, serviceEnvVars); + + UiTestHelpers.StartAndVerifyProcessesAreRunning([serviceProcessOptions, clientProcessOptions], out processes, ProcessStartupRetryNum); + + // 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(); + uint InitialConnectionRetryCount = 5; + while (InitialConnectionRetryCount > 0) + { + try + { + await page.GotoAsync(TC.LocalhostUrl + TodoListClientPort); + break; + } + catch (PlaywrightException) + { + await Task.Delay(1000); + InitialConnectionRetryCount--; + if (InitialConnectionRetryCount == 0) { throw; } + } + } + LabResponse labResponse = await LabUserHelper.GetB2CLocalAccountAsync(); + + // Initial sign in + _output.WriteLine("Starting web app sign-in flow."); + ILocator emailEntryBox = page.GetByPlaceholder("Email Address"); + await emailEntryBox.FillAsync(email); + await emailEntryBox.PressAsync("Tab"); + await page.GetByPlaceholder("Password").FillAsync(password); + await page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync(); + await Assertions.Expect(page.GetByText("TodoList")).ToBeVisibleAsync(_assertVisibleOptions); + await Assertions.Expect(page.GetByText(NameOfUser)).ToBeVisibleAsync(_assertVisibleOptions); + _output.WriteLine("Web app sign-in flow successful."); + + // Sign out + _output.WriteLine("Starting web app sign-out flow."); + await page.GetByRole(AriaRole.Link, new() { Name = "Sign out" }).ClickAsync(); + _output.WriteLine("Signing out ..."); + await Assertions.Expect(page.GetByText("You have successfully signed out.")).ToBeVisibleAsync(_assertVisibleOptions); + await Assertions.Expect(page.GetByText(NameOfUser)).ToBeHiddenAsync(); + _output.WriteLine("Web app sign out successful."); + } + catch (Exception ex) + { + Assert.Fail($"the UI automation failed: {ex} output: {ex.Message}."); + } + finally + { + // End all processes. + UiTestHelpers.EndProcesses(processes); + + // Stop tracing and export it into a zip archive. + string path = UiTestHelpers.GetTracePath(_testAssemblyLocation, TraceFileName); + await context.Tracing.StopAsync(new() { Path = path }); + _output.WriteLine($"Trace data for {TraceFileName} recorded to {path}."); + + // Close the browser and stop Playwright. + await browser.CloseAsync(); + playwright.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/UiTests/B2CUiTest/B2CUiTest.csproj b/UiTests/B2CUiTest/B2CUiTest.csproj new file mode 100644 index 00000000..78d257c8 --- /dev/null +++ b/UiTests/B2CUiTest/B2CUiTest.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + false + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + \ No newline at end of file diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 9ceb365e..75a467f7 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -235,7 +235,7 @@ private static string GetApplicationWorkingDirectory(string testAssemblyLocation /// The path to the test's directory /// The path to the processes directory /// The path to the directory for the given app - private static string GetAppsettingsDirectory(string testAssemblyLocation, string appLocation) + private static string GetAbsoluteAppDirectory(string testAssemblyLocation, string appLocation) { string testedAppLocation = Path.GetDirectoryName(testAssemblyLocation)!; // e.g. microsoft-identity-web\tests\E2E Tests\WebAppUiTests\bin\Debug\net6.0 @@ -380,7 +380,7 @@ public static void InstallPlaywrightBrowser() /// The name of the secret /// The value of the secret from key vault /// Throws if no secret name is provided - internal static async Task GetValueFromKeyvaultWitDefaultCreds(Uri keyvaultUri, string keyvaultSecretName, TokenCredential creds) + public static async Task GetValueFromKeyvaultWitDefaultCreds(Uri keyvaultUri, string keyvaultSecretName, TokenCredential creds) { if (string.IsNullOrEmpty(keyvaultSecretName)) { @@ -528,7 +528,7 @@ private static void BuildSolution(string solutionPath) process.WaitForExit(); } - Console.WriteLine("Solution rebuild initiated."); + Console.WriteLine("Solution build initiated."); } /// @@ -538,23 +538,29 @@ private static void BuildSolution(string solutionPath) /// Relative path to the sample app to build starting at the repo's root, does not include appsettings filename /// Relative path to the test appsettings file starting at the repo's root, includes appsettings filename /// Filename for the sln file to build - public static void BuildSampleWithTestAppsettings( + public static void BuildSampleUsingTestAppsettings( string testAssemblyLocation, string sampleRelPath, string testAppsettingsRelPath, string solutionFileName ) { - string appsettingsDirectory = GetAppsettingsDirectory(testAssemblyLocation, sampleRelPath); + string appsettingsDirectory = GetAbsoluteAppDirectory(testAssemblyLocation, sampleRelPath); string appsettingsAbsPath = Path.Combine(appsettingsDirectory, TestConstants.AppSetttingsDotJson); - string testAppsettingsAbsPath = GetAppsettingsDirectory(testAssemblyLocation, testAppsettingsRelPath); + string testAppsettingsAbsPath = GetAbsoluteAppDirectory(testAssemblyLocation, testAppsettingsRelPath); SwapFiles(appsettingsAbsPath, testAppsettingsAbsPath); - try { BuildSolution(appsettingsDirectory + solutionFileName); } + try { BuildSolution(Path.Combine(appsettingsDirectory, solutionFileName)); } catch (Exception) { throw; } finally { SwapFiles(appsettingsAbsPath, testAppsettingsAbsPath); } } + + public static void BuildSampleUsingSampleAppsettings(string testAssemblyLocation, string sampleRelPath, string solutionFileName) + { + string appsDirectory = GetAbsoluteAppDirectory(testAssemblyLocation, sampleRelPath); + BuildSolution(Path.Combine(appsDirectory, solutionFileName)); + } } /// @@ -597,4 +603,3 @@ public ProcessStartOptions( } } } - diff --git a/UiTests/Directory.Build.props b/UiTests/Directory.Build.props index 98953e18..3767fd5f 100644 --- a/UiTests/Directory.Build.props +++ b/UiTests/Directory.Build.props @@ -5,6 +5,7 @@ net8.0 false false + enable @@ -14,7 +15,7 @@ 17.11.1 1.47.0 8.0.0 - 8.0.4 + 8.0.5 2.9.1 2.9.1 2.8.2 diff --git a/UiTests/UiTests.sln b/UiTests/UiTests.sln index f0febbc6..6ebc613e 100644 --- a/UiTests/UiTests.sln +++ b/UiTests/UiTests.sln @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Directory.Build.props = Directory.Build.props EndProjectSection EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B2CUiTest", "B2CUiTest\B2CUiTest.csproj", "{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -26,6 +28,10 @@ Global {3074B729-52E8-408E-8BBC-815FE9217385}.Debug|Any CPU.Build.0 = Debug|Any CPU {3074B729-52E8-408E-8BBC-815FE9217385}.Release|Any CPU.ActiveCfg = Release|Any CPU {3074B729-52E8-408E-8BBC-815FE9217385}.Release|Any CPU.Build.0 = Release|Any CPU + {BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Debug|Any CPU.Build.0 = Debug|Any CPU + {BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Release|Any CPU.ActiveCfg = Release|Any CPU + {BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE