From e9988319fc60117fbd7c254f7f1236d1272f85ae Mon Sep 17 00:00:00 2001 From: Josh Lozensky <joshlozensky@microsoft.com> Date: Wed, 30 Oct 2024 18:23:19 -0700 Subject: [PATCH 1/3] Create test for hybrid flow sample --- .../2-5-HybridFlow/appsettings.json | Bin 1988 -> 970 bytes .../AnyOrgOrPersonalTest.cs | 1 - UiTests/HybridFlowUiTest/HybridFlowTest.cs | 141 ++++++++++++++++++ .../HybridFlowUiTest/HybridFlowUiTest.csproj | 31 ++++ UiTests/HybridFlowUiTest/appsettings.json | 28 ++++ UiTests/UiTests.sln | 10 +- 6 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 UiTests/HybridFlowUiTest/HybridFlowTest.cs create mode 100644 UiTests/HybridFlowUiTest/HybridFlowUiTest.csproj create mode 100644 UiTests/HybridFlowUiTest/appsettings.json diff --git a/2-WebApp-graph-user/2-5-HybridFlow/appsettings.json b/2-WebApp-graph-user/2-5-HybridFlow/appsettings.json index 5321a5906c07f8545e910c505138107911864a05..1a4512ff58e2413a27ab590e42b58da1390ac792 100644 GIT binary patch literal 970 zcmaJ=OK;mS48HeQ2)<++YA1HwhkMJCVhd29L$jcVq6bEzZDERN$V!X0$bTQDH14*; zE{6Sy`W`g-b|OSFd)+ylspLw$@h1aI+k3DQ8A)32yZ&lCHq>aFZMAgNQ|-wbZ84Ly z9Ve%UTMOEPw&Bjhwe{$PZ&9cS3aZ5m?HED})}IQ@noLNtp2;$@18pFDJQDtl#mivq zNIBy3E!okjASXuT8nF5mye8X^0u&?<*ThnZPq!P-mQab>(Ka5&tfC|5!DM2E2BjCH zYpXia)-%aaIhu8yaM<YHyQ9L!fI<OQiCvG8@7sF05ZW^K5<&+3_1#VX`8}e7GB3(A zI7{<<Gfk`NT&9x=T&AU}Du;Ph7gaF?o*RwU{}nh89^e>=JsB;d9l^r%-+V_C8@MPh zE^}F=XH$NZWdUh*!EaM_K5<wUbvc>-V`7EUp^u%PQ}+^s$!JgX@koGD+*nWX2)I{6 z=l<;R2Q3)00r~3(_%_gvd)-)VQ_@NvfA}Hj9G%y7NQpgHK0Ln9*S+@bs)O0iw{ucq zIKA5;&)z;1BsbLXgf<MnPcZ{G_=ILaxM+bj>`csc503SlBo6O>pqUv%mr?}K{s zh0P@kdghgI2-{hw2fpMh^tg6GU|5sJLDzn)NnDVpJewRNt|WCaR>BkHGNx2<4?y}p keg!{=K%Dgsm3BO#YZp`p5knG8%MP!~a$4l$<Ya&H8=rP8nE(I) literal 1988 zcmbuAT~8BH5QgX4#Q(6_o2rr0mO_O)1fwJ*8Y<y}c(HW1uyJ8Y*=o@6*VX4er>DDg zi(Z(M_I%C9JMYYy{r&5wJ-25TF=E@dA2zp{Wvo*6dz{wyUgpSd?9M7nm?ubjso1VK ztL&D$J=<gyR@$ZINOkR+d4k=N+tQ}Um7EvuHsgN6{u-UwUN6yjZ-?$CagxE)xxGWX z!uJe(6`nKZv>_R>o^p22tav1qp(D%{J|cEq&~;hKw@y8_uG_Wo28>_UVEo*$eYE5) zw#PU-izu(1hmJ#0I&2+IW}p$eoD3wO$(_`lbwKE)LYz%+k;z|VFYKdTpyheW+}RYH zVzp+d&Td4WWK|l4V_{4T1-}Vr5njb`>ZFvxio6L)+h&JKxn)FH%Ur%y8?#mn7g$fQ zs7xtNp`K$^`w-cCJ0+e&PUO?e-81`K2B}j!v12q}uH-Sr?$G)+;D4L-wr#NM!^;+{ zUB(X4ZCH<y*c(<uQ0QF9`^-~xcloW$@qw#W)llU)|9dSqrRyAdrd0J45QH4-UNM*d zk-=3iF0el$z7cWu@irjZZFumjAvXgg#L8%KwgneEFe5H>JLSKJ#=p2w-RNa!#8Y!} zKs2|!zB;#)27i&ud(cvzQ--fvy~A27S=3g$?w+dU^>|g~1^gyp{c4|>RZRi!CU|mc zSFi5|R!Zg+$#yqt=Jf!?dO<l{g^Xrc@>b7bZnc-4At%3MqDpu-bGH*$ir(w#?abMQ zp8U|T5>xl;slKaTE;LsAvzIx>#y5V&TB*=mGq5N4D?k$1IOAlt;6gqE66riFcaL%H zJAO<1j>ptKfKpu`a1q-p?07a}YF%>v8=|^N=uvtZ`C6p0y+uNO;uKUNn)cV`l3P{F z72WLV?!5^oeI$BIJc`P*F(%`x+B%2vqC?Hl@JQ9Q!uLY8q!{bFnybg`_<Wx@ZgOWy inu;gH+>S?IPj!ich&-EnyvQLhazF(7QYtS#AAbOrhcCeZ 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..b36165a0 --- /dev/null +++ b/UiTests/HybridFlowUiTest/HybridFlowTest.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +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 static System.Net.Mime.MediaTypeNames; +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 = 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 + { + // 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, 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 From 8d1f19cc1ca95b142d4620cd1e8cbacb93afc38c Mon Sep 17 00:00:00 2001 From: Josh Lozensky <joshlozensky@microsoft.com> Date: Wed, 30 Oct 2024 18:25:24 -0700 Subject: [PATCH 2/3] remove unneeded usings --- UiTests/HybridFlowUiTest/HybridFlowTest.cs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/UiTests/HybridFlowUiTest/HybridFlowTest.cs b/UiTests/HybridFlowUiTest/HybridFlowTest.cs index b36165a0..8e660d78 100644 --- a/UiTests/HybridFlowUiTest/HybridFlowTest.cs +++ b/UiTests/HybridFlowUiTest/HybridFlowTest.cs @@ -1,20 +1,17 @@ // 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.Linq; -using System.Net; 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 static System.Net.Mime.MediaTypeNames; using Process = System.Diagnostics.Process; using TC = Common.TestConstants; From c0de050b50622889dba324e178e156352c47a1ab Mon Sep 17 00:00:00 2001 From: Josh Lozensky <joshlozensky@microsoft.com> Date: Wed, 30 Oct 2024 18:29:56 -0700 Subject: [PATCH 3/3] minor cleanup --- UiTests/HybridFlowUiTest/HybridFlowTest.cs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/UiTests/HybridFlowUiTest/HybridFlowTest.cs b/UiTests/HybridFlowUiTest/HybridFlowTest.cs index 8e660d78..bdab75bb 100644 --- a/UiTests/HybridFlowUiTest/HybridFlowTest.cs +++ b/UiTests/HybridFlowUiTest/HybridFlowTest.cs @@ -24,7 +24,7 @@ public class HybridFlowTest : IClassFixture<InstallPlaywrightBrowserFixture> 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 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; @@ -52,7 +52,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // Arrange Playwright setup, to see the browser UI set Headless = false. const string TraceFileName = TraceFileClassName + "_LoginLogout"; using IPlaywright playwright = await Playwright.CreateAsync(); - IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = false }); + IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = true }); IBrowserContext context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true }); await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true }); IPage page = await context.NewPageAsync(); @@ -65,7 +65,12 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds // 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, SampleExeFileName, clientEnvVars); + var clientProcessOptions = new ProcessStartOptions( + _testAssemblyLocation, + _sampleAppPath, + Path.DirectorySeparatorChar.ToString() + SampleExeFileName, + clientEnvVars + ); bool areProcessesRunning = UiTestHelpers.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries);