From 04f5a6ad4ab1b60a3682c7f3b4e3bb55b1edeeed Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Tue, 24 Sep 2024 17:58:44 -0700 Subject: [PATCH 01/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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/25] 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 1bccda8dea661115ffa90545ea3b3eb487faa6ba Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Fri, 4 Oct 2024 11:44:28 -0700 Subject: [PATCH 08/25] adding helpers --- .../1-3-AnyOrgOrPersonal/appsettings.json | 10 +-- UiTests/Common/TestConstants.cs | 19 +++-- UiTests/Common/UiTestHelpers.cs | 84 +++++++++++++++++++ UiTests/UiTests/appsettings.Development.json | 9 -- 4 files changed, 102 insertions(+), 20 deletions(-) delete mode 100644 UiTests/UiTests/appsettings.Development.json diff --git a/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json b/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json index b7775c52..472b589a 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" }, @@ -15,4 +15,4 @@ } }, "AllowedHosts": "*" -} +} \ No newline at end of file diff --git a/UiTests/Common/TestConstants.cs b/UiTests/Common/TestConstants.cs index da869623..93b4a346 100644 --- a/UiTests/Common/TestConstants.cs +++ b/UiTests/Common/TestConstants.cs @@ -9,19 +9,26 @@ namespace Common public static class TestConstants { public const string AppSetttingsDotJson = "appsettings.json"; + public const string ClientFilePrefix = "client_"; + public const string EmailText = "Email"; public const string Headless = "headless"; public const string HeaderText = "Header"; - public const string EmailText = "Email"; + public const string HttpStarColon = "http://*:"; + public const string HttpsStarColon = "https://*:"; + public const string KestrelEndpointEnvVar = "Kestrel:Endpoints:Http:Url"; + public const string LocalhostUrl = @"https://localhost:"; + public const string OIDCUser = "fIDLAB@MSIDLAB3.com"; public const string PasswordText = "Password"; + public const string ServerFilePrefix = "server_"; 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"; + 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..3f40ed2e 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -223,6 +223,26 @@ private static string GetApplicationWorkingDirectory(string testAssemblyLocation ); } + /// + /// 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 GetAppsettingsDirectory(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 + ); + } + /// /// Creates absolute path for Playwright trace file /// @@ -428,6 +448,70 @@ public static string GetRunningProcessAsString(Dictionary? proc } return runningProcesses.ToString(); } + + /// + /// Takes two paths to existing files and swaps their names and locations effectively swapping the contents of the files. + /// + /// The path of the first file to swap + /// The path of the file to swap it with + public static void SwapFiles(string path1, string path2) + { + string tempFile = Path.GetTempFileName(); + string file1Name = Path.GetFileName(path1); + string file2Name = Path.GetFileName(path2); + string file1Dir = Path.GetDirectoryName(path1); + string file2Dir = Path.GetDirectoryName(path2); + + // Move file1 to tempFile + File.Move(path1, tempFile); + + // Move file2 to file1's original location and rename it to file1's name + File.Move(path2, Path.Combine(file1Dir, file1Name)); + + // Move tempFile (original file1) to file2's original location and rename it to file2's name + File.Move(tempFile, Path.Combine(file2Dir, file2Name)); + + Console.WriteLine("Files swapped and renamed successfully."); + } + + + public static void RebuildSolution(string solutionPath) + { + ProcessStartInfo startInfo = new ProcessStartInfo + { + FileName = "dotnet", + Arguments = $"build {solutionPath} --no-incremental", + RedirectStandardOutput = true, + RedirectStandardError = true, + UseShellExecute = false, + CreateNoWindow = true + }; + + using (Process process = new Process()) + { + process.StartInfo = startInfo; + process.OutputDataReceived += (sender, e) => Console.WriteLine(e.Data); + process.ErrorDataReceived += (sender, e) => Console.WriteLine(e.Data); + + process.Start(); + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + process.WaitForExit(); + } + + Console.WriteLine("Solution rebuild initiated."); + } + + public static void BuildSampleWithTestAppsettings(string testAssemblyLocation, string appLocation, string testAppsettingsName) + { + string appsettingsDirectory = GetAppsettingsDirectory(testAssemblyLocation, appLocation); + string appsettingsPath = Path.Combine(appsettingsDirectory, TestConstants.AppSetttingsDotJson); + string testAppsettingsPath = Path.Combine(appsettingsDirectory, testAppsettingsName); + SwapFiles(appsettingsPath, testAppsettingsPath); + RebuildSolution(appsettingsDirectory); + SwapFiles(appsettingsPath, testAppsettingsPath); + + } } /// diff --git a/UiTests/UiTests/appsettings.Development.json b/UiTests/UiTests/appsettings.Development.json deleted file mode 100644 index 0623a3f4..00000000 --- a/UiTests/UiTests/appsettings.Development.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } -} From 2e29cf0e378b5d6b132aa3087bc13f11508b2deb Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Fri, 4 Oct 2024 13:19:09 -0700 Subject: [PATCH 09/25] removing project going in a different branch --- UiTests/UiTests.sln | 6 ------ 1 file changed, 6 deletions(-) diff --git a/UiTests/UiTests.sln b/UiTests/UiTests.sln index 5d987913..b06270c2 100644 --- a/UiTests/UiTests.sln +++ b/UiTests/UiTests.sln @@ -12,8 +12,6 @@ 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 @@ -28,10 +26,6 @@ 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 02d5dc050b9c61b4945a1a7c4d1cd8b56df2c185 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Fri, 4 Oct 2024 14:45:52 -0700 Subject: [PATCH 10/25] Added building sample to test --- .../1-3-AnyOrgOrPersonal/appsettings.json | 2 +- .../AnyOrgOrPersonalTest.cs | 5 +++ .../AnyOrgOrPersonalUiTest.csproj | 1 - UiTests/Common/UiTestHelpers.cs | 44 ++++++++++--------- 4 files changed, 29 insertions(+), 23 deletions(-) diff --git a/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json b/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json index 472b589a..f02dafbe 100644 --- a/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json +++ b/1-WebApp-OIDC/1-3-AnyOrgOrPersonal/appsettings.json @@ -1,4 +1,4 @@ -{ +{ "AzureAd": { "Instance": "https://login.microsoftonline.com/", "Domain": "[Enter the domain of your tenant, e.g. contoso.onmicrosoft.com]", diff --git a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs index 883d5c25..bbce9d87 100644 --- a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs +++ b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs @@ -24,8 +24,10 @@ public class AnyOrgOrPersonalTest : IClassFixture - diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 3f40ed2e..88e19854 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -454,37 +454,32 @@ public static string GetRunningProcessAsString(Dictionary? proc /// /// The path of the first file to swap /// The path of the file to swap it with - public static void SwapFiles(string path1, string path2) + private static void SwapFiles(string path1, string path2) { - string tempFile = Path.GetTempFileName(); - string file1Name = Path.GetFileName(path1); - string file2Name = Path.GetFileName(path2); - string file1Dir = Path.GetDirectoryName(path1); - string file2Dir = Path.GetDirectoryName(path2); + // Read the contents of both files + string file1Contents = File.ReadAllText(path1); + string file2Contents = File.ReadAllText(path2); - // Move file1 to tempFile - File.Move(path1, tempFile); + // Write the contents of file2 to file1 + File.WriteAllText(path1, file2Contents); - // Move file2 to file1's original location and rename it to file1's name - File.Move(path2, Path.Combine(file1Dir, file1Name)); + // Write the contents of file1 to file2 + File.WriteAllText(path2, file1Contents); - // Move tempFile (original file1) to file2's original location and rename it to file2's name - File.Move(tempFile, Path.Combine(file2Dir, file2Name)); - - Console.WriteLine("Files swapped and renamed successfully."); + Console.WriteLine("File contents swapped successfully."); } - public static void RebuildSolution(string solutionPath) + private static void RebuildSolution(string solutionPath) { ProcessStartInfo startInfo = new ProcessStartInfo { FileName = "dotnet", - Arguments = $"build {solutionPath} --no-incremental", + Arguments = $"build {solutionPath}", RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, - CreateNoWindow = true + CreateNoWindow = false }; using (Process process = new Process()) @@ -502,15 +497,22 @@ public static void RebuildSolution(string solutionPath) Console.WriteLine("Solution rebuild initiated."); } - public static void BuildSampleWithTestAppsettings(string testAssemblyLocation, string appLocation, string testAppsettingsName) + public static void BuildSampleWithTestAppsettings( + string testAssemblyLocation, + string appLocation, + string testAppsettingsPathFromRepoRoot, + string solutionFileName + ) { string appsettingsDirectory = GetAppsettingsDirectory(testAssemblyLocation, appLocation); string appsettingsPath = Path.Combine(appsettingsDirectory, TestConstants.AppSetttingsDotJson); - string testAppsettingsPath = Path.Combine(appsettingsDirectory, testAppsettingsName); - SwapFiles(appsettingsPath, testAppsettingsPath); - RebuildSolution(appsettingsDirectory); + string testAppsettingsPath = GetAppsettingsDirectory(testAssemblyLocation, testAppsettingsPathFromRepoRoot); + SwapFiles(appsettingsPath, testAppsettingsPath); + try { RebuildSolution(appsettingsDirectory + solutionFileName); } + catch (Exception) { throw; } + finally { SwapFiles(appsettingsPath, testAppsettingsPath); } } } From 025947b7f3a6bfafd9fa3d20f20343b61beec3fd Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Fri, 4 Oct 2024 15:09:44 -0700 Subject: [PATCH 11/25] Adding notes --- UiTests/Common/UiTestHelpers.cs | 40 +++++++++++++++++++++++---------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 88e19854..c57b30e4 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -414,7 +414,7 @@ public static bool StartAndVerifyProcessesAreRunning(List p return true; } - static void RestartProcesses(Dictionary processes, List processDataEntries) + private static void RestartProcesses(Dictionary processes, List processDataEntries) { //attempt to restart failed processes foreach (KeyValuePair processEntry in processes) @@ -434,6 +434,12 @@ static void RestartProcesses(Dictionary processes, List + /// Returns a string representation of the running processes + /// + /// Dict of running processes + /// A string of all processes from the give dict public static string GetRunningProcessAsString(Dictionary? processes) { StringBuilder runningProcesses = new StringBuilder(); @@ -469,8 +475,11 @@ private static void SwapFiles(string path1, string path2) Console.WriteLine("File contents swapped successfully."); } - - private static void RebuildSolution(string solutionPath) + /// + /// Builds the solution at the given path. + /// + /// Absolute path to the sln file to be built + private static void BuildSolution(string solutionPath) { ProcessStartInfo startInfo = new ProcessStartInfo { @@ -479,7 +488,7 @@ private static void RebuildSolution(string solutionPath) RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, - CreateNoWindow = false + CreateNoWindow = true }; using (Process process = new Process()) @@ -497,22 +506,29 @@ private static void RebuildSolution(string solutionPath) Console.WriteLine("Solution rebuild initiated."); } + /// + /// Replaces existing appsettings.json file with a test appsettings file, builds the solution, and then swaps the files back. + /// + /// Absolute path to the current test's working directory + /// 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( string testAssemblyLocation, - string appLocation, - string testAppsettingsPathFromRepoRoot, + string sampleRelPath, + string testAppsettingsRelPath, string solutionFileName ) { - string appsettingsDirectory = GetAppsettingsDirectory(testAssemblyLocation, appLocation); - string appsettingsPath = Path.Combine(appsettingsDirectory, TestConstants.AppSetttingsDotJson); - string testAppsettingsPath = GetAppsettingsDirectory(testAssemblyLocation, testAppsettingsPathFromRepoRoot); + string appsettingsDirectory = GetAppsettingsDirectory(testAssemblyLocation, sampleRelPath); + string appsettingsAbsPath = Path.Combine(appsettingsDirectory, TestConstants.AppSetttingsDotJson); + string testAppsettingsAbsPath = GetAppsettingsDirectory(testAssemblyLocation, testAppsettingsRelPath); - SwapFiles(appsettingsPath, testAppsettingsPath); + SwapFiles(appsettingsAbsPath, testAppsettingsAbsPath); - try { RebuildSolution(appsettingsDirectory + solutionFileName); } + try { BuildSolution(appsettingsDirectory + solutionFileName); } catch (Exception) { throw; } - finally { SwapFiles(appsettingsPath, testAppsettingsPath); } + finally { SwapFiles(appsettingsAbsPath, testAppsettingsAbsPath); } } } From 3f14ac10d0e7dde1d939009cc905cadd9c9bea84 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Fri, 4 Oct 2024 15:12:14 -0700 Subject: [PATCH 12/25] remove unneeded edits --- 3-WebApp-multi-APIs/appsettings.json | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/3-WebApp-multi-APIs/appsettings.json b/3-WebApp-multi-APIs/appsettings.json index b7775c52..24c97a37 100644 --- a/3-WebApp-multi-APIs/appsettings.json +++ b/3-WebApp-multi-APIs/appsettings.json @@ -1,18 +1,23 @@ { "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": "*" -} + "AllowedHosts": "*", + "GraphApiUrl": "https://graph.microsoft.com" +} \ No newline at end of file From ea5f25886e679ee53487cac7a129aae556c449a9 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Fri, 4 Oct 2024 16:24:20 -0700 Subject: [PATCH 13/25] Update UiTests/Common/TestConstants.cs Co-authored-by: kellyyangsong <69649063+kellyyangsong@users.noreply.github.com> --- UiTests/Common/TestConstants.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/UiTests/Common/TestConstants.cs b/UiTests/Common/TestConstants.cs index 93b4a346..426e4fec 100644 --- a/UiTests/Common/TestConstants.cs +++ b/UiTests/Common/TestConstants.cs @@ -1,3 +1,6 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + using System; using System.Collections.Generic; using System.Linq; From 880cf53c50102645a7211dc9bc32f0f8d98ae4fa Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Fri, 4 Oct 2024 16:34:22 -0700 Subject: [PATCH 14/25] enabled nullables --- UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj index ac90c325..87959004 100644 --- a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj +++ b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj @@ -4,6 +4,7 @@ net8.0 false + enable From 4a022422ea54f535e58eadc7ad7c78c47f730a99 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:25:24 -0700 Subject: [PATCH 15/25] Update UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> --- UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs index bbce9d87..a5b86ceb 100644 --- a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs +++ b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs @@ -121,7 +121,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds } finally { - // Add the following to make sure all processes and their children are stopped. + // Make sure all processes and their children are stopped. UiTestHelpers.EndProcesses(processes); // Stop tracing and export it into a zip archive. From 4d48da002da01b4ede04f587fce244b629ea0f5c Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:25:37 -0700 Subject: [PATCH 16/25] Update UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> --- UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs index a5b86ceb..e7aeddbd 100644 --- a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs +++ b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs @@ -102,7 +102,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds } catch (Exception ex) { - //Adding guid incase of multiple test runs. This will allow screenshots to be matched to their appropriet test runs. + // Adding guid in case of multiple test runs. This will allow screenshots to be matched to their appropriate test runs. var guid = Guid.NewGuid().ToString(); try { From e8a5398073fbd78bea6d3c9d276b0895898e5cb8 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:33:25 -0700 Subject: [PATCH 17/25] Update UiTests/Common/UiTestHelpers.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> --- UiTests/Common/UiTestHelpers.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index c57b30e4..89c49e40 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -136,6 +136,7 @@ public static async Task FillEntryBox(ILocator entryBox, string entryText) await entryBox.FillAsync(entryText); await entryBox.PressAsync("Enter"); } + private static void WriteLine(ITestOutputHelper? output, string message) { if (output != null) From 7b7e203b5058a5d4661c1ec74ea1f9a2b8975346 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Mon, 7 Oct 2024 16:33:51 -0700 Subject: [PATCH 18/25] Update UiTests/Common/UiTestHelpers.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> --- UiTests/Common/UiTestHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 89c49e40..90d10852 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -150,7 +150,7 @@ private static void WriteLine(ITestOutputHelper? output, string message) } /// - /// This starts the recording of playwright trace files. The corresponsing EndAndWritePlaywrightTrace method will also need to be used. + /// This starts the recording of playwright trace files. The corresponding 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. From 6e369fc736be3b18dd750f1057c6b339845e3980 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 7 Oct 2024 17:03:09 -0700 Subject: [PATCH 19/25] Addressing PR comments --- UiTests/Common/UiTestHelpers.cs | 37 ++++++++++++++++++++++++++------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 90d10852..65ec628e 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -15,11 +15,10 @@ 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. + /// Navigates to a web page with retry logic to more reliably connect 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; @@ -124,13 +123,13 @@ public static async Task EnterPasswordAsync(IPage page, string password, ITestOu await FillEntryBox(passwordInputLocator, password); } - public static async Task StaySignedIn_MicrosoftIdFlow(IPage page, string staySignedInText, ITestOutputHelper? output = null) + private 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) + private static async Task FillEntryBox(ILocator entryBox, string entryText) { await entryBox.ClickAsync(); await entryBox.FillAsync(entryText); @@ -269,6 +268,10 @@ public static string GetTracePath(string testAssemblyLocation, string traceName) ); } + /// + /// Goes through all processes and ends them and any child processes they spawned + /// + /// public static void EndProcesses(Dictionary? processes) { Queue processQueue = new(); @@ -286,7 +289,7 @@ public static void EndProcesses(Dictionary? processes) /// Kills the processes in the queue and all of their children /// /// queue of parent processes - public static void KillProcessTrees(Queue processQueue) + private static void KillProcessTrees(Queue processQueue) { #if WINDOWS Process currentProcess; @@ -381,6 +384,13 @@ internal static async Task GetValueFromKeyvaultWitDefaultCreds(Uri keyva return (await client.GetSecretAsync(keyvaultSecretName)).Value.Value; } + /// + /// Starts all processes in the given list and verifies that they are running + /// + /// The startup options for each process to be started + /// A dictionary to hold the process objects once started + /// The number of times to retry starting a process + /// A boolean to say whether all the processes were able to start up successfully public static bool StartAndVerifyProcessesAreRunning(List processDataEntries, out Dictionary processes, uint numRetries) { processes = new Dictionary(); @@ -469,9 +479,17 @@ private static void SwapFiles(string path1, string path2) // Write the contents of file2 to file1 File.WriteAllText(path1, file2Contents); - - // Write the contents of file1 to file2 - File.WriteAllText(path2, file1Contents); + try + { + // Write the contents of file1 to file2 + File.WriteAllText(path2, file1Contents); + } + catch (Exception) + { + // If the second write fails, revert the first write + File.WriteAllText(path1, file1Contents); + throw; + } Console.WriteLine("File contents swapped successfully."); } @@ -547,6 +565,9 @@ public void Dispose() } } + /// + /// A POCO class to hold the options for starting a process + /// public class ProcessStartOptions { public string TestAssemblyLocation { get; } From 167b79e567c2102b042628e4b389f4ffd4b96a66 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 7 Oct 2024 17:11:00 -0700 Subject: [PATCH 20/25] addressed PR feedback --- .../AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj | 1 - UiTests/Directory.Build.props | 8 ++++---- UiTests/UiTests.sln | 2 +- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj index 87959004..3ecc58f1 100644 --- a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj +++ b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalUiTest.csproj @@ -2,7 +2,6 @@ net8.0 - false enable diff --git a/UiTests/Directory.Build.props b/UiTests/Directory.Build.props index 6515b104..98953e18 100644 --- a/UiTests/Directory.Build.props +++ b/UiTests/Directory.Build.props @@ -1,8 +1,8 @@ + false net8.0 - $(TargetFrameworks); net9.0 false false @@ -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.sln b/UiTests/UiTests.sln index b06270c2..f0febbc6 100644 --- a/UiTests/UiTests.sln +++ b/UiTests/UiTests.sln @@ -9,7 +9,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Common", "Common\Common.csp 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 + Directory.Build.props = Directory.Build.props EndProjectSection EndProject Global From 0913e363fea675213ab33fc9787d7b891755e3f3 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:34:20 -0700 Subject: [PATCH 21/25] Update UiTests/Common/UiTestHelpers.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> --- UiTests/Common/UiTestHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 65ec628e..2ab7c5db 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -410,7 +410,7 @@ public static bool StartAndVerifyProcessesAreRunning(List p Thread.Sleep(2000); } - //Verify that processes are running + // Verify that processes are running for (int i = 0; i < numRetries; i++) { if (!UiTestHelpers.ProcessesAreAlive(processes.Values.ToList())) { RestartProcesses(processes, processDataEntries); } From 1971a1a26f315ab4350c993cf7b961db3c154b3e Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:34:41 -0700 Subject: [PATCH 22/25] Update UiTests/Common/UiTestHelpers.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> --- UiTests/Common/UiTestHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 2ab7c5db..09280a13 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -427,7 +427,7 @@ public static bool StartAndVerifyProcessesAreRunning(List p private static void RestartProcesses(Dictionary processes, List processDataEntries) { - //attempt to restart failed processes + // attempt to restart failed processes foreach (KeyValuePair processEntry in processes) { if (!ProcessIsAlive(processEntry.Value)) From a41e865f5e3d985e71ec0447338339d21b1ef77d Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:34:47 -0700 Subject: [PATCH 23/25] Update UiTests/Common/UiTestHelpers.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> --- UiTests/Common/UiTestHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 09280a13..3ad18714 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -440,7 +440,7 @@ private static void RestartProcesses(Dictionary processes, List processDataEntry.EnvironmentVariables); Thread.Sleep(5000); - //Update process in collection + // Update process in collection processes[processEntry.Key] = process; } } From e27c8bb4528c9aaa6a8d59dbe465598b6be0da34 Mon Sep 17 00:00:00 2001 From: JLoze <103777376+JoshLozensky@users.noreply.github.com> Date: Mon, 7 Oct 2024 17:35:00 -0700 Subject: [PATCH 24/25] Update UiTests/Common/UiTestHelpers.cs Co-authored-by: Westin Musser <127992899+westin-m@users.noreply.github.com> --- UiTests/Common/UiTestHelpers.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 3ad18714..23d69ca1 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -395,7 +395,7 @@ public static bool StartAndVerifyProcessesAreRunning(List p { processes = new Dictionary(); - //Start Processes + // Start Processes foreach (ProcessStartOptions processDataEntry in processDataEntries) { var process = UiTestHelpers.StartProcessLocally( From 89854c4a939d993746cca44d52f93a9160d40e18 Mon Sep 17 00:00:00 2001 From: Josh Lozensky Date: Mon, 7 Oct 2024 17:37:58 -0700 Subject: [PATCH 25/25] added docstring --- UiTests/Common/UiTestHelpers.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/UiTests/Common/UiTestHelpers.cs b/UiTests/Common/UiTestHelpers.cs index 65ec628e..1dc7d5b5 100644 --- a/UiTests/Common/UiTestHelpers.cs +++ b/UiTests/Common/UiTestHelpers.cs @@ -73,6 +73,12 @@ public static async Task SuccessiveLogin_MicrosoftIdFlow_ValidEmailPassword(IPag await StaySignedIn_MicrosoftIdFlow(page, staySignedInText, output); } + /// + /// Enters the email of the user to sign in. + /// + /// Playwright Page object the login is occurring on + /// The email to use for the login + /// Used to communicate output to the test's Standard Output public static async Task EnterEmailAsync(IPage page, string email, ITestOutputHelper? output = null) { WriteLine(output, $"Logging in ... Entering and submitting user name: {email}.");