Skip to content

B2C UI test #793

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 27 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
04f5a6a
added Ui test for multiple API sample
JoshLozensky Sep 25, 2024
e576909
updating for help with debugging
JoshLozensky Sep 26, 2024
03e71ce
making draft PR
JoshLozensky Sep 27, 2024
f84fb66
adding temporary appsettings.json fix
JoshLozensky Sep 27, 2024
6cfd554
small constants update
JoshLozensky Sep 30, 2024
fdac0a3
Created B2C UI test
JoshLozensky Sep 30, 2024
b836757
sort usings
JoshLozensky Oct 4, 2024
871809c
added Ui test for multiple API sample
JoshLozensky Sep 25, 2024
fa3aa7b
updating for help with debugging
JoshLozensky Sep 26, 2024
98db390
making draft PR
JoshLozensky Sep 27, 2024
cb4a500
adding temporary appsettings.json fix
JoshLozensky Sep 27, 2024
3ace0dd
Created B2C UI test
JoshLozensky Sep 30, 2024
25a9e4b
sort usings
JoshLozensky Oct 4, 2024
a68efab
Rebase onto main
JoshLozensky Oct 8, 2024
0a4f825
updated test to use refactored helpers
JoshLozensky Oct 8, 2024
9490e6e
removed extraneous comment
JoshLozensky Oct 8, 2024
04cd23e
Added sample build logic to test
JoshLozensky Oct 8, 2024
b246a80
removing unneeded changes
JoshLozensky Oct 9, 2024
6154bc0
Remove extra brace
JoshLozensky Oct 9, 2024
d264824
removing unneeded code
JoshLozensky Oct 9, 2024
a5c19a1
Merge branch 'lozensky/AddingB2cUiTest' of https://github.com/Azure-S…
JoshLozensky Oct 9, 2024
9a58e06
removing duplicated file
JoshLozensky Oct 9, 2024
5406c43
fixing spacing
JoshLozensky Oct 9, 2024
f15e6ec
Remove extra space
JoshLozensky Oct 9, 2024
26c1dfc
setting headless to true
JoshLozensky Oct 9, 2024
b1ccd59
Merge branch 'lozensky/AddingB2cUiTest' of https://github.com/Azure-S…
JoshLozensky Oct 9, 2024
0a6e26a
address PR feedback
JoshLozensky Oct 9, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion 3-WebApp-multi-APIs/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
},
"AllowedHosts": "*",
"GraphApiUrl": "https://graph.microsoft.com"
}
}
1 change: 0 additions & 1 deletion 4-WebApp-your-API/4-2-B2C/Client/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 2 additions & 2 deletions UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -61,7 +61,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds
try
{
// Build the sample app with correct appsettings file.
UiTestHelpers.BuildSampleWithTestAppsettings(_testAssemblyLocation, _sampleAppPath, _testAppsettingsPath, SampleSlnFileName);
UiTestHelpers.BuildSampleUsingTestAppsettings(_testAssemblyLocation, _sampleAppPath, _testAppsettingsPath, SampleSlnFileName);

// Start the web app and api processes.
// The delay before starting client prevents transient devbox issue where the client fails to load the first time after rebuilding
Expand Down
147 changes: 147 additions & 0 deletions UiTests/B2CUiTest/B2CUiTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Azure.Identity;
using Common;
using Microsoft.Identity.Lab.Api;
using Microsoft.Playwright;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.Versioning;
using System.Threading.Tasks;
using Xunit;
using Xunit.Abstractions;
using TC = Common.TestConstants;

namespace B2CUiTest
{
public class B2CUiTest : IClassFixture<InstallPlaywrightBrowserFixture>
{
private const string KeyvaultEmailName = "IdWeb-B2C-user";
private const string KeyvaultPasswordName = "IdWeb-B2C-password";
private const string KeyvaultClientSecretName = "IdWeb-B2C-Client-ClientSecret";
private const string NameOfUser = "unknown";
private const uint ProcessStartupRetryNum = 3;
private const string SampleSolutionFileName = "4-2-B2C-Secured-API.sln";
private const uint TodoListClientPort = 5000;
private const uint TodoListServicePort = 44332;
private const string TraceClassName = "B2C-Login";

private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 };
private readonly string _sampleClientAppPath;
private readonly string _samplePath = Path.Join("4-WebApp-your-API", "4-2-B2C");
private readonly string _sampleServiceAppPath;
private readonly Uri _keyvaultUri = new("https://webappsapistests.vault.azure.net");
private readonly ITestOutputHelper _output;
private readonly string _testAssemblyLocation = typeof(B2CUiTest).Assembly.Location;

public B2CUiTest(ITestOutputHelper output)
{
_output = output;
_sampleClientAppPath = Path.Join(_samplePath, TC.s_todoListClientPath);
_sampleServiceAppPath = Path.Join(_samplePath, TC.s_todoListServicePath);
}

[Fact]
[SupportedOSPlatform("windows")]
public async Task B2C_ValidCreds_LoginLogout()
{
// Web app and api environmental variable setup.
Dictionary<string, Process>? processes = null;

Check warning on line 52 in UiTests/B2CUiTest/B2CUiTest.cs

View workflow job for this annotation

GitHub Actions / build

The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check the warning, there is one in the GitHub UI

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

enabled nullable in B2C UI test csproj

DefaultAzureCredential azureCred = new();
string clientSecret = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultClientSecretName, azureCred);
var serviceEnvVars = new Dictionary<string, string>
{
{"ASPNETCORE_ENVIRONMENT", "Development" },
{TC.KestrelEndpointEnvVar, TC.HttpStarColon + TodoListServicePort}
};
var clientEnvVars = new Dictionary<string, string>
{
{"ASPNETCORE_ENVIRONMENT", "Development"},
{"AzureAdB2C__ClientSecret", clientSecret},
{TC.KestrelEndpointEnvVar, TC.HttpsStarColon + TodoListClientPort}
};

// Get email and password from keyvault.
string email = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultEmailName, azureCred);
string password = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultPasswordName, azureCred);

// Playwright setup. To see browser UI, set 'Headless = false'.
const string TraceFileName = TraceClassName + "_TodoAppFunctionsCorrectly";
using IPlaywright playwright = await Playwright.CreateAsync();
IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = true });
IBrowserContext context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true });
await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true });

try
{
UiTestHelpers.BuildSampleUsingSampleAppsettings(_testAssemblyLocation, _samplePath, SampleSolutionFileName);

// Start the web app and api processes.
// The delay before starting client prevents transient devbox issue where the client fails to load the first time after rebuilding.
var clientProcessOptions = new ProcessStartOptions(_testAssemblyLocation, _sampleClientAppPath, TC.s_todoListClientExe, clientEnvVars); // probs need to add client specific path
var serviceProcessOptions = new ProcessStartOptions(_testAssemblyLocation, _sampleServiceAppPath, TC.s_todoListServiceExe, serviceEnvVars);

UiTestHelpers.StartAndVerifyProcessesAreRunning([serviceProcessOptions, clientProcessOptions], out processes, ProcessStartupRetryNum);

// Navigate to web app the retry logic ensures the web app has time to start up to establish a connection.
IPage page = await context.NewPageAsync();
uint InitialConnectionRetryCount = 5;
while (InitialConnectionRetryCount > 0)
{
try
{
await page.GotoAsync(TC.LocalhostUrl + TodoListClientPort);
break;
}
catch (PlaywrightException)
{
await Task.Delay(1000);
InitialConnectionRetryCount--;
if (InitialConnectionRetryCount == 0) { throw; }
}
}
LabResponse labResponse = await LabUserHelper.GetB2CLocalAccountAsync();

// Initial sign in
_output.WriteLine("Starting web app sign-in flow.");
ILocator emailEntryBox = page.GetByPlaceholder("Email Address");
await emailEntryBox.FillAsync(email);
await emailEntryBox.PressAsync("Tab");
await page.GetByPlaceholder("Password").FillAsync(password);
await page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();
await Assertions.Expect(page.GetByText("TodoList")).ToBeVisibleAsync(_assertVisibleOptions);
await Assertions.Expect(page.GetByText(NameOfUser)).ToBeVisibleAsync(_assertVisibleOptions);
_output.WriteLine("Web app sign-in flow successful.");

// Sign out
_output.WriteLine("Starting web app sign-out flow.");
await page.GetByRole(AriaRole.Link, new() { Name = "Sign out" }).ClickAsync();
_output.WriteLine("Signing out ...");
await Assertions.Expect(page.GetByText("You have successfully signed out.")).ToBeVisibleAsync(_assertVisibleOptions);
await Assertions.Expect(page.GetByText(NameOfUser)).ToBeHiddenAsync();
_output.WriteLine("Web app sign out successful.");
}
catch (Exception ex)
{
Assert.Fail($"the UI automation failed: {ex} output: {ex.Message}.");
}
finally
{
// End all processes.
UiTestHelpers.EndProcesses(processes);

// Stop tracing and export it into a zip archive.
string path = UiTestHelpers.GetTracePath(_testAssemblyLocation, TraceFileName);
await context.Tracing.StopAsync(new() { Path = path });
_output.WriteLine($"Trace data for {TraceFileName} recorded to {path}.");

// Close the browser and stop Playwright.
await browser.CloseAsync();
playwright.Dispose();
}
}
}
}
32 changes: 32 additions & 0 deletions UiTests/B2CUiTest/B2CUiTest.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>net8.0</TargetFrameworks>
<!--<TargetFrameworks Condition="'$(TargetNet9)'== 'True'">$(TargetFrameworks); net9.0</TargetFrameworks>-->
<IsPackable>false</IsPackable>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="Azure.Identity" Version="1.12.1" />
<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>
21 changes: 13 additions & 8 deletions UiTests/Common/UiTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ private static string GetApplicationWorkingDirectory(string testAssemblyLocation
/// <param name="testAssemblyLocation">The path to the test's directory</param>
/// <param name="appLocation">The path to the processes directory</param>
/// <returns>The path to the directory for the given app</returns>
private static string GetAppsettingsDirectory(string testAssemblyLocation, string appLocation)
private static string GetAbsoluteAppDirectory(string testAssemblyLocation, string appLocation)
{
string testedAppLocation = Path.GetDirectoryName(testAssemblyLocation)!;
// e.g. microsoft-identity-web\tests\E2E Tests\WebAppUiTests\bin\Debug\net6.0
Expand Down Expand Up @@ -380,7 +380,7 @@ public static void InstallPlaywrightBrowser()
/// <param name="keyvaultSecretName">The name of the secret</param>
/// <returns>The value of the secret from key vault</returns>
/// <exception cref="ArgumentNullException">Throws if no secret name is provided</exception>
internal static async Task<string> GetValueFromKeyvaultWitDefaultCreds(Uri keyvaultUri, string keyvaultSecretName, TokenCredential creds)
public static async Task<string> GetValueFromKeyvaultWitDefaultCreds(Uri keyvaultUri, string keyvaultSecretName, TokenCredential creds)
{
if (string.IsNullOrEmpty(keyvaultSecretName))
{
Expand Down Expand Up @@ -528,7 +528,7 @@ private static void BuildSolution(string solutionPath)
process.WaitForExit();
}

Console.WriteLine("Solution rebuild initiated.");
Console.WriteLine("Solution build initiated.");
}

/// <summary>
Expand All @@ -538,23 +538,29 @@ private static void BuildSolution(string solutionPath)
/// <param name="sampleRelPath">Relative path to the sample app to build starting at the repo's root, does not include appsettings filename</param>
/// <param name="testAppsettingsRelPath">Relative path to the test appsettings file starting at the repo's root, includes appsettings filename</param>
/// <param name="solutionFileName">Filename for the sln file to build</param>
public static void BuildSampleWithTestAppsettings(
public static void BuildSampleUsingTestAppsettings(
string testAssemblyLocation,
string sampleRelPath,
string testAppsettingsRelPath,
string solutionFileName
)
{
string appsettingsDirectory = GetAppsettingsDirectory(testAssemblyLocation, sampleRelPath);
string appsettingsDirectory = GetAbsoluteAppDirectory(testAssemblyLocation, sampleRelPath);
string appsettingsAbsPath = Path.Combine(appsettingsDirectory, TestConstants.AppSetttingsDotJson);
string testAppsettingsAbsPath = GetAppsettingsDirectory(testAssemblyLocation, testAppsettingsRelPath);
string testAppsettingsAbsPath = GetAbsoluteAppDirectory(testAssemblyLocation, testAppsettingsRelPath);

SwapFiles(appsettingsAbsPath, testAppsettingsAbsPath);

try { BuildSolution(appsettingsDirectory + solutionFileName); }
try { BuildSolution(Path.Combine(appsettingsDirectory, solutionFileName)); }
catch (Exception) { throw; }
finally { SwapFiles(appsettingsAbsPath, testAppsettingsAbsPath); }
}

public static void BuildSampleUsingSampleAppsettings(string testAssemblyLocation, string sampleRelPath, string solutionFileName)
{
string appsDirectory = GetAbsoluteAppDirectory(testAssemblyLocation, sampleRelPath);
BuildSolution(Path.Combine(appsDirectory, solutionFileName));
}
}

/// <summary>
Expand Down Expand Up @@ -597,4 +603,3 @@ public ProcessStartOptions(
}
}
}

6 changes: 6 additions & 0 deletions UiTests/UiTests.sln
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B2CUiTest", "B2CUiTest\B2CUiTest.csproj", "{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -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
Expand Down
Loading