From 04f5a6ad4ab1b60a3682c7f3b4e3bb55b1edeeed Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 24 Sep 2024 17:58:44 -0700 Subject: [PATCH 01/24] added Ui test for multiple API sample --- UiTests/Common/Common.csproj | 32 ++ UiTests/Common/TestConstants.cs | 26 ++ UiTests/Common/UiTestHelpers.cs | 475 +++++++++++++++++++++++ UiTests/Directory.Build.props | 24 ++ UiTests/UiTests/MultiApiTest.cs | 133 +++++++ UiTests/UiTests/MultipleApiUiTest.csproj | 31 ++ UiTests/UiTests/UiTests.sln | 36 ++ 7 files changed, 757 insertions(+) create mode 100644 UiTests/Common/Common.csproj create mode 100644 UiTests/Common/TestConstants.cs create mode 100644 UiTests/Common/UiTestHelpers.cs create mode 100644 UiTests/Directory.Build.props create mode 100644 UiTests/UiTests/MultiApiTest.cs create mode 100644 UiTests/UiTests/MultipleApiUiTest.csproj create mode 100644 UiTests/UiTests/UiTests.sln diff --git a/UiTests/Common/Common.csproj b/UiTests/Common/Common.csproj new file mode 100644 index 00000000..7fefe64d --- /dev/null +++ b/UiTests/Common/Common.csproj @@ -0,0 +1,32 @@ + + + + net8.0 + enable + enable + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/UiTests/Common/TestConstants.cs b/UiTests/Common/TestConstants.cs new file mode 100644 index 00000000..2fc89e78 --- /dev/null +++ b/UiTests/Common/TestConstants.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Common +{ + public static class TestConstants + { + public const string Headless = "headless"; + public const string HeaderText = "Header"; + public const string EmailText = "Email"; + public const string PasswordText = "Password"; + public const string TodoTitle1 = "Testing create todo item"; + public const string TodoTitle2 = "Testing edit todo item"; + public const string LocalhostUrl = @"https://localhost:"; + public const string KestrelEndpointEnvVar = "Kestrel:Endpoints:Http:Url"; + public const string HttpStarColon = "http://*:"; + public const string HttpsStarColon = "https://*:"; + public const string WebAppCrashedString = $"The web app process has exited prematurely."; + public const string OIDCUser = "fIDLAB@MSIDLAB3.com"; + public static readonly string s_oidcWebAppExe = Path.DirectorySeparatorChar.ToString() + "WebApp-OpenIDConnect-DotNet.exe"; + public static readonly string s_oidcWebAppPath = Path.DirectorySeparatorChar.ToString() + "WebApp-OpenIDConnect"; + } +} diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs new file mode 100644 index 00000000..6192c571 --- /dev/null +++ b/UiTests/Common/UiTestHelpers.cs @@ -0,0 +1,475 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Management; +using System.Runtime.Versioning; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Azure.Core; +using Azure.Security.KeyVault.Secrets; +using Microsoft.Playwright; +using Xunit.Abstractions; + +namespace Common +{ + public static class UiTestHelpers + { + /// + /// Navigates to a web page with retry logic to ensure establish a connection in case a web app needs more startup time. + /// + /// The uri to navigate to + /// A page in a playwright browser + /// + public static async Task NavigateToWebApp(string uri, IPage page) + { + uint InitialConnectionRetryCount = 5; + while (InitialConnectionRetryCount > 0) + { + try + { + await page.GotoAsync(uri); + break; + } + catch (PlaywrightException) + { + await Task.Delay(1000); + InitialConnectionRetryCount--; + if (InitialConnectionRetryCount == 0) + { throw; } + } + } + } + + /// + /// Login flow for the first time in a given browsing session. + /// + /// Playwright Page object the web app is accessed from + /// email of the user to sign in + /// password for sign in + /// Used to communicate output to the test's Standard Output + /// Whether to select "stay signed in" on login + public static async Task FirstLogin_MicrosoftIdFlow_ValidEmailPassword(IPage page, string email, string password, ITestOutputHelper? output = null, bool staySignedIn = false) + { + string staySignedInText = staySignedIn ? "Yes" : "No"; + await EnterEmailAsync(page, email, output); + await EnterPasswordAsync(page, password, output); + await StaySignedIn_MicrosoftIdFlow(page, staySignedInText, output); + } + + /// + /// Login flow for anytime after the first time in a given browsing session. + /// + /// Playwright Page object the web app is accessed from + /// email of the user to sign in + /// password for sign in + /// Used to communicate output to the test's Standard Output + /// Whether to select "stay signed in" on login + public static async Task SuccessiveLogin_MicrosoftIdFlow_ValidEmailPassword(IPage page, string email, string password, ITestOutputHelper? output = null, bool staySignedIn = false) + { + string staySignedInText = staySignedIn ? "Yes" : "No"; + + WriteLine(output, $"Logging in again in this browsing session... selecting user via email: {email}."); + await SelectKnownAccountByEmail_MicrosoftIdFlow(page, email); + await EnterPasswordAsync(page, password, output); + await StaySignedIn_MicrosoftIdFlow(page, staySignedInText, output); + } + + public static async Task EnterEmailAsync(IPage page, string email, ITestOutputHelper? output = null) + { + WriteLine(output, $"Logging in ... Entering and submitting user name: {email}."); + ILocator emailInputLocator = page.GetByPlaceholder(TestConstants.EmailText); + await FillEntryBox(emailInputLocator, email); + } + + /// + /// Signs the current user out of the web app. + /// + /// Playwright Page object the web app is accessed from + /// email of the user to sign out + /// The url for the page arrived at once successfully signed out + public static async Task PerformSignOut_MicrosoftIdFlow(IPage page, string email, string signOutPageUrl, ITestOutputHelper? output = null) + { + WriteLine(output, "Signing out ..."); + await SelectKnownAccountByEmail_MicrosoftIdFlow(page, email.ToLowerInvariant()); + await page.WaitForURLAsync(signOutPageUrl); + WriteLine(output, "Sign out page successfully reached."); + } + + /// + /// In the Microsoft Identity flow, the user is at certain stages presented with a list of accounts known in + /// the current browsing session to choose from. This method selects the account using the user's email. + /// + /// page for the playwright browser + /// user email address to select + private static async Task SelectKnownAccountByEmail_MicrosoftIdFlow(IPage page, string email) + { + await page.Locator($"[data-test-id=\"{email}\"]").ClickAsync(); + } + + /// + /// The set of steps to take when given a password to enter and submit when logging in via Microsoft. + /// + /// The browser page instance. + /// The password for the account you're logging into. + /// "Yes" or "No" to stay signed in for the given browsing session. + /// The writer for output to the test's console. + public static async Task EnterPasswordAsync(IPage page, string password, ITestOutputHelper? output = null) + { + // If using an account that has other non-password validation options, the below code should be uncommented + /* WriteLine(output, "Selecting \"Password\" as authentication method"); + await page.GetByRole(AriaRole.Button, new() { Name = TestConstants.PasswordText }).ClickAsync();*/ + + WriteLine(output, "Logging in ... entering and submitting password."); + ILocator passwordInputLocator = page.GetByPlaceholder(TestConstants.PasswordText); + await FillEntryBox(passwordInputLocator, password); + } + + public static async Task StaySignedIn_MicrosoftIdFlow(IPage page, string staySignedInText, ITestOutputHelper? output = null) + { + WriteLine(output, $"Logging in ... Clicking {staySignedInText} on whether the browser should stay signed in."); + await page.GetByRole(AriaRole.Button, new() { Name = staySignedInText }).ClickAsync(); + } + + public static async Task FillEntryBox(ILocator entryBox, string entryText) + { + await entryBox.ClickAsync(); + await entryBox.FillAsync(entryText); + await entryBox.PressAsync("Enter"); + } + private static void WriteLine(ITestOutputHelper? output, string message) + { + if (output != null) + { + output.WriteLine(message); + } + else + { + Trace.WriteLine(message); + } + } + + /// + /// 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. + /// + /// The page object whose context the trace will record. + public static async Task StartPlaywrightTrace(IPage page) + { + await page.Context.Tracing.StartAsync(new() + { + Screenshots = true, + Snapshots = true, + Sources = true + }); + } + + /// + /// Starts a process from an executable, sets its working directory, and redirects its output to the test's output. + /// + /// The path to the test's directory. + /// The path to the processes directory. + /// The name of the executable that launches the process. + /// The port for the process to listen on. + /// If the launch URL is http or https. Default is https. + /// The started process. + public static Process StartProcessLocally(string testAssemblyLocation, string appLocation, string executableName, Dictionary? environmentVariables = null) + { + string applicationWorkingDirectory = GetApplicationWorkingDirectory(testAssemblyLocation, appLocation); + ProcessStartInfo processStartInfo = new ProcessStartInfo(applicationWorkingDirectory + executableName) + { + WorkingDirectory = applicationWorkingDirectory, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + + if (environmentVariables != null) + { + foreach (var kvp in environmentVariables) + { + processStartInfo.EnvironmentVariables[kvp.Key] = kvp.Value; + } + } + + Process? process = Process.Start(processStartInfo); + + if (process == null) + { + throw new Exception($"Could not start process {executableName}"); + } + else + { + return process; + } + } + + /// + /// Builds the path to the process's directory + /// + /// 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 GetApplicationWorkingDirectory(string testAssemblyLocation, string appLocation) + { + string testedAppLocation = Path.GetDirectoryName(testAssemblyLocation)!; + // e.g. microsoft-identity-web\tests\E2E Tests\WebAppUiTests\bin\Debug\net6.0 + string[] segments = testedAppLocation.Split(Path.DirectorySeparatorChar); + int numberSegments = segments.Length; + int startLastSegments = numberSegments - 3; + int endFirstSegments = startLastSegments - 2; + return Path.Combine( + Path.Combine(segments.Take(endFirstSegments).ToArray()), + appLocation, + Path.Combine(segments.Skip(startLastSegments).ToArray()) + ); + } + + /// + /// Creates absolute path for Playwright trace file + /// + /// The path the test is being run from + /// The name for the zip file containing the trace + /// An absolute path to a Playwright Trace zip folder + public static string GetTracePath(string testAssemblyLocation, string traceName) + { + const string traceParentFolder = "E2E Tests"; + const string traceFolder = "PlaywrightTraces"; + const string zipExtension = ".zip"; + const int netVersionNumberLength = 3; + + int parentFolderIndex = testAssemblyLocation.IndexOf(traceParentFolder, StringComparison.InvariantCulture); + string substring = testAssemblyLocation[..(parentFolderIndex + traceParentFolder.Length)]; + string netVersion = "_net" + Environment.Version.ToString()[..netVersionNumberLength]; + + // e.g. [absolute path to repo root]\tests\E2E Tests\PlaywrightTraces\[traceName]_net[versionNum].zip + return Path.Combine( + substring, + traceFolder, + traceName + netVersion + zipExtension + ); + } + + public static void EndProcesses(Dictionary? processes) + { + Queue processQueue = new(); + if (processes != null) + { + foreach (var process in processes) + { + processQueue.Enqueue(process.Value); + } + } + KillProcessTrees(processQueue); + } + + /// + /// Kills the processes in the queue and all of their children + /// + /// queue of parent processes + public static void KillProcessTrees(Queue processQueue) + { +#if WINDOWS + Process currentProcess; + while (processQueue.Count > 0) + { + currentProcess = processQueue.Dequeue(); + if (currentProcess == null) + continue; + + foreach (Process child in GetChildProcesses(currentProcess)) + { + processQueue.Enqueue(child); + } + currentProcess.Kill(); + currentProcess.Close(); + } +#else + while (processQueue.Count > 0) + { + Process p = processQueue.Dequeue(); + p.Kill(); + p.WaitForExit(); + } +#endif + } + + /// + /// Gets the child processes of a process on Windows + /// + /// The parent process + /// A list of child processes + [SupportedOSPlatform("windows")] + public static IList GetChildProcesses(this Process process) + { + ManagementObjectSearcher processSearch = new ManagementObjectSearcher($"Select * From Win32_Process Where ParentProcessID={process.Id}"); + IList processList = processSearch.Get() + .Cast() + .Select(mo => + Process.GetProcessById(Convert.ToInt32(mo["ProcessID"], System.Globalization.CultureInfo.InvariantCulture))) + .ToList(); + processSearch.Dispose(); + return processList; + } + + /// + /// Checks if all processes in a list are alive + /// + /// List of processes to check + /// True if all are alive else false + public static bool ProcessesAreAlive(List processes) + { + return processes.All(ProcessIsAlive); + } + + /// + /// Checks if a process is alive + /// + /// Process to check + /// True if alive false if not + public static bool ProcessIsAlive(Process process) + { + return !process.HasExited; + } + + /// + /// Installs the chromium browser for Playwright enabling it to run even if no browser otherwise exists in the test environment + /// + /// Thrown if playwright is unable to install the browsers + public static void InstallPlaywrightBrowser() + { + var exitCode = Microsoft.Playwright.Program.Main(new[] { "install", "chromium" }); + if (exitCode != 0) + { + throw new Exception($"Playwright exited with code {exitCode}"); + } + } + + /// + /// Requests a secret from keyvault using the default azure credentials + /// + /// The URI including path to the secret directory in keyvault + /// 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) + { + if (string.IsNullOrEmpty(keyvaultSecretName)) + { + throw new ArgumentNullException(nameof(keyvaultSecretName)); + } + SecretClient client = new(keyvaultUri, creds); + return (await client.GetSecretAsync(keyvaultSecretName)).Value.Value; + } + + public static bool StartAndVerifyProcessesAreRunning(List processDataEntries, out Dictionary processes) + { + processes = new Dictionary(); + + //Start Processes + foreach (ProcessStartOptions processDataEntry in processDataEntries) + { + var process = UiTestHelpers.StartProcessLocally( + processDataEntry.TestAssemblyLocation, + processDataEntry.AppLocation, + processDataEntry.ExecutableName, + processDataEntry.EnvironmentVariables); + + processes.Add(processDataEntry.ExecutableName, process); + Thread.Sleep(5000); + } + + //Verify that processes are running + for (int i = 0; i < 2; i++) + { + if (!UiTestHelpers.ProcessesAreAlive(processes.Values.ToList())) + { + RestartProcesses(processes, processDataEntries); + } + } + + if (!UiTestHelpers.ProcessesAreAlive(processes.Values.ToList())) + { + return false; + } + + return true; + } + + static void RestartProcesses(Dictionary processes, List processDataEntries) + { + //attempt to restart failed processes + foreach (KeyValuePair processEntry in processes) + { + if (!ProcessIsAlive(processEntry.Value)) + { + var processDataEntry = processDataEntries.Where(x => x.ExecutableName == processEntry.Key).Single(); + var process = StartProcessLocally( + processDataEntry.TestAssemblyLocation, + processDataEntry.AppLocation, + processDataEntry.ExecutableName, + processDataEntry.EnvironmentVariables); + Thread.Sleep(5000); + + //Update process in collection + processes[processEntry.Key] = process; + } + } + } + public static string GetRunningProcessAsString(Dictionary? processes) + { + StringBuilder runningProcesses = new StringBuilder(); + if (processes != null) + { + foreach (var process in processes) + { +#pragma warning disable CA1305 // Specify IFormatProvider + runningProcesses.AppendLine($"Is {process.Key} running: {UiTestHelpers.ProcessIsAlive(process.Value)}"); +#pragma warning restore CA1305 // Specify IFormatProvider + } + } + return runningProcesses.ToString(); + } + } + + /// + /// Fixture class that installs Playwright browser once per xunit test class that implements it + /// + public class InstallPlaywrightBrowserFixture : IDisposable + { + public InstallPlaywrightBrowserFixture() + { + UiTestHelpers.InstallPlaywrightBrowser(); + } + public void Dispose() + { + } + } + + public class ProcessStartOptions + { + public string TestAssemblyLocation { get; } + + public string AppLocation { get; } + + public string ExecutableName { get; } + + public Dictionary? EnvironmentVariables { get; } + + public ProcessStartOptions( + string testAssemblyLocation, + string appLocation, + string executableName, + Dictionary? environmentVariables = null) + { + TestAssemblyLocation = testAssemblyLocation; + AppLocation = appLocation; + ExecutableName = executableName; + EnvironmentVariables = environmentVariables; + } + } +} diff --git a/UiTests/Directory.Build.props b/UiTests/Directory.Build.props new file mode 100644 index 00000000..6515b104 --- /dev/null +++ b/UiTests/Directory.Build.props @@ -0,0 +1,24 @@ + + + false + net8.0 + $(TargetFrameworks); net9.0 + false + false + + + + 6.0.2 + 8.0.8 + 1.0.2 + 17.11.1 + 1.47.0 + 8.0.0 + 8.0.4 + 2.9.1 + 2.9.1 + 2.8.2 + 2.9.1 + + + diff --git a/UiTests/UiTests/MultiApiTest.cs b/UiTests/UiTests/MultiApiTest.cs new file mode 100644 index 00000000..d32507c9 --- /dev/null +++ b/UiTests/UiTests/MultiApiTest.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Versioning; +using System.Text; +using System.Threading.Tasks; +using Common; +using Microsoft.Identity.Lab.Api; +using Microsoft.Playwright; +using Xunit; +using Xunit.Abstractions; +using Process = System.Diagnostics.Process; +using TC = Common.TestConstants; +using TH = Common.UiTestHelpers; + +namespace MultipleApiUiTest +{ + public class MultiApiTest : IClassFixture + { + private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut"; + private const uint ClientPort = 44321; + private const string TraceFileClassName = "OpenIDConnect"; + private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 5000 }; + private readonly string _sampleAppPath = "3-WebApp-multi-APIs" + Path.DirectorySeparatorChar.ToString(); + private readonly string _testAssemblyLocation = typeof(MultiApiTest).Assembly.Location; + private readonly ITestOutputHelper _output; + + public MultiApiTest(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + [SupportedOSPlatform("windows")] + public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_LoginLogout() + { + // Setup web app and api environmental variables. + var clientEnvVars = new Dictionary + { + {"ASPNETCORE_ENVIRONMENT", "Development"}, + {TC.KestrelEndpointEnvVar, TC.HttpsStarColon + ClientPort} + }; + + Dictionary? processes = null; + + // 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 }); + 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(); + string uriWithPort = TC.LocalhostUrl + ClientPort; + + 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 + var clientProcessOptions = new ProcessStartOptions(_testAssemblyLocation, _sampleAppPath, TC.s_oidcWebAppExe, clientEnvVars); + + bool areProcessesRunning = TH.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes); + + if (!areProcessesRunning) + { + _output.WriteLine("Process not started after 3 attempts."); + StringBuilder runningProcesses = new StringBuilder(); + foreach (var process in processes) + { +#pragma warning disable CA1305 // Specify IFormatProvider + runningProcesses.AppendLine($"Is {process.Key} running: {TH.ProcessIsAlive(process.Value)}"); +#pragma warning restore CA1305 // Specify IFormatProvider + } + Assert.Fail(TC.WebAppCrashedString + " " + runningProcesses.ToString()); + } + + LabResponse labResponse = await LabUserHelper.GetSpecificUserAsync(TC.OIDCUser); + + // Initial sign in + _output.WriteLine("Starting web app sign-in flow."); + string email = labResponse.User.Upn; + await TH.NavigateToWebApp(uriWithPort, page); + await TH.EnterEmailAsync(page, email, _output); + await TH.EnterPasswordAsync(page, labResponse.User.GetOrFetchPassword(), _output); + await Assertions.Expect(page.GetByText("Integrating Azure AD V2")).ToBeVisibleAsync(_assertVisibleOptions); + await Assertions.Expect(page.GetByText(email)).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(); + await TH.PerformSignOut_MicrosoftIdFlow(page, email, TC.LocalhostUrl + ClientPort + SignOutPageUriPath, _output); + _output.WriteLine("Web app sign out successful."); + } + catch (Exception ex) + { + //Adding guid incase of multiple test runs. This will allow screenshots to be matched to their appropriet test runs. + var guid = Guid.NewGuid().ToString(); + try + { + if (page != null) + { + await page.ScreenshotAsync(new PageScreenshotOptions() { Path = $"ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_TodoAppFunctionsCorrectlyScreenshotFail{guid}.png", FullPage = true }); + } + } + catch + { + _output.WriteLine("No Screenshot."); + } + + string runningProcesses = TH.GetRunningProcessAsString(processes); + Assert.Fail($"the UI automation failed: {ex} output: {ex.Message}.\n{runningProcesses}\nTest run: {guid}"); + } + finally + { + // Add the following to make sure all processes and their children are stopped. + TH.EndProcesses(processes); + + // Stop tracing and export it into a zip archive. + string path = TH.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/UiTests/MultipleApiUiTest.csproj b/UiTests/UiTests/MultipleApiUiTest.csproj new file mode 100644 index 00000000..ac90c325 --- /dev/null +++ b/UiTests/UiTests/MultipleApiUiTest.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/UiTests/UiTests/UiTests.sln b/UiTests/UiTests/UiTests.sln new file mode 100644 index 00000000..a383d0b8 --- /dev/null +++ b/UiTests/UiTests/UiTests.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35303.130 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipleApiUiTest", "MultipleApiUiTest.csproj", "{2B42751A-8650-4DE4-9B46-B01C21825EB1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "..\Common\Common.csproj", "{3074B729-52E8-408E-8BBC-815FE9217385}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6A8B8F57-DBC6-43E2-84E7-16D24E54157B}" + ProjectSection(SolutionItems) = preProject + ..\Directory.Build.props = ..\Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Release|Any CPU.Build.0 = Release|Any CPU + {3074B729-52E8-408E-8BBC-815FE9217385}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F7161FC1-9BC2-4CE4-B59C-504328CA6C7F} + EndGlobalSection +EndGlobal From e57690964a43bfaa450157b9b10cd7977fb70d46 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Thu, 26 Sep 2024 14:02:55 -0700 Subject: [PATCH 02/24] updating for help with debugging --- 3-WebApp-multi-APIs/appsettings.json | 21 +++++++----------- UiTests/Common/UiTestHelpers.cs | 27 ++++++++++-------------- UiTests/UiTests/MultiApiTest.cs | 7 +++--- UiTests/UiTests/MultipleApiUiTest.csproj | 1 + 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/3-WebApp-multi-APIs/appsettings.json b/3-WebApp-multi-APIs/appsettings.json index 919da2d1..b7775c52 100644 --- a/3-WebApp-multi-APIs/appsettings.json +++ b/3-WebApp-multi-APIs/appsettings.json @@ -1,23 +1,18 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]", - "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", - "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "Domain": "msidlab3.onmicrosoft.com", + "TenantId": "8e44f19d-bbab-4a82-b76b-4cd0a6fbc97a", + "ClientId": "d9cde0be-ad97-41e6-855e-2f85136671c1", "CallbackPath": "/signin-oidc", - "SignedOutCallbackPath": "/signout-callback-oidc", - - // To call an API - "ClientSecret": "[Copy the client secret added to the app from the Azure portal]" - + "SignedOutCallbackPath": "/signout-callback-oidc" }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" } }, - "AllowedHosts": "*", - "GraphApiUrl": "https://graph.microsoft.com" + "AllowedHosts": "*" } diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 6192c571..22d6c3a1 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -1,19 +1,13 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; -using System.Collections.Generic; +using Azure.Core; +using Azure.Security.KeyVault.Secrets; +using Microsoft.Playwright; using System.Diagnostics; -using System.IO; -using System.Linq; using System.Management; using System.Runtime.Versioning; using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Azure.Core; -using Azure.Security.KeyVault.Secrets; -using Microsoft.Playwright; using Xunit.Abstractions; namespace Common @@ -41,7 +35,7 @@ public static async Task NavigateToWebApp(string uri, IPage page) await Task.Delay(1000); InitialConnectionRetryCount--; if (InitialConnectionRetryCount == 0) - { throw; } + { throw; } } } } @@ -366,7 +360,7 @@ internal static async Task GetValueFromKeyvaultWitDefaultCreds(Uri keyva return (await client.GetSecretAsync(keyvaultSecretName)).Value.Value; } - public static bool StartAndVerifyProcessesAreRunning(List processDataEntries, out Dictionary processes) + public static bool StartAndVerifyProcessesAreRunning(List processDataEntries, out Dictionary processes, uint numRetries) { processes = new Dictionary(); @@ -380,16 +374,16 @@ public static bool StartAndVerifyProcessesAreRunning(List p processDataEntry.EnvironmentVariables); processes.Add(processDataEntry.ExecutableName, process); + + // Gives the current process time to start up before the next process is run Thread.Sleep(5000); } //Verify that processes are running - for (int i = 0; i < 2; i++) + for (int i = 0; i < numRetries; i++) { - if (!UiTestHelpers.ProcessesAreAlive(processes.Values.ToList())) - { - RestartProcesses(processes, processDataEntries); - } + if (!UiTestHelpers.ProcessesAreAlive(processes.Values.ToList())) { RestartProcesses(processes, processDataEntries); } + else { break; } } if (!UiTestHelpers.ProcessesAreAlive(processes.Values.ToList())) @@ -473,3 +467,4 @@ public ProcessStartOptions( } } } + diff --git a/UiTests/UiTests/MultiApiTest.cs b/UiTests/UiTests/MultiApiTest.cs index d32507c9..8d53aa12 100644 --- a/UiTests/UiTests/MultiApiTest.cs +++ b/UiTests/UiTests/MultiApiTest.cs @@ -23,7 +23,8 @@ public class MultiApiTest : IClassFixture private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut"; private const uint ClientPort = 44321; private const string TraceFileClassName = "OpenIDConnect"; - private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 5000 }; + private const uint NumProcessRetries = 3; + private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 15000 }; private readonly string _sampleAppPath = "3-WebApp-multi-APIs" + Path.DirectorySeparatorChar.ToString(); private readonly string _testAssemblyLocation = typeof(MultiApiTest).Assembly.Location; private readonly ITestOutputHelper _output; @@ -61,11 +62,11 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // 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, _sampleAppPath, TC.s_oidcWebAppExe, clientEnvVars); - bool areProcessesRunning = TH.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes); + bool areProcessesRunning = TH.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries); if (!areProcessesRunning) { - _output.WriteLine("Process not started after 3 attempts."); + _output.WriteLine($"Process not started after {NumProcessRetries} attempts."); StringBuilder runningProcesses = new StringBuilder(); foreach (var process in processes) { diff --git a/UiTests/UiTests/MultipleApiUiTest.csproj b/UiTests/UiTests/MultipleApiUiTest.csproj index ac90c325..18574c4d 100644 --- a/UiTests/UiTests/MultipleApiUiTest.csproj +++ b/UiTests/UiTests/MultipleApiUiTest.csproj @@ -26,6 +26,7 @@ + From 03e71ce8855a1db6de3a3275836714311b8171eb Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Fri, 27 Sep 2024 16:55:05 -0700 Subject: [PATCH 03/24] making draft PR --- UiTests/Common/UiTestHelpers.cs | 2 +- ...ultiApiTest.cs => AnyOrgOrPersonalTest.cs} | 30 +++++++++---------- ...t.csproj => AnyOrgOrPersonalUiTest.csproj} | 2 +- UiTests/UiTests/UiTests.sln | 4 +-- UiTests/UiTests/appsettings.Development.json | 9 ++++++ UiTests/UiTests/appsettings.json | 18 +++++++++++ 6 files changed, 46 insertions(+), 19 deletions(-) rename UiTests/UiTests/{MultiApiTest.cs => AnyOrgOrPersonalTest.cs} (79%) rename UiTests/UiTests/{MultipleApiUiTest.csproj => AnyOrgOrPersonalUiTest.csproj} (93%) create mode 100644 UiTests/UiTests/appsettings.Development.json create mode 100644 UiTests/UiTests/appsettings.json diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 22d6c3a1..da178fd6 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -376,7 +376,7 @@ public static bool StartAndVerifyProcessesAreRunning(List p processes.Add(processDataEntry.ExecutableName, process); // Gives the current process time to start up before the next process is run - Thread.Sleep(5000); + Thread.Sleep(2000); } //Verify that processes are running diff --git a/UiTests/UiTests/MultiApiTest.cs b/UiTests/UiTests/AnyOrgOrPersonalTest.cs similarity index 79% rename from UiTests/UiTests/MultiApiTest.cs rename to UiTests/UiTests/AnyOrgOrPersonalTest.cs index 8d53aa12..883d5c25 100644 --- a/UiTests/UiTests/MultiApiTest.cs +++ b/UiTests/UiTests/AnyOrgOrPersonalTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Runtime.Versioning; using System.Text; using System.Threading.Tasks; @@ -14,22 +15,21 @@ using Xunit.Abstractions; using Process = System.Diagnostics.Process; using TC = Common.TestConstants; -using TH = Common.UiTestHelpers; namespace MultipleApiUiTest { - public class MultiApiTest : IClassFixture + public class AnyOrgOrPersonalTest : IClassFixture { private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut"; private const uint ClientPort = 44321; private const string TraceFileClassName = "OpenIDConnect"; private const uint NumProcessRetries = 3; - private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 15000 }; - private readonly string _sampleAppPath = "3-WebApp-multi-APIs" + Path.DirectorySeparatorChar.ToString(); - private readonly string _testAssemblyLocation = typeof(MultiApiTest).Assembly.Location; + private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 }; + private readonly string _sampleAppPath = "1-WebApp-OIDC" + Path.DirectorySeparatorChar + "1-3-AnyOrgOrPersonal" + Path.DirectorySeparatorChar.ToString(); + private readonly string _testAssemblyLocation = typeof(AnyOrgOrPersonalTest).Assembly.Location; private readonly ITestOutputHelper _output; - public MultiApiTest(ITestOutputHelper output) + public AnyOrgOrPersonalTest(ITestOutputHelper output) { _output = output; } @@ -62,7 +62,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // 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, _sampleAppPath, TC.s_oidcWebAppExe, clientEnvVars); - bool areProcessesRunning = TH.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries); + bool areProcessesRunning = UiTestHelpers.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries); if (!areProcessesRunning) { @@ -71,7 +71,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds foreach (var process in processes) { #pragma warning disable CA1305 // Specify IFormatProvider - runningProcesses.AppendLine($"Is {process.Key} running: {TH.ProcessIsAlive(process.Value)}"); + runningProcesses.AppendLine($"Is {process.Key} running: {UiTestHelpers.ProcessIsAlive(process.Value)}"); #pragma warning restore CA1305 // Specify IFormatProvider } Assert.Fail(TC.WebAppCrashedString + " " + runningProcesses.ToString()); @@ -82,9 +82,9 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // Initial sign in _output.WriteLine("Starting web app sign-in flow."); string email = labResponse.User.Upn; - await TH.NavigateToWebApp(uriWithPort, page); - await TH.EnterEmailAsync(page, email, _output); - await TH.EnterPasswordAsync(page, labResponse.User.GetOrFetchPassword(), _output); + await UiTestHelpers.NavigateToWebApp(uriWithPort, page); + await UiTestHelpers.EnterEmailAsync(page, email, _output); + await UiTestHelpers.EnterPasswordAsync(page, labResponse.User.GetOrFetchPassword(), _output); await Assertions.Expect(page.GetByText("Integrating Azure AD V2")).ToBeVisibleAsync(_assertVisibleOptions); await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync(_assertVisibleOptions); _output.WriteLine("Web app sign-in flow successful."); @@ -92,7 +92,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // Sign out _output.WriteLine("Starting web app sign-out flow."); await page.GetByRole(AriaRole.Link, new() { Name = "Sign out" }).ClickAsync(); - await TH.PerformSignOut_MicrosoftIdFlow(page, email, TC.LocalhostUrl + ClientPort + SignOutPageUriPath, _output); + await UiTestHelpers.PerformSignOut_MicrosoftIdFlow(page, email, TC.LocalhostUrl + ClientPort + SignOutPageUriPath, _output); _output.WriteLine("Web app sign out successful."); } catch (Exception ex) @@ -111,16 +111,16 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds _output.WriteLine("No Screenshot."); } - string runningProcesses = TH.GetRunningProcessAsString(processes); + string runningProcesses = UiTestHelpers.GetRunningProcessAsString(processes); Assert.Fail($"the UI automation failed: {ex} output: {ex.Message}.\n{runningProcesses}\nTest run: {guid}"); } finally { // Add the following to make sure all processes and their children are stopped. - TH.EndProcesses(processes); + UiTestHelpers.EndProcesses(processes); // Stop tracing and export it into a zip archive. - string path = TH.GetTracePath(_testAssemblyLocation, TraceFileName); + string path = UiTestHelpers.GetTracePath(_testAssemblyLocation, TraceFileName); await context.Tracing.StopAsync(new() { Path = path }); _output.WriteLine($"Trace data for {TraceFileName} recorded to {path}."); diff --git a/UiTests/UiTests/MultipleApiUiTest.csproj b/UiTests/UiTests/AnyOrgOrPersonalUiTest.csproj similarity index 93% rename from UiTests/UiTests/MultipleApiUiTest.csproj rename to UiTests/UiTests/AnyOrgOrPersonalUiTest.csproj index 18574c4d..30bd2ccc 100644 --- a/UiTests/UiTests/MultipleApiUiTest.csproj +++ b/UiTests/UiTests/AnyOrgOrPersonalUiTest.csproj @@ -26,7 +26,7 @@ - + diff --git a/UiTests/UiTests/UiTests.sln b/UiTests/UiTests/UiTests.sln index a383d0b8..f7a38e3f 100644 --- a/UiTests/UiTests/UiTests.sln +++ b/UiTests/UiTests/UiTests.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35303.130 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipleApiUiTest", "MultipleApiUiTest.csproj", "{2B42751A-8650-4DE4-9B46-B01C21825EB1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnyOrgOrPersonalUiTest", "AnyOrgOrPersonalUiTest.csproj", "{2B42751A-8650-4DE4-9B46-B01C21825EB1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "..\Common\Common.csproj", "{3074B729-52E8-408E-8BBC-815FE9217385}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "..\Common\Common.csproj", "{3074B729-52E8-408E-8BBC-815FE9217385}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6A8B8F57-DBC6-43E2-84E7-16D24E54157B}" ProjectSection(SolutionItems) = preProject diff --git a/UiTests/UiTests/appsettings.Development.json b/UiTests/UiTests/appsettings.Development.json new file mode 100644 index 00000000..0623a3f4 --- /dev/null +++ b/UiTests/UiTests/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/UiTests/UiTests/appsettings.json b/UiTests/UiTests/appsettings.json new file mode 100644 index 00000000..b7775c52 --- /dev/null +++ b/UiTests/UiTests/appsettings.json @@ -0,0 +1,18 @@ +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "msidlab3.onmicrosoft.com", + "TenantId": "8e44f19d-bbab-4a82-b76b-4cd0a6fbc97a", + "ClientId": "d9cde0be-ad97-41e6-855e-2f85136671c1", + "CallbackPath": "/signin-oidc", + "SignedOutCallbackPath": "/signout-callback-oidc" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} From f84fb66728ced6f86b014c22e2a46e91d0225880 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Fri, 27 Sep 2024 16:55:33 -0700 Subject: [PATCH 04/24] adding temporary appsettings.json fix --- 1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json b/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json index 41b09c5d..b7775c52 100644 --- a/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json +++ b/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json @@ -1,9 +1,9 @@ -{ +{ "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]", - "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", - "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "Domain": "msidlab3.onmicrosoft.com", + "TenantId": "8e44f19d-bbab-4a82-b76b-4cd0a6fbc97a", + "ClientId": "d9cde0be-ad97-41e6-855e-2f85136671c1", "CallbackPath": "/signin-oidc", "SignedOutCallbackPath": "/signout-callback-oidc" }, From 6cfd55498ea10e1e86fb0262895d5a7717f74bbf Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 30 Sep 2024 13:41:07 -0700 Subject: [PATCH 05/24] small constants update --- UiTests/Common/TestConstants.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/UiTests/Common/TestConstants.cs b/UiTests/Common/TestConstants.cs index 2fc89e78..da869623 100644 --- a/UiTests/Common/TestConstants.cs +++ b/UiTests/Common/TestConstants.cs @@ -8,6 +8,7 @@ namespace Common { public static class TestConstants { + public const string AppSetttingsDotJson = "appsettings.json"; public const string Headless = "headless"; public const string HeaderText = "Header"; public const string EmailText = "Email"; From fdac0a38a33954d07a636893d148f538005d474f Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 30 Sep 2024 16:20:04 -0700 Subject: [PATCH 06/24] Created B2C UI test --- .../4-2-B2C/Client/appsettings.json | 1 - .../AnyOrgOrPersonalTest.cs | 0 .../AnyOrgOrPersonalUiTest.csproj | 0 .../appsettings.Development.json | 0 .../appsettings.json | 0 UiTests/B2CUiTest/B2CUiTest.cs | 148 ++++++++++++++++++ UiTests/B2CUiTest/B2CUiTest.csproj | 33 ++++ UiTests/Common/TestConstants.cs | 5 + UiTests/Common/UiTestHelpers.cs | 2 +- UiTests/{UiTests => }/UiTests.sln | 10 +- 10 files changed, 195 insertions(+), 4 deletions(-) rename UiTests/{UiTests => AnyOrgOrPersonalUiTest}/AnyOrgOrPersonalTest.cs (100%) rename UiTests/{UiTests => AnyOrgOrPersonalUiTest}/AnyOrgOrPersonalUiTest.csproj (100%) rename UiTests/{UiTests => AnyOrgOrPersonalUiTest}/appsettings.Development.json (100%) rename UiTests/{UiTests => AnyOrgOrPersonalUiTest}/appsettings.json (100%) create mode 100644 UiTests/B2CUiTest/B2CUiTest.cs create mode 100644 UiTests/B2CUiTest/B2CUiTest.csproj rename UiTests/{UiTests => }/UiTests.sln (69%) 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/UiTests/AnyOrgOrPersonalTest.cs b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs similarity index 100% rename from UiTests/UiTests/AnyOrgOrPersonalTest.cs rename to UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs diff --git a/UiTests/UiTests/AnyOrgOrPersonalUiTest.csproj b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj similarity index 100% rename from UiTests/UiTests/AnyOrgOrPersonalUiTest.csproj rename to UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj diff --git a/UiTests/UiTests/appsettings.Development.json b/UiTests/AnyOrgOrPersonalUiTest/appsettings.Development.json similarity index 100% rename from UiTests/UiTests/appsettings.Development.json rename to UiTests/AnyOrgOrPersonalUiTest/appsettings.Development.json diff --git a/UiTests/UiTests/appsettings.json b/UiTests/AnyOrgOrPersonalUiTest/appsettings.json similarity index 100% rename from UiTests/UiTests/appsettings.json rename to UiTests/AnyOrgOrPersonalUiTest/appsettings.json diff --git a/UiTests/B2CUiTest/B2CUiTest.cs b/UiTests/B2CUiTest/B2CUiTest.cs new file mode 100644 index 00000000..6eb7836c --- /dev/null +++ b/UiTests/B2CUiTest/B2CUiTest.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +using Azure.Identity; +using Common; +using Microsoft.Identity.Lab.Api; +using Microsoft.Playwright; +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 TodoListClientPort = 5000; + private const uint TodoListServicePort = 44332; + private const string TraceClassName = "B2C-Login"; + private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 }; + private readonly string _sampleAppPath = Path.Join("4-WebApp-your-API", "4-2-B2C"); + private readonly Uri _keyvaultUri = new("https://webappsapistests.vault.azure.net"); + private readonly ITestOutputHelper _output; + private readonly string _testAssemblyPath = typeof(B2CUiTest).Assembly.Location; + + public B2CUiTest(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + [SupportedOSPlatform("windows")] + public async Task B2C_ValidCreds_LoginLogout() + { + // Web app and api environmental variable setup. + 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 = false }); + 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, _sampleAppPath + TC.s_todoListServicePath, TC.s_todoListServiceExe, serviceEnvVars); + await Task.Delay(3000); + clientProcess = UiTestHelpers.StartProcessLocally(_testAssemblyPath, _sampleAppPath + TC.s_todoListClientPath, TC.s_todoListClientExe, clientEnvVars); + + if (!UiTestHelpers.ProcessesAreAlive(new List() { clientProcess, serviceProcess })) + { + Assert.Fail(TC.WebAppCrashedString); + } + + // 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 ex) + { + await Task.Delay(1000); + InitialConnectionRetryCount--; + if (InitialConnectionRetryCount == 0) { throw ex; } + } + } + 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 + { + // Add the following to make sure all processes and their children are stopped. + Queue processes = new Queue(); + if (serviceProcess != null) { processes.Enqueue(serviceProcess); } + if (clientProcess != null) { processes.Enqueue(clientProcess); } + UiTestHelpers.KillProcessTrees(processes); + + // Stop tracing and export it into a zip archive. + string path = UiTestHelpers.GetTracePath(_testAssemblyPath, 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..9d9bcc59 --- /dev/null +++ b/UiTests/B2CUiTest/B2CUiTest.csproj @@ -0,0 +1,33 @@ + + + + net8.0 + + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + diff --git a/UiTests/Common/TestConstants.cs b/UiTests/Common/TestConstants.cs index da869623..6e7d066d 100644 --- a/UiTests/Common/TestConstants.cs +++ b/UiTests/Common/TestConstants.cs @@ -21,7 +21,12 @@ public static class TestConstants public const string HttpsStarColon = "https://*:"; public const string WebAppCrashedString = $"The web app process has exited prematurely."; public const string OIDCUser = "fIDLAB@MSIDLAB3.com"; + public static readonly string s_oidcWebAppExe = Path.DirectorySeparatorChar.ToString() + "WebApp-OpenIDConnect-DotNet.exe"; public static readonly string s_oidcWebAppPath = Path.DirectorySeparatorChar.ToString() + "WebApp-OpenIDConnect"; + public static readonly string s_todoListClientExe = Path.DirectorySeparatorChar.ToString() + "TodoListClient.exe"; + public static readonly string s_todoListClientPath = Path.DirectorySeparatorChar.ToString() + "Client"; + public static readonly string s_todoListServiceExe = Path.DirectorySeparatorChar.ToString() + "TodoListService.exe"; + public static readonly string s_todoListServicePath = Path.DirectorySeparatorChar.ToString() + "TodoListService"; } } diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index da178fd6..fe7a27b2 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -350,7 +350,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)) { diff --git a/UiTests/UiTests/UiTests.sln b/UiTests/UiTests.sln similarity index 69% rename from UiTests/UiTests/UiTests.sln rename to UiTests/UiTests.sln index f7a38e3f..5d987913 100644 --- a/UiTests/UiTests/UiTests.sln +++ b/UiTests/UiTests.sln @@ -3,15 +3,17 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35303.130 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnyOrgOrPersonalUiTest", "AnyOrgOrPersonalUiTest.csproj", "{2B42751A-8650-4DE4-9B46-B01C21825EB1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnyOrgOrPersonalUiTest", "AnyOrgOrPersonalUiTest\AnyOrgOrPersonalUiTest.csproj", "{2B42751A-8650-4DE4-9B46-B01C21825EB1}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "..\Common\Common.csproj", "{3074B729-52E8-408E-8BBC-815FE9217385}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csproj", "{3074B729-52E8-408E-8BBC-815FE9217385}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6A8B8F57-DBC6-43E2-84E7-16D24E54157B}" ProjectSection(SolutionItems) = preProject ..\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 From b836757d72e223f5a8040990d3254e0875977481 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Thu, 3 Oct 2024 17:52:48 -0700 Subject: [PATCH 07/24] sort usings --- UiTests/B2CUiTest/B2CUiTest.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/UiTests/B2CUiTest/B2CUiTest.cs b/UiTests/B2CUiTest/B2CUiTest.cs index 6eb7836c..77196c74 100644 --- a/UiTests/B2CUiTest/B2CUiTest.cs +++ b/UiTests/B2CUiTest/B2CUiTest.cs @@ -1,17 +1,16 @@ // 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; using System.Threading.Tasks; -using Azure.Identity; -using Common; -using Microsoft.Identity.Lab.Api; -using Microsoft.Playwright; using Xunit; using Xunit.Abstractions; using TC = Common.TestConstants; From 871809ced810777e2da6b15ee4930f41ebcbcbf4 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 24 Sep 2024 17:58:44 -0700 Subject: [PATCH 08/24] added Ui test for multiple API sample --- UiTests/Common/UiTestHelpers.cs | 3 +- UiTests/Directory.Build.props | 6 +- UiTests/UiTests/MultiApiTest.cs | 133 +++++++++++++++++++++++ UiTests/UiTests/MultipleApiUiTest.csproj | 31 ++++++ UiTests/UiTests/UiTests.sln | 36 ++++++ 5 files changed, 205 insertions(+), 4 deletions(-) create mode 100644 UiTests/UiTests/MultiApiTest.cs create mode 100644 UiTests/UiTests/MultipleApiUiTest.csproj create mode 100644 UiTests/UiTests/UiTests.sln diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 9ceb365e..45f35961 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -34,7 +34,7 @@ public static async Task NavigateToWebApp(string uri, IPage page) await Task.Delay(1000); InitialConnectionRetryCount--; if (InitialConnectionRetryCount == 0) - { throw; } + { throw; } } } } @@ -421,6 +421,7 @@ public static bool StartAndVerifyProcessesAreRunning(List p { if (!UiTestHelpers.ProcessesAreAlive(processes.Values.ToList())) { RestartProcesses(processes, processDataEntries); } else { break; } + } } if (!UiTestHelpers.ProcessesAreAlive(processes.Values.ToList())) diff --git a/UiTests/Directory.Build.props b/UiTests/Directory.Build.props index 98953e18..ce0bb340 100644 --- a/UiTests/Directory.Build.props +++ b/UiTests/Directory.Build.props @@ -17,8 +17,8 @@ 8.0.4 2.9.1 2.9.1 - 2.8.2 - 2.9.1 + 2.8.2 + 2.9.1 - + diff --git a/UiTests/UiTests/MultiApiTest.cs b/UiTests/UiTests/MultiApiTest.cs new file mode 100644 index 00000000..d32507c9 --- /dev/null +++ b/UiTests/UiTests/MultiApiTest.cs @@ -0,0 +1,133 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Versioning; +using System.Text; +using System.Threading.Tasks; +using Common; +using Microsoft.Identity.Lab.Api; +using Microsoft.Playwright; +using Xunit; +using Xunit.Abstractions; +using Process = System.Diagnostics.Process; +using TC = Common.TestConstants; +using TH = Common.UiTestHelpers; + +namespace MultipleApiUiTest +{ + public class MultiApiTest : IClassFixture + { + private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut"; + private const uint ClientPort = 44321; + private const string TraceFileClassName = "OpenIDConnect"; + private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 5000 }; + private readonly string _sampleAppPath = "3-WebApp-multi-APIs" + Path.DirectorySeparatorChar.ToString(); + private readonly string _testAssemblyLocation = typeof(MultiApiTest).Assembly.Location; + private readonly ITestOutputHelper _output; + + public MultiApiTest(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + [SupportedOSPlatform("windows")] + public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_LoginLogout() + { + // Setup web app and api environmental variables. + var clientEnvVars = new Dictionary + { + {"ASPNETCORE_ENVIRONMENT", "Development"}, + {TC.KestrelEndpointEnvVar, TC.HttpsStarColon + ClientPort} + }; + + Dictionary? processes = null; + + // 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 }); + 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(); + string uriWithPort = TC.LocalhostUrl + ClientPort; + + 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 + var clientProcessOptions = new ProcessStartOptions(_testAssemblyLocation, _sampleAppPath, TC.s_oidcWebAppExe, clientEnvVars); + + bool areProcessesRunning = TH.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes); + + if (!areProcessesRunning) + { + _output.WriteLine("Process not started after 3 attempts."); + StringBuilder runningProcesses = new StringBuilder(); + foreach (var process in processes) + { +#pragma warning disable CA1305 // Specify IFormatProvider + runningProcesses.AppendLine($"Is {process.Key} running: {TH.ProcessIsAlive(process.Value)}"); +#pragma warning restore CA1305 // Specify IFormatProvider + } + Assert.Fail(TC.WebAppCrashedString + " " + runningProcesses.ToString()); + } + + LabResponse labResponse = await LabUserHelper.GetSpecificUserAsync(TC.OIDCUser); + + // Initial sign in + _output.WriteLine("Starting web app sign-in flow."); + string email = labResponse.User.Upn; + await TH.NavigateToWebApp(uriWithPort, page); + await TH.EnterEmailAsync(page, email, _output); + await TH.EnterPasswordAsync(page, labResponse.User.GetOrFetchPassword(), _output); + await Assertions.Expect(page.GetByText("Integrating Azure AD V2")).ToBeVisibleAsync(_assertVisibleOptions); + await Assertions.Expect(page.GetByText(email)).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(); + await TH.PerformSignOut_MicrosoftIdFlow(page, email, TC.LocalhostUrl + ClientPort + SignOutPageUriPath, _output); + _output.WriteLine("Web app sign out successful."); + } + catch (Exception ex) + { + //Adding guid incase of multiple test runs. This will allow screenshots to be matched to their appropriet test runs. + var guid = Guid.NewGuid().ToString(); + try + { + if (page != null) + { + await page.ScreenshotAsync(new PageScreenshotOptions() { Path = $"ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_TodoAppFunctionsCorrectlyScreenshotFail{guid}.png", FullPage = true }); + } + } + catch + { + _output.WriteLine("No Screenshot."); + } + + string runningProcesses = TH.GetRunningProcessAsString(processes); + Assert.Fail($"the UI automation failed: {ex} output: {ex.Message}.\n{runningProcesses}\nTest run: {guid}"); + } + finally + { + // Add the following to make sure all processes and their children are stopped. + TH.EndProcesses(processes); + + // Stop tracing and export it into a zip archive. + string path = TH.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/UiTests/MultipleApiUiTest.csproj b/UiTests/UiTests/MultipleApiUiTest.csproj new file mode 100644 index 00000000..ac90c325 --- /dev/null +++ b/UiTests/UiTests/MultipleApiUiTest.csproj @@ -0,0 +1,31 @@ + + + + net8.0 + + false + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/UiTests/UiTests/UiTests.sln b/UiTests/UiTests/UiTests.sln new file mode 100644 index 00000000..a383d0b8 --- /dev/null +++ b/UiTests/UiTests/UiTests.sln @@ -0,0 +1,36 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.11.35303.130 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipleApiUiTest", "MultipleApiUiTest.csproj", "{2B42751A-8650-4DE4-9B46-B01C21825EB1}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "..\Common\Common.csproj", "{3074B729-52E8-408E-8BBC-815FE9217385}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6A8B8F57-DBC6-43E2-84E7-16D24E54157B}" + ProjectSection(SolutionItems) = preProject + ..\Directory.Build.props = ..\Directory.Build.props + EndProjectSection +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Release|Any CPU.Build.0 = Release|Any CPU + {3074B729-52E8-408E-8BBC-815FE9217385}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {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 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {F7161FC1-9BC2-4CE4-B59C-504328CA6C7F} + EndGlobalSection +EndGlobal From fa3aa7bb940cfbc60b43764cd88c631bc6cf5322 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Thu, 26 Sep 2024 14:02:55 -0700 Subject: [PATCH 09/24] updating for help with debugging --- UiTests/Common/UiTestHelpers.cs | 8 +++----- UiTests/UiTests/MultiApiTest.cs | 7 ++++--- UiTests/UiTests/MultipleApiUiTest.csproj | 1 + 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 45f35961..141f6dae 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -1,9 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using Azure.Core; -using Azure.Security.KeyVault.Secrets; -using Microsoft.Playwright; +using System; +using System.Collections.Generic; using System.Diagnostics; using System.Management; using System.Runtime.Versioning; @@ -34,7 +33,7 @@ public static async Task NavigateToWebApp(string uri, IPage page) await Task.Delay(1000); InitialConnectionRetryCount--; if (InitialConnectionRetryCount == 0) - { throw; } + { throw; } } } } @@ -598,4 +597,3 @@ public ProcessStartOptions( } } } - diff --git a/UiTests/UiTests/MultiApiTest.cs b/UiTests/UiTests/MultiApiTest.cs index d32507c9..8d53aa12 100644 --- a/UiTests/UiTests/MultiApiTest.cs +++ b/UiTests/UiTests/MultiApiTest.cs @@ -23,7 +23,8 @@ public class MultiApiTest : IClassFixture private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut"; private const uint ClientPort = 44321; private const string TraceFileClassName = "OpenIDConnect"; - private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 5000 }; + private const uint NumProcessRetries = 3; + private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 15000 }; private readonly string _sampleAppPath = "3-WebApp-multi-APIs" + Path.DirectorySeparatorChar.ToString(); private readonly string _testAssemblyLocation = typeof(MultiApiTest).Assembly.Location; private readonly ITestOutputHelper _output; @@ -61,11 +62,11 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // 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, _sampleAppPath, TC.s_oidcWebAppExe, clientEnvVars); - bool areProcessesRunning = TH.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes); + bool areProcessesRunning = TH.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries); if (!areProcessesRunning) { - _output.WriteLine("Process not started after 3 attempts."); + _output.WriteLine($"Process not started after {NumProcessRetries} attempts."); StringBuilder runningProcesses = new StringBuilder(); foreach (var process in processes) { diff --git a/UiTests/UiTests/MultipleApiUiTest.csproj b/UiTests/UiTests/MultipleApiUiTest.csproj index ac90c325..18574c4d 100644 --- a/UiTests/UiTests/MultipleApiUiTest.csproj +++ b/UiTests/UiTests/MultipleApiUiTest.csproj @@ -26,6 +26,7 @@ + From 98db390ec191b6c595f8f1b4302aba808ed91b05 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Fri, 27 Sep 2024 16:55:05 -0700 Subject: [PATCH 10/24] making draft PR --- ...ultiApiTest.cs => AnyOrgOrPersonalTest.cs} | 30 +++++++++---------- ...t.csproj => AnyOrgOrPersonalUiTest.csproj} | 2 +- UiTests/UiTests/UiTests.sln | 4 +-- UiTests/UiTests/appsettings.Development.json | 9 ++++++ UiTests/UiTests/appsettings.json | 18 +++++++++++ 5 files changed, 45 insertions(+), 18 deletions(-) rename UiTests/UiTests/{MultiApiTest.cs => AnyOrgOrPersonalTest.cs} (79%) rename UiTests/UiTests/{MultipleApiUiTest.csproj => AnyOrgOrPersonalUiTest.csproj} (93%) create mode 100644 UiTests/UiTests/appsettings.Development.json create mode 100644 UiTests/UiTests/appsettings.json diff --git a/UiTests/UiTests/MultiApiTest.cs b/UiTests/UiTests/AnyOrgOrPersonalTest.cs similarity index 79% rename from UiTests/UiTests/MultiApiTest.cs rename to UiTests/UiTests/AnyOrgOrPersonalTest.cs index 8d53aa12..883d5c25 100644 --- a/UiTests/UiTests/MultiApiTest.cs +++ b/UiTests/UiTests/AnyOrgOrPersonalTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Runtime.Versioning; using System.Text; using System.Threading.Tasks; @@ -14,22 +15,21 @@ using Xunit.Abstractions; using Process = System.Diagnostics.Process; using TC = Common.TestConstants; -using TH = Common.UiTestHelpers; namespace MultipleApiUiTest { - public class MultiApiTest : IClassFixture + public class AnyOrgOrPersonalTest : IClassFixture { private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut"; private const uint ClientPort = 44321; private const string TraceFileClassName = "OpenIDConnect"; private const uint NumProcessRetries = 3; - private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 15000 }; - private readonly string _sampleAppPath = "3-WebApp-multi-APIs" + Path.DirectorySeparatorChar.ToString(); - private readonly string _testAssemblyLocation = typeof(MultiApiTest).Assembly.Location; + private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 }; + private readonly string _sampleAppPath = "1-WebApp-OIDC" + Path.DirectorySeparatorChar + "1-3-AnyOrgOrPersonal" + Path.DirectorySeparatorChar.ToString(); + private readonly string _testAssemblyLocation = typeof(AnyOrgOrPersonalTest).Assembly.Location; private readonly ITestOutputHelper _output; - public MultiApiTest(ITestOutputHelper output) + public AnyOrgOrPersonalTest(ITestOutputHelper output) { _output = output; } @@ -62,7 +62,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // 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, _sampleAppPath, TC.s_oidcWebAppExe, clientEnvVars); - bool areProcessesRunning = TH.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries); + bool areProcessesRunning = UiTestHelpers.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries); if (!areProcessesRunning) { @@ -71,7 +71,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds foreach (var process in processes) { #pragma warning disable CA1305 // Specify IFormatProvider - runningProcesses.AppendLine($"Is {process.Key} running: {TH.ProcessIsAlive(process.Value)}"); + runningProcesses.AppendLine($"Is {process.Key} running: {UiTestHelpers.ProcessIsAlive(process.Value)}"); #pragma warning restore CA1305 // Specify IFormatProvider } Assert.Fail(TC.WebAppCrashedString + " " + runningProcesses.ToString()); @@ -82,9 +82,9 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // Initial sign in _output.WriteLine("Starting web app sign-in flow."); string email = labResponse.User.Upn; - await TH.NavigateToWebApp(uriWithPort, page); - await TH.EnterEmailAsync(page, email, _output); - await TH.EnterPasswordAsync(page, labResponse.User.GetOrFetchPassword(), _output); + await UiTestHelpers.NavigateToWebApp(uriWithPort, page); + await UiTestHelpers.EnterEmailAsync(page, email, _output); + await UiTestHelpers.EnterPasswordAsync(page, labResponse.User.GetOrFetchPassword(), _output); await Assertions.Expect(page.GetByText("Integrating Azure AD V2")).ToBeVisibleAsync(_assertVisibleOptions); await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync(_assertVisibleOptions); _output.WriteLine("Web app sign-in flow successful."); @@ -92,7 +92,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // Sign out _output.WriteLine("Starting web app sign-out flow."); await page.GetByRole(AriaRole.Link, new() { Name = "Sign out" }).ClickAsync(); - await TH.PerformSignOut_MicrosoftIdFlow(page, email, TC.LocalhostUrl + ClientPort + SignOutPageUriPath, _output); + await UiTestHelpers.PerformSignOut_MicrosoftIdFlow(page, email, TC.LocalhostUrl + ClientPort + SignOutPageUriPath, _output); _output.WriteLine("Web app sign out successful."); } catch (Exception ex) @@ -111,16 +111,16 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds _output.WriteLine("No Screenshot."); } - string runningProcesses = TH.GetRunningProcessAsString(processes); + string runningProcesses = UiTestHelpers.GetRunningProcessAsString(processes); Assert.Fail($"the UI automation failed: {ex} output: {ex.Message}.\n{runningProcesses}\nTest run: {guid}"); } finally { // Add the following to make sure all processes and their children are stopped. - TH.EndProcesses(processes); + UiTestHelpers.EndProcesses(processes); // Stop tracing and export it into a zip archive. - string path = TH.GetTracePath(_testAssemblyLocation, TraceFileName); + string path = UiTestHelpers.GetTracePath(_testAssemblyLocation, TraceFileName); await context.Tracing.StopAsync(new() { Path = path }); _output.WriteLine($"Trace data for {TraceFileName} recorded to {path}."); diff --git a/UiTests/UiTests/MultipleApiUiTest.csproj b/UiTests/UiTests/AnyOrgOrPersonalUiTest.csproj similarity index 93% rename from UiTests/UiTests/MultipleApiUiTest.csproj rename to UiTests/UiTests/AnyOrgOrPersonalUiTest.csproj index 18574c4d..30bd2ccc 100644 --- a/UiTests/UiTests/MultipleApiUiTest.csproj +++ b/UiTests/UiTests/AnyOrgOrPersonalUiTest.csproj @@ -26,7 +26,7 @@ - + diff --git a/UiTests/UiTests/UiTests.sln b/UiTests/UiTests/UiTests.sln index a383d0b8..f7a38e3f 100644 --- a/UiTests/UiTests/UiTests.sln +++ b/UiTests/UiTests/UiTests.sln @@ -3,9 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.11.35303.130 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MultipleApiUiTest", "MultipleApiUiTest.csproj", "{2B42751A-8650-4DE4-9B46-B01C21825EB1}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnyOrgOrPersonalUiTest", "AnyOrgOrPersonalUiTest.csproj", "{2B42751A-8650-4DE4-9B46-B01C21825EB1}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Common", "..\Common\Common.csproj", "{3074B729-52E8-408E-8BBC-815FE9217385}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "..\Common\Common.csproj", "{3074B729-52E8-408E-8BBC-815FE9217385}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6A8B8F57-DBC6-43E2-84E7-16D24E54157B}" ProjectSection(SolutionItems) = preProject diff --git a/UiTests/UiTests/appsettings.Development.json b/UiTests/UiTests/appsettings.Development.json new file mode 100644 index 00000000..0623a3f4 --- /dev/null +++ b/UiTests/UiTests/appsettings.Development.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } +} diff --git a/UiTests/UiTests/appsettings.json b/UiTests/UiTests/appsettings.json new file mode 100644 index 00000000..b7775c52 --- /dev/null +++ b/UiTests/UiTests/appsettings.json @@ -0,0 +1,18 @@ +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "msidlab3.onmicrosoft.com", + "TenantId": "8e44f19d-bbab-4a82-b76b-4cd0a6fbc97a", + "ClientId": "d9cde0be-ad97-41e6-855e-2f85136671c1", + "CallbackPath": "/signin-oidc", + "SignedOutCallbackPath": "/signout-callback-oidc" + }, + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} From cb4a5005f1fc596187a57530b63b48c2de624c19 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Fri, 27 Sep 2024 16:55:33 -0700 Subject: [PATCH 11/24] adding temporary appsettings.json fix --- 1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json b/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json index f02dafbe..b1bb7855 100644 --- a/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json +++ b/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json @@ -1,9 +1,9 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]", - "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", - "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", + "Domain": "msidlab3.onmicrosoft.com", + "TenantId": "8e44f19d-bbab-4a82-b76b-4cd0a6fbc97a", + "ClientId": "d9cde0be-ad97-41e6-855e-2f85136671c1", "CallbackPath": "/signin-oidc", "SignedOutCallbackPath": "/signout-callback-oidc" }, From 3ace0dd739594476063c9f762b133b2ee81fb2dd Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 30 Sep 2024 16:20:04 -0700 Subject: [PATCH 12/24] Created B2C UI test --- .../4-2-B2C/Client/appsettings.json | 1 - .../AnyOrgOrPersonalUiTest.csproj | 1 + .../appsettings.Development.json | 0 UiTests/B2CUiTest/B2CUiTest.cs | 148 ++++++++++++++++++ .../B2CUiTest.csproj} | 7 +- UiTests/Common/UiTestHelpers.cs | 2 +- UiTests/UiTests.sln | 6 + UiTests/UiTests/AnyOrgOrPersonalTest.cs | 134 ---------------- UiTests/UiTests/UiTests.sln | 36 ----- UiTests/UiTests/appsettings.json | 18 --- 10 files changed, 160 insertions(+), 193 deletions(-) rename UiTests/{UiTests => AnyOrgOrPersonalUiTest}/appsettings.Development.json (100%) create mode 100644 UiTests/B2CUiTest/B2CUiTest.cs rename UiTests/{UiTests/AnyOrgOrPersonalUiTest.csproj => B2CUiTest/B2CUiTest.csproj} (86%) delete mode 100644 UiTests/UiTests/AnyOrgOrPersonalTest.cs delete mode 100644 UiTests/UiTests/UiTests.sln delete mode 100644 UiTests/UiTests/appsettings.json 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/AnyOrgOrPersonalUiTest.csproj b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj index 3ecc58f1..9941510a 100644 --- a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj +++ b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj @@ -26,6 +26,7 @@ + diff --git a/UiTests/UiTests/appsettings.Development.json b/UiTests/AnyOrgOrPersonalUiTest/appsettings.Development.json similarity index 100% rename from UiTests/UiTests/appsettings.Development.json rename to UiTests/AnyOrgOrPersonalUiTest/appsettings.Development.json diff --git a/UiTests/B2CUiTest/B2CUiTest.cs b/UiTests/B2CUiTest/B2CUiTest.cs new file mode 100644 index 00000000..6eb7836c --- /dev/null +++ b/UiTests/B2CUiTest/B2CUiTest.cs @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Runtime.Versioning; +using System.Threading; +using System.Threading.Tasks; +using Azure.Identity; +using Common; +using Microsoft.Identity.Lab.Api; +using Microsoft.Playwright; +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 TodoListClientPort = 5000; + private const uint TodoListServicePort = 44332; + private const string TraceClassName = "B2C-Login"; + private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 }; + private readonly string _sampleAppPath = Path.Join("4-WebApp-your-API", "4-2-B2C"); + private readonly Uri _keyvaultUri = new("https://webappsapistests.vault.azure.net"); + private readonly ITestOutputHelper _output; + private readonly string _testAssemblyPath = typeof(B2CUiTest).Assembly.Location; + + public B2CUiTest(ITestOutputHelper output) + { + _output = output; + } + + [Fact] + [SupportedOSPlatform("windows")] + public async Task B2C_ValidCreds_LoginLogout() + { + // Web app and api environmental variable setup. + 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 = false }); + 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, _sampleAppPath + TC.s_todoListServicePath, TC.s_todoListServiceExe, serviceEnvVars); + await Task.Delay(3000); + clientProcess = UiTestHelpers.StartProcessLocally(_testAssemblyPath, _sampleAppPath + TC.s_todoListClientPath, TC.s_todoListClientExe, clientEnvVars); + + if (!UiTestHelpers.ProcessesAreAlive(new List() { clientProcess, serviceProcess })) + { + Assert.Fail(TC.WebAppCrashedString); + } + + // 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 ex) + { + await Task.Delay(1000); + InitialConnectionRetryCount--; + if (InitialConnectionRetryCount == 0) { throw ex; } + } + } + 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 + { + // Add the following to make sure all processes and their children are stopped. + Queue processes = new Queue(); + if (serviceProcess != null) { processes.Enqueue(serviceProcess); } + if (clientProcess != null) { processes.Enqueue(clientProcess); } + UiTestHelpers.KillProcessTrees(processes); + + // Stop tracing and export it into a zip archive. + string path = UiTestHelpers.GetTracePath(_testAssemblyPath, 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/UiTests/AnyOrgOrPersonalUiTest.csproj b/UiTests/B2CUiTest/B2CUiTest.csproj similarity index 86% rename from UiTests/UiTests/AnyOrgOrPersonalUiTest.csproj rename to UiTests/B2CUiTest/B2CUiTest.csproj index 30bd2ccc..9d9bcc59 100644 --- a/UiTests/UiTests/AnyOrgOrPersonalUiTest.csproj +++ b/UiTests/B2CUiTest/B2CUiTest.csproj @@ -1,5 +1,5 @@ - - + + net8.0 @@ -26,7 +26,8 @@ - + + diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 141f6dae..c2e2c135 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -379,7 +379,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)) { 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 diff --git a/UiTests/UiTests/AnyOrgOrPersonalTest.cs b/UiTests/UiTests/AnyOrgOrPersonalTest.cs deleted file mode 100644 index 883d5c25..00000000 --- a/UiTests/UiTests/AnyOrgOrPersonalTest.cs +++ /dev/null @@ -1,134 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Runtime.Versioning; -using System.Text; -using System.Threading.Tasks; -using Common; -using Microsoft.Identity.Lab.Api; -using Microsoft.Playwright; -using Xunit; -using Xunit.Abstractions; -using Process = System.Diagnostics.Process; -using TC = Common.TestConstants; - -namespace MultipleApiUiTest -{ - public class AnyOrgOrPersonalTest : IClassFixture - { - private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut"; - private const uint ClientPort = 44321; - private const string TraceFileClassName = "OpenIDConnect"; - private const uint NumProcessRetries = 3; - private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 }; - private readonly string _sampleAppPath = "1-WebApp-OIDC" + Path.DirectorySeparatorChar + "1-3-AnyOrgOrPersonal" + Path.DirectorySeparatorChar.ToString(); - private readonly string _testAssemblyLocation = typeof(AnyOrgOrPersonalTest).Assembly.Location; - private readonly ITestOutputHelper _output; - - public AnyOrgOrPersonalTest(ITestOutputHelper output) - { - _output = output; - } - - [Fact] - [SupportedOSPlatform("windows")] - public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_LoginLogout() - { - // Setup web app and api environmental variables. - var clientEnvVars = new Dictionary - { - {"ASPNETCORE_ENVIRONMENT", "Development"}, - {TC.KestrelEndpointEnvVar, TC.HttpsStarColon + ClientPort} - }; - - Dictionary? processes = null; - - // 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 }); - 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(); - string uriWithPort = TC.LocalhostUrl + ClientPort; - - 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 - var clientProcessOptions = new ProcessStartOptions(_testAssemblyLocation, _sampleAppPath, TC.s_oidcWebAppExe, clientEnvVars); - - bool areProcessesRunning = UiTestHelpers.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries); - - if (!areProcessesRunning) - { - _output.WriteLine($"Process not started after {NumProcessRetries} attempts."); - StringBuilder runningProcesses = new StringBuilder(); - foreach (var process in processes) - { -#pragma warning disable CA1305 // Specify IFormatProvider - runningProcesses.AppendLine($"Is {process.Key} running: {UiTestHelpers.ProcessIsAlive(process.Value)}"); -#pragma warning restore CA1305 // Specify IFormatProvider - } - Assert.Fail(TC.WebAppCrashedString + " " + runningProcesses.ToString()); - } - - LabResponse labResponse = await LabUserHelper.GetSpecificUserAsync(TC.OIDCUser); - - // Initial sign in - _output.WriteLine("Starting web app sign-in flow."); - string email = labResponse.User.Upn; - await UiTestHelpers.NavigateToWebApp(uriWithPort, page); - await UiTestHelpers.EnterEmailAsync(page, email, _output); - await UiTestHelpers.EnterPasswordAsync(page, labResponse.User.GetOrFetchPassword(), _output); - await Assertions.Expect(page.GetByText("Integrating Azure AD V2")).ToBeVisibleAsync(_assertVisibleOptions); - await Assertions.Expect(page.GetByText(email)).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(); - await UiTestHelpers.PerformSignOut_MicrosoftIdFlow(page, email, TC.LocalhostUrl + ClientPort + SignOutPageUriPath, _output); - _output.WriteLine("Web app sign out successful."); - } - catch (Exception ex) - { - //Adding guid incase of multiple test runs. This will allow screenshots to be matched to their appropriet test runs. - var guid = Guid.NewGuid().ToString(); - try - { - if (page != null) - { - await page.ScreenshotAsync(new PageScreenshotOptions() { Path = $"ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_TodoAppFunctionsCorrectlyScreenshotFail{guid}.png", FullPage = true }); - } - } - catch - { - _output.WriteLine("No Screenshot."); - } - - string runningProcesses = UiTestHelpers.GetRunningProcessAsString(processes); - Assert.Fail($"the UI automation failed: {ex} output: {ex.Message}.\n{runningProcesses}\nTest run: {guid}"); - } - finally - { - // Add the following to make sure all processes and their children are stopped. - 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/UiTests/UiTests.sln b/UiTests/UiTests/UiTests.sln deleted file mode 100644 index f7a38e3f..00000000 --- a/UiTests/UiTests/UiTests.sln +++ /dev/null @@ -1,36 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 17 -VisualStudioVersion = 17.11.35303.130 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AnyOrgOrPersonalUiTest", "AnyOrgOrPersonalUiTest.csproj", "{2B42751A-8650-4DE4-9B46-B01C21825EB1}" -EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "..\Common\Common.csproj", "{3074B729-52E8-408E-8BBC-815FE9217385}" -EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{6A8B8F57-DBC6-43E2-84E7-16D24E54157B}" - ProjectSection(SolutionItems) = preProject - ..\Directory.Build.props = ..\Directory.Build.props - EndProjectSection -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {2B42751A-8650-4DE4-9B46-B01C21825EB1}.Release|Any CPU.Build.0 = Release|Any CPU - {3074B729-52E8-408E-8BBC-815FE9217385}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {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 - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {F7161FC1-9BC2-4CE4-B59C-504328CA6C7F} - EndGlobalSection -EndGlobal diff --git a/UiTests/UiTests/appsettings.json b/UiTests/UiTests/appsettings.json deleted file mode 100644 index b7775c52..00000000 --- a/UiTests/UiTests/appsettings.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "AzureAd": { - "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab3.onmicrosoft.com", - "TenantId": "8e44f19d-bbab-4a82-b76b-4cd0a6fbc97a", - "ClientId": "d9cde0be-ad97-41e6-855e-2f85136671c1", - "CallbackPath": "/signin-oidc", - "SignedOutCallbackPath": "/signout-callback-oidc" - }, - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} From 25a9e4b756ee70b1f79f66e318f4798752312998 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Thu, 3 Oct 2024 17:52:48 -0700 Subject: [PATCH 13/24] sort usings --- UiTests/B2CUiTest/B2CUiTest.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/UiTests/B2CUiTest/B2CUiTest.cs b/UiTests/B2CUiTest/B2CUiTest.cs index 6eb7836c..77196c74 100644 --- a/UiTests/B2CUiTest/B2CUiTest.cs +++ b/UiTests/B2CUiTest/B2CUiTest.cs @@ -1,17 +1,16 @@ // 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; using System.Threading.Tasks; -using Azure.Identity; -using Common; -using Microsoft.Identity.Lab.Api; -using Microsoft.Playwright; using Xunit; using Xunit.Abstractions; using TC = Common.TestConstants; From 0a4f825ce350744bd482f437fa98d5b58780eaac Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 8 Oct 2024 12:32:25 -0700 Subject: [PATCH 14/24] updated test to use refactored helpers --- UiTests/B2CUiTest/B2CUiTest.cs | 35 ++++++++++++------------------ UiTests/B2CUiTest/B2CUiTest.csproj | 5 ++--- UiTests/Common/UiTestHelpers.cs | 6 ++--- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/UiTests/B2CUiTest/B2CUiTest.cs b/UiTests/B2CUiTest/B2CUiTest.cs index 77196c74..2f6c4ba0 100644 --- a/UiTests/B2CUiTest/B2CUiTest.cs +++ b/UiTests/B2CUiTest/B2CUiTest.cs @@ -18,19 +18,21 @@ namespace B2CUiTest { public class B2CUiTest : IClassFixture - { + {// if some other app is listening on the port I want can I kick it off? Also, could I kill the process using the port or at least ID it? 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 uint TodoListClientPort = 5000; private const uint TodoListServicePort = 44332; private const string TraceClassName = "B2C-Login"; private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 }; - private readonly string _sampleAppPath = Path.Join("4-WebApp-your-API", "4-2-B2C"); + private readonly string _sampleClientAppPath = Path.Join("4-WebApp-your-API", "4-2-B2C", TC.s_todoListClientPath); + private readonly string _sampleServiceAppPath = Path.Join("4-WebApp-your-API", "4-2-B2C", TC.s_todoListServicePath); private readonly Uri _keyvaultUri = new("https://webappsapistests.vault.azure.net"); private readonly ITestOutputHelper _output; - private readonly string _testAssemblyPath = typeof(B2CUiTest).Assembly.Location; + private readonly string _testAssemblyLocation = typeof(B2CUiTest).Assembly.Location; public B2CUiTest(ITestOutputHelper output) { @@ -42,6 +44,7 @@ public B2CUiTest(ITestOutputHelper output) 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 @@ -67,21 +70,14 @@ public async Task B2C_ValidCreds_LoginLogout() 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, _sampleAppPath + TC.s_todoListServicePath, TC.s_todoListServiceExe, serviceEnvVars); - await Task.Delay(3000); - clientProcess = UiTestHelpers.StartProcessLocally(_testAssemblyPath, _sampleAppPath + TC.s_todoListClientPath, TC.s_todoListClientExe, clientEnvVars); + 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); - if (!UiTestHelpers.ProcessesAreAlive(new List() { clientProcess, serviceProcess })) - { - Assert.Fail(TC.WebAppCrashedString); - } + 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(); @@ -93,11 +89,11 @@ public async Task B2C_ValidCreds_LoginLogout() await page.GotoAsync(TC.LocalhostUrl + TodoListClientPort); break; } - catch (PlaywrightException ex) + catch (PlaywrightException) { await Task.Delay(1000); InitialConnectionRetryCount--; - if (InitialConnectionRetryCount == 0) { throw ex; } + if (InitialConnectionRetryCount == 0) { throw; } } } LabResponse labResponse = await LabUserHelper.GetB2CLocalAccountAsync(); @@ -127,14 +123,11 @@ public async Task B2C_ValidCreds_LoginLogout() } finally { - // Add the following to make sure all processes and their children are stopped. - Queue processes = new Queue(); - if (serviceProcess != null) { processes.Enqueue(serviceProcess); } - if (clientProcess != null) { processes.Enqueue(clientProcess); } - UiTestHelpers.KillProcessTrees(processes); + // End all processes. + UiTestHelpers.EndProcesses(processes); // Stop tracing and export it into a zip archive. - string path = UiTestHelpers.GetTracePath(_testAssemblyPath, TraceFileName); + string path = UiTestHelpers.GetTracePath(_testAssemblyLocation, TraceFileName); await context.Tracing.StopAsync(new() { Path = path }); _output.WriteLine($"Trace data for {TraceFileName} recorded to {path}."); diff --git a/UiTests/B2CUiTest/B2CUiTest.csproj b/UiTests/B2CUiTest/B2CUiTest.csproj index 9d9bcc59..871ae2d6 100644 --- a/UiTests/B2CUiTest/B2CUiTest.csproj +++ b/UiTests/B2CUiTest/B2CUiTest.csproj @@ -7,6 +7,7 @@ + @@ -26,8 +27,6 @@ - - - + \ No newline at end of file diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index a77c6f5e..c854bde3 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -1,8 +1,9 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -using System; -using System.Collections.Generic; +using Azure.Core; +using Azure.Security.KeyVault.Secrets; +using Microsoft.Playwright; using System.Diagnostics; using System.Management; using System.Runtime.Versioning; @@ -421,7 +422,6 @@ public static bool StartAndVerifyProcessesAreRunning(List p if (!UiTestHelpers.ProcessesAreAlive(processes.Values.ToList())) { RestartProcesses(processes, processDataEntries); } else { break; } } - } if (!UiTestHelpers.ProcessesAreAlive(processes.Values.ToList())) { From 9490e6eaf761e843642c0d9e76fd0f2f380ca6fc Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 8 Oct 2024 12:32:50 -0700 Subject: [PATCH 15/24] removed extraneous comment --- UiTests/B2CUiTest/B2CUiTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UiTests/B2CUiTest/B2CUiTest.cs b/UiTests/B2CUiTest/B2CUiTest.cs index 2f6c4ba0..e9990c6e 100644 --- a/UiTests/B2CUiTest/B2CUiTest.cs +++ b/UiTests/B2CUiTest/B2CUiTest.cs @@ -18,7 +18,7 @@ namespace B2CUiTest { public class B2CUiTest : IClassFixture - {// if some other app is listening on the port I want can I kick it off? Also, could I kill the process using the port or at least ID it? + { private const string KeyvaultEmailName = "IdWeb-B2C-user"; private const string KeyvaultPasswordName = "IdWeb-B2C-password"; private const string KeyvaultClientSecretName = "IdWeb-B2C-Client-ClientSecret"; From 04cd23e6af3c2a19bdc36f0ce24e7467de4a3a87 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 8 Oct 2024 13:44:02 -0700 Subject: [PATCH 16/24] Added sample build logic to test --- .../AnyOrgOrPersonalTest.cs | 2 +- UiTests/B2CUiTest/B2CUiTest.cs | 11 +++++++++-- UiTests/Common/UiTestHelpers.cs | 18 ++++++++++++------ 3 files changed, 22 insertions(+), 9 deletions(-) diff --git a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs index e7aeddbd..a8e18b8e 100644 --- a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs +++ b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs @@ -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 index e9990c6e..41e71c14 100644 --- a/UiTests/B2CUiTest/B2CUiTest.cs +++ b/UiTests/B2CUiTest/B2CUiTest.cs @@ -24,12 +24,15 @@ public class B2CUiTest : IClassFixture 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 = Path.Join("4-WebApp-your-API", "4-2-B2C", TC.s_todoListClientPath); - private readonly string _sampleServiceAppPath = Path.Join("4-WebApp-your-API", "4-2-B2C", TC.s_todoListServicePath); + 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; @@ -37,6 +40,8 @@ public class B2CUiTest : IClassFixture public B2CUiTest(ITestOutputHelper output) { _output = output; + _sampleClientAppPath = Path.Join(_samplePath, TC.s_todoListClientPath); + _sampleServiceAppPath = Path.Join(_samplePath, TC.s_todoListServicePath); } [Fact] @@ -72,6 +77,8 @@ public async Task B2C_ValidCreds_LoginLogout() 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 diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index c854bde3..35162ff5 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 @@ -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)); + } } /// From b246a80026529bec20b0e2a0dab653fbe8d7c554 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 8 Oct 2024 20:03:29 -0700 Subject: [PATCH 17/24] removing unneeded changes --- .../1-3-AnyOrgOrPersonal/appsettings.json | 6 +++--- 3-WebApp-multi-APIs/appsettings.json | 20 +++++++++++-------- 2 files changed, 15 insertions(+), 11 deletions(-) diff --git a/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json b/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json index b1bb7855..f02dafbe 100644 --- a/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json +++ b/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json @@ -1,9 +1,9 @@ { "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab3.onmicrosoft.com", - "TenantId": "8e44f19d-bbab-4a82-b76b-4cd0a6fbc97a", - "ClientId": "d9cde0be-ad97-41e6-855e-2f85136671c1", + "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]", + "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", + "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", "CallbackPath": "/signin-oidc", "SignedOutCallbackPath": "/signout-callback-oidc" }, diff --git a/3-WebApp-multi-APIs/appsettings.json b/3-WebApp-multi-APIs/appsettings.json index df47a4f9..db5b5e4a 100644 --- a/3-WebApp-multi-APIs/appsettings.json +++ b/3-WebApp-multi-APIs/appsettings.json @@ -1,17 +1,21 @@ -{ +{{ "AzureAd": { "Instance": "https://login.microsoftonline.com/", - "Domain": "msidlab3.onmicrosoft.com", - "TenantId": "8e44f19d-bbab-4a82-b76b-4cd0a6fbc97a", - "ClientId": "d9cde0be-ad97-41e6-855e-2f85136671c1", + "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]", + "TenantId": "[Enter 'common', or 'organizations' or the Tenant Id (Obtained from the Azure portal. Select 'Endpoints' from the 'App registrations' blade and use the GUID in any of the URLs), e.g. da41245a5-11b3-996c-00a8-4d99re19f292]", + "ClientId": "[Enter the Client Id (Application ID obtained from the Azure portal), e.g. ba74781c2-53c2-442a-97c2-3d60re42f403]", "CallbackPath": "/signin-oidc", - "SignedOutCallbackPath": "/signout-callback-oidc" + "SignedOutCallbackPath": "/signout-callback-oidc", + + // To call an API + "ClientSecret": "[Copy the client secret added to the app from the Azure portal]" + }, "Logging": { "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" } }, "AllowedHosts": "*", From 6154bc007b2a7b0a544b5962fa81c45b2d86fd84 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:04:24 -0700 Subject: [PATCH 18/24] Remove extra brace --- 3-WebApp-multi-APIs/appsettings.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/3-WebApp-multi-APIs/appsettings.json b/3-WebApp-multi-APIs/appsettings.json index db5b5e4a..919da2d1 100644 --- a/3-WebApp-multi-APIs/appsettings.json +++ b/3-WebApp-multi-APIs/appsettings.json @@ -1,4 +1,4 @@ -{{ +{ "AzureAd": { "Instance": "https://login.microsoftonline.com/", "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]", @@ -20,4 +20,4 @@ }, "AllowedHosts": "*", "GraphApiUrl": "https://graph.microsoft.com" -} \ No newline at end of file +} From d264824b9d98812c02e970b43bc56c3c2570ac47 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 8 Oct 2024 20:06:07 -0700 Subject: [PATCH 19/24] removing unneeded code --- UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj index 9941510a..3ecc58f1 100644 --- a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj +++ b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj @@ -26,7 +26,6 @@ - From 9a58e06a0e682dfb101928ce44a1dfc4e28556e4 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 8 Oct 2024 20:07:41 -0700 Subject: [PATCH 20/24] removing duplicated file --- .../AnyOrgOrPersonalUiTest/appsettings.Development.json | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 UiTests/AnyOrgOrPersonalUiTest/appsettings.Development.json diff --git a/UiTests/AnyOrgOrPersonalUiTest/appsettings.Development.json b/UiTests/AnyOrgOrPersonalUiTest/appsettings.Development.json deleted file mode 100644 index 0623a3f4..00000000 --- a/UiTests/AnyOrgOrPersonalUiTest/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} From 5406c4337f2ba7dce5cb7bab2e84c640c8aceb55 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 8 Oct 2024 20:11:49 -0700 Subject: [PATCH 21/24] fixing spacing --- UiTests/Common/UiTestHelpers.cs | 4 ++-- UiTests/Directory.Build.props | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 35162ff5..75a467f7 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -489,7 +489,7 @@ private static void SwapFiles(string path1, string path2) { // Write the contents of file1 to file2 File.WriteAllText(path2, file1Contents); - } + } catch (Exception) { // If the second write fails, revert the first write @@ -500,7 +500,7 @@ private static void SwapFiles(string path1, string path2) Console.WriteLine("File contents swapped successfully."); } - /// + /// /// Builds the solution at the given path. /// /// Absolute path to the sln file to be built diff --git a/UiTests/Directory.Build.props b/UiTests/Directory.Build.props index ce0bb340..80a4b8ef 100644 --- a/UiTests/Directory.Build.props +++ b/UiTests/Directory.Build.props @@ -17,8 +17,8 @@ 8.0.4 2.9.1 2.9.1 - 2.8.2 - 2.9.1 + 2.8.2 + 2.9.1 From f15e6ec0aa561397552a899e2c36d9f6b89ec467 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Tue, 8 Oct 2024 20:17:13 -0700 Subject: [PATCH 22/24] Remove extra space --- UiTests/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UiTests/Directory.Build.props b/UiTests/Directory.Build.props index 80a4b8ef..98953e18 100644 --- a/UiTests/Directory.Build.props +++ b/UiTests/Directory.Build.props @@ -20,5 +20,5 @@ 2.8.2 2.9.1 - + From 26c1dfc6effdc586692827a000b8c4b80c485bd9 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 8 Oct 2024 20:20:06 -0700 Subject: [PATCH 23/24] setting headless to true --- UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs | 2 +- UiTests/B2CUiTest/B2CUiTest.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs index a8e18b8e..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(); diff --git a/UiTests/B2CUiTest/B2CUiTest.cs b/UiTests/B2CUiTest/B2CUiTest.cs index 41e71c14..10ca6c03 100644 --- a/UiTests/B2CUiTest/B2CUiTest.cs +++ b/UiTests/B2CUiTest/B2CUiTest.cs @@ -71,7 +71,7 @@ public async Task B2C_ValidCreds_LoginLogout() // 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 = 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 }); From 0a6e26a3aed1e38d2a8495568bfd199c793786cd Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Wed, 9 Oct 2024 13:28:23 -0700 Subject: [PATCH 24/24] address PR feedback --- UiTests/B2CUiTest/B2CUiTest.csproj | 1 - UiTests/Directory.Build.props | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/UiTests/B2CUiTest/B2CUiTest.csproj b/UiTests/B2CUiTest/B2CUiTest.csproj index 871ae2d6..78d257c8 100644 --- a/UiTests/B2CUiTest/B2CUiTest.csproj +++ b/UiTests/B2CUiTest/B2CUiTest.csproj @@ -2,7 +2,6 @@ net8.0 - false 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