diff --git a/2-WebApp-graph-user/2-5-HybridFlow/appsettings.json b/2-WebApp-graph-user/2-5-HybridFlow/appsettings.json index 5321a590..1a4512ff 100644 Binary files a/2-WebApp-graph-user/2-5-HybridFlow/appsettings.json and b/2-WebApp-graph-user/2-5-HybridFlow/appsettings.json differ diff --git a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs index 563f6218..3df45d55 100644 --- a/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs +++ b/UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs @@ -134,6 +134,5 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds playwright.Dispose(); } } - } } \ No newline at end of file diff --git a/UiTests/HybridFlowUiTest/HybridFlowTest.cs b/UiTests/HybridFlowUiTest/HybridFlowTest.cs new file mode 100644 index 00000000..bdab75bb --- /dev/null +++ b/UiTests/HybridFlowUiTest/HybridFlowTest.cs @@ -0,0 +1,143 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Common; +using Microsoft.Identity.Lab.Api; +using Microsoft.Playwright; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Versioning; +using System.Text; +using System.Threading.Tasks; +using Xunit; +using Xunit.Abstractions; +using Process = System.Diagnostics.Process; +using TC = Common.TestConstants; + +namespace HybridFlowUiTest +{ + public class HybridFlowTest : IClassFixture<InstallPlaywrightBrowserFixture> + { + private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut"; + private const uint ClientPort = 44321; + private const string TraceFileClassName = "OpenIDConnect-HybridFlow"; + private const uint NumProcessRetries = 3; + private const string SampleSlnFileName = "2-5-HybridFlow.sln"; + private const string SampleExeFileName = "2-5-HybridFlow.exe"; + private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 }; + private readonly string _sampleAppPath = "2-WebApp-graph-user" + Path.DirectorySeparatorChar + "2-5-HybridFlow" + Path.DirectorySeparatorChar.ToString(); + private readonly string _testAppsettingsPath = "UiTests" + Path.DirectorySeparatorChar + "HybridFlowUiTest" + Path.DirectorySeparatorChar.ToString() + TC.AppSetttingsDotJson; + private readonly string _testAssemblyLocation = typeof(HybridFlowTest).Assembly.Location; + private readonly ITestOutputHelper _output; + + public HybridFlowTest(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<string, string> + { + {"ASPNETCORE_ENVIRONMENT", "Development"}, + {TC.KestrelEndpointEnvVar, TC.HttpsStarColon + ClientPort} + }; + + Dictionary<string, Process>? 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 = true }); + IBrowserContext context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true }); + await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true }); + IPage page = await context.NewPageAsync(); + string uriWithPort = TC.LocalhostUrl + ClientPort; + + try + { + // Build the sample app with correct appsettings file. + UiTestHelpers.BuildSampleUsingTestAppsettings(_testAssemblyLocation, _sampleAppPath, _testAppsettingsPath, SampleSlnFileName); + + // Start the web app and api processes. + // The delay before starting client prevents transient devbox issue where the client fails to load the first time after rebuilding + var clientProcessOptions = new ProcessStartOptions( + _testAssemblyLocation, + _sampleAppPath, + Path.DirectorySeparatorChar.ToString() + SampleExeFileName, + clientEnvVars + ); + + bool areProcessesRunning = UiTestHelpers.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries); + + if (!areProcessesRunning) + { + _output.WriteLine($"Process not started after {NumProcessRetries} attempts."); + StringBuilder runningProcesses = new StringBuilder(); + foreach (var process in processes) + { +#pragma warning disable CA1305 // Specify IFormatProvider + runningProcesses.AppendLine($"Is {process.Key} running: {UiTestHelpers.ProcessIsAlive(process.Value)}"); +#pragma warning restore CA1305 // Specify IFormatProvider + } + Assert.Fail(TC.WebAppCrashedString + " " + runningProcesses.ToString()); + } + + LabResponse labResponse = await LabUserHelper.GetSpecificUserAsync(TC.MsidLab4User); + + // Initial sign in + _output.WriteLine("Starting web app sign-in flow."); + string email = labResponse.User.Upn; + await UiTestHelpers.NavigateToWebApp(uriWithPort, page); + await page.GetByRole(AriaRole.Link, new() { Name = "Sign in" }).ClickAsync(); + await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPassword(page, email, labResponse.User.GetOrFetchPassword(), _output); + await Assertions.Expect(page.GetByText("SPA Authorization Code")).ToBeVisibleAsync(_assertVisibleOptions); + await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync(_assertVisibleOptions); + _output.WriteLine("Web app sign-in flow successful."); + + // Sign out + _output.WriteLine("Starting web app sign-out flow."); + await page.GetByRole(AriaRole.Link, new() { Name = "Sign out" }).ClickAsync(); + await UiTestHelpers.PerformSignOut_MicrosoftIdFlow(page, email, TC.LocalhostUrl + ClientPort + SignOutPageUriPath, _output); + _output.WriteLine("Web app sign out successful."); + } + catch (Exception ex) + { + // Adding guid in case of multiple test runs. This will allow screenshots to be matched to their appropriate test runs. + var guid = Guid.NewGuid().ToString(); + try + { + if (page != null) + { + await page.ScreenshotAsync(new PageScreenshotOptions() { Path = $"ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_TodoAppFunctionsCorrectlyScreenshotFail{guid}.png", FullPage = true }); + } + } + catch + { + _output.WriteLine("No Screenshot."); + } + + string runningProcesses = UiTestHelpers.GetRunningProcessAsString(processes); + Assert.Fail($"the UI automation failed: {ex} output: {ex.Message}.\n{runningProcesses}\nTest run: {guid}"); + } + finally + { + // Make sure all processes and their children are stopped. + UiTestHelpers.EndProcesses(processes); + + // Stop tracing and export it into a zip archive. + string path = UiTestHelpers.GetTracePath(_testAssemblyLocation, TraceFileName); + await context.Tracing.StopAsync(new() { Path = path }); + _output.WriteLine($"Trace data for {TraceFileName} recorded to {path}."); + + // Close the browser and stop Playwright. + await browser.CloseAsync(); + playwright.Dispose(); + } + } + } +} \ No newline at end of file diff --git a/UiTests/HybridFlowUiTest/HybridFlowUiTest.csproj b/UiTests/HybridFlowUiTest/HybridFlowUiTest.csproj new file mode 100644 index 00000000..56b7a9b5 --- /dev/null +++ b/UiTests/HybridFlowUiTest/HybridFlowUiTest.csproj @@ -0,0 +1,31 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <TargetFrameworks>net8.0</TargetFrameworks> + <IsPackable>false</IsPackable> + <Nullable>enable</Nullable> + </PropertyGroup> + + <ItemGroup> + <PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftAspNetCoreMvcTestingVersion)" /> + <PackageReference Include="Microsoft.Identity.Lab.Api" Version="$(MicrosoftIdentityLabApiVersion)" /> + <PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" /> + <PackageReference Include="Microsoft.Playwright" Version="$(MicrosoftPlaywrightVersion)" /> + <PackageReference Include="System.Management" Version="$(SystemManagementVersion)" /> + <PackageReference Include="System.Text.Json" Version="$(SystemTextJsonVersion)" /> + <PackageReference Include="xunit" Version="$(XunitVersion)" /> + <PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioVersion)"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + <PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)"> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + <PrivateAssets>all</PrivateAssets> + </PackageReference> + </ItemGroup> + + <ItemGroup> + <ProjectReference Include="..\Common\Common.csproj" /> + </ItemGroup> + +</Project> diff --git a/UiTests/HybridFlowUiTest/appsettings.json b/UiTests/HybridFlowUiTest/appsettings.json new file mode 100644 index 00000000..5b999da1 --- /dev/null +++ b/UiTests/HybridFlowUiTest/appsettings.json @@ -0,0 +1,28 @@ +{ + "AzureAd": { + "Instance": "https://login.microsoftonline.com/", + "Domain": "msidlab4.onmicrosoft.com", + "TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca", + "ClientId": "9a192b78-6580-4f8a-aace-f36ffea4f7be", + "CallbackPath": "/signin-oidc", + "SignedOutCallbackPath": "/signout-callback-oidc", + "ClientCertificates": [ + { + "SourceType": "KeyVault", + "KeyVaultUrl": "https://webappsapistests.vault.azure.net", + "KeyVaultCertificateName": "Self-Signed-5-5-22" + } + ] + }, + "Logging": { + "LogLevel": { + "Default": "Warning" + } + }, + "AllowedHosts": "*", + "DownstreamApi": { + "BaseUrl": "https://graph.microsoft.com/v1.0", + "Scopes": "user.read contacts.read" + }, + "SpaRedirectUri": "https://localhost:44321/" +} diff --git a/UiTests/UiTests.sln b/UiTests/UiTests.sln index af7897cc..b8f997db 100644 --- a/UiTests/UiTests.sln +++ b/UiTests/UiTests.sln @@ -12,9 +12,11 @@ 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}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "B2CUiTest", "B2CUiTest\B2CUiTest.csproj", "{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphUserTokenCache", "GraphUserTokenCache\GraphUserTokenCache.csproj", "{B083D288-AB6E-4849-9AC2-E1DA1F727483}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GraphUserTokenCache", "GraphUserTokenCache\GraphUserTokenCache.csproj", "{B083D288-AB6E-4849-9AC2-E1DA1F727483}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "HybridFlowUiTest", "HybridFlowUiTest\HybridFlowUiTest.csproj", "{344CD55E-14C7-4999-A040-6C049F0070CB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -38,6 +40,10 @@ Global {B083D288-AB6E-4849-9AC2-E1DA1F727483}.Debug|Any CPU.Build.0 = Debug|Any CPU {B083D288-AB6E-4849-9AC2-E1DA1F727483}.Release|Any CPU.ActiveCfg = Release|Any CPU {B083D288-AB6E-4849-9AC2-E1DA1F727483}.Release|Any CPU.Build.0 = Release|Any CPU + {344CD55E-14C7-4999-A040-6C049F0070CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {344CD55E-14C7-4999-A040-6C049F0070CB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {344CD55E-14C7-4999-A040-6C049F0070CB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {344CD55E-14C7-4999-A040-6C049F0070CB}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE