Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit a42cad9

Browse files
authoredOct 9, 2024··
B2C UI test (#793)
Added automated test for B2C
1 parent 5b9c713 commit a42cad9

File tree

8 files changed

+202
-13
lines changed

8 files changed

+202
-13
lines changed
 

‎3-WebApp-multi-APIs/appsettings.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,4 @@
2020
},
2121
"AllowedHosts": "*",
2222
"GraphApiUrl": "https://graph.microsoft.com"
23-
}
23+
}

‎4-WebApp-your-API/4-2-B2C/Client/appsettings.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
"SignedOutCallbackPath": "/signout/B2C_1_susi_reset_v2",
77
"SignUpSignInPolicyId": "B2C_1_susi_reset_v2",
88
"EditProfilePolicyId": "B2C_1_edit_profile_v2", // Optional profile editing policy
9-
"ClientSecret": "X330F3#92!z614M4"
109
//"CallbackPath": "/signin/B2C_1_sign_up_in" // defaults to /signin-oidc
1110
},
1211
"TodoList": {

‎UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds
5252
// Arrange Playwright setup, to see the browser UI set Headless = false.
5353
const string TraceFileName = TraceFileClassName + "_LoginLogout";
5454
using IPlaywright playwright = await Playwright.CreateAsync();
55-
IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = false });
55+
IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = true });
5656
IBrowserContext context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true });
5757
await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true });
5858
IPage page = await context.NewPageAsync();
@@ -61,7 +61,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds
6161
try
6262
{
6363
// Build the sample app with correct appsettings file.
64-
UiTestHelpers.BuildSampleWithTestAppsettings(_testAssemblyLocation, _sampleAppPath, _testAppsettingsPath, SampleSlnFileName);
64+
UiTestHelpers.BuildSampleUsingTestAppsettings(_testAssemblyLocation, _sampleAppPath, _testAppsettingsPath, SampleSlnFileName);
6565

6666
// Start the web app and api processes.
6767
// The delay before starting client prevents transient devbox issue where the client fails to load the first time after rebuilding

‎UiTests/B2CUiTest/B2CUiTest.cs

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Azure.Identity;
5+
using Common;
6+
using Microsoft.Identity.Lab.Api;
7+
using Microsoft.Playwright;
8+
using System;
9+
using System.Collections.Generic;
10+
using System.Diagnostics;
11+
using System.IO;
12+
using System.Runtime.Versioning;
13+
using System.Threading.Tasks;
14+
using Xunit;
15+
using Xunit.Abstractions;
16+
using TC = Common.TestConstants;
17+
18+
namespace B2CUiTest
19+
{
20+
public class B2CUiTest : IClassFixture<InstallPlaywrightBrowserFixture>
21+
{
22+
private const string KeyvaultEmailName = "IdWeb-B2C-user";
23+
private const string KeyvaultPasswordName = "IdWeb-B2C-password";
24+
private const string KeyvaultClientSecretName = "IdWeb-B2C-Client-ClientSecret";
25+
private const string NameOfUser = "unknown";
26+
private const uint ProcessStartupRetryNum = 3;
27+
private const string SampleSolutionFileName = "4-2-B2C-Secured-API.sln";
28+
private const uint TodoListClientPort = 5000;
29+
private const uint TodoListServicePort = 44332;
30+
private const string TraceClassName = "B2C-Login";
31+
32+
private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 };
33+
private readonly string _sampleClientAppPath;
34+
private readonly string _samplePath = Path.Join("4-WebApp-your-API", "4-2-B2C");
35+
private readonly string _sampleServiceAppPath;
36+
private readonly Uri _keyvaultUri = new("https://webappsapistests.vault.azure.net");
37+
private readonly ITestOutputHelper _output;
38+
private readonly string _testAssemblyLocation = typeof(B2CUiTest).Assembly.Location;
39+
40+
public B2CUiTest(ITestOutputHelper output)
41+
{
42+
_output = output;
43+
_sampleClientAppPath = Path.Join(_samplePath, TC.s_todoListClientPath);
44+
_sampleServiceAppPath = Path.Join(_samplePath, TC.s_todoListServicePath);
45+
}
46+
47+
[Fact]
48+
[SupportedOSPlatform("windows")]
49+
public async Task B2C_ValidCreds_LoginLogout()
50+
{
51+
// Web app and api environmental variable setup.
52+
Dictionary<string, Process>? processes = null;
53+
DefaultAzureCredential azureCred = new();
54+
string clientSecret = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultClientSecretName, azureCred);
55+
var serviceEnvVars = new Dictionary<string, string>
56+
{
57+
{"ASPNETCORE_ENVIRONMENT", "Development" },
58+
{TC.KestrelEndpointEnvVar, TC.HttpStarColon + TodoListServicePort}
59+
};
60+
var clientEnvVars = new Dictionary<string, string>
61+
{
62+
{"ASPNETCORE_ENVIRONMENT", "Development"},
63+
{"AzureAdB2C__ClientSecret", clientSecret},
64+
{TC.KestrelEndpointEnvVar, TC.HttpsStarColon + TodoListClientPort}
65+
};
66+
67+
// Get email and password from keyvault.
68+
string email = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultEmailName, azureCred);
69+
string password = await UiTestHelpers.GetValueFromKeyvaultWitDefaultCreds(_keyvaultUri, KeyvaultPasswordName, azureCred);
70+
71+
// Playwright setup. To see browser UI, set 'Headless = false'.
72+
const string TraceFileName = TraceClassName + "_TodoAppFunctionsCorrectly";
73+
using IPlaywright playwright = await Playwright.CreateAsync();
74+
IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = true });
75+
IBrowserContext context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true });
76+
await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true });
77+
78+
try
79+
{
80+
UiTestHelpers.BuildSampleUsingSampleAppsettings(_testAssemblyLocation, _samplePath, SampleSolutionFileName);
81+
82+
// Start the web app and api processes.
83+
// The delay before starting client prevents transient devbox issue where the client fails to load the first time after rebuilding.
84+
var clientProcessOptions = new ProcessStartOptions(_testAssemblyLocation, _sampleClientAppPath, TC.s_todoListClientExe, clientEnvVars); // probs need to add client specific path
85+
var serviceProcessOptions = new ProcessStartOptions(_testAssemblyLocation, _sampleServiceAppPath, TC.s_todoListServiceExe, serviceEnvVars);
86+
87+
UiTestHelpers.StartAndVerifyProcessesAreRunning([serviceProcessOptions, clientProcessOptions], out processes, ProcessStartupRetryNum);
88+
89+
// Navigate to web app the retry logic ensures the web app has time to start up to establish a connection.
90+
IPage page = await context.NewPageAsync();
91+
uint InitialConnectionRetryCount = 5;
92+
while (InitialConnectionRetryCount > 0)
93+
{
94+
try
95+
{
96+
await page.GotoAsync(TC.LocalhostUrl + TodoListClientPort);
97+
break;
98+
}
99+
catch (PlaywrightException)
100+
{
101+
await Task.Delay(1000);
102+
InitialConnectionRetryCount--;
103+
if (InitialConnectionRetryCount == 0) { throw; }
104+
}
105+
}
106+
LabResponse labResponse = await LabUserHelper.GetB2CLocalAccountAsync();
107+
108+
// Initial sign in
109+
_output.WriteLine("Starting web app sign-in flow.");
110+
ILocator emailEntryBox = page.GetByPlaceholder("Email Address");
111+
await emailEntryBox.FillAsync(email);
112+
await emailEntryBox.PressAsync("Tab");
113+
await page.GetByPlaceholder("Password").FillAsync(password);
114+
await page.GetByRole(AriaRole.Button, new() { Name = "Sign in" }).ClickAsync();
115+
await Assertions.Expect(page.GetByText("TodoList")).ToBeVisibleAsync(_assertVisibleOptions);
116+
await Assertions.Expect(page.GetByText(NameOfUser)).ToBeVisibleAsync(_assertVisibleOptions);
117+
_output.WriteLine("Web app sign-in flow successful.");
118+
119+
// Sign out
120+
_output.WriteLine("Starting web app sign-out flow.");
121+
await page.GetByRole(AriaRole.Link, new() { Name = "Sign out" }).ClickAsync();
122+
_output.WriteLine("Signing out ...");
123+
await Assertions.Expect(page.GetByText("You have successfully signed out.")).ToBeVisibleAsync(_assertVisibleOptions);
124+
await Assertions.Expect(page.GetByText(NameOfUser)).ToBeHiddenAsync();
125+
_output.WriteLine("Web app sign out successful.");
126+
}
127+
catch (Exception ex)
128+
{
129+
Assert.Fail($"the UI automation failed: {ex} output: {ex.Message}.");
130+
}
131+
finally
132+
{
133+
// End all processes.
134+
UiTestHelpers.EndProcesses(processes);
135+
136+
// Stop tracing and export it into a zip archive.
137+
string path = UiTestHelpers.GetTracePath(_testAssemblyLocation, TraceFileName);
138+
await context.Tracing.StopAsync(new() { Path = path });
139+
_output.WriteLine($"Trace data for {TraceFileName} recorded to {path}.");
140+
141+
// Close the browser and stop Playwright.
142+
await browser.CloseAsync();
143+
playwright.Dispose();
144+
}
145+
}
146+
}
147+
}

‎UiTests/B2CUiTest/B2CUiTest.csproj

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFrameworks>net8.0</TargetFrameworks>
5+
<IsPackable>false</IsPackable>
6+
</PropertyGroup>
7+
8+
<ItemGroup>
9+
<PackageReference Include="Azure.Identity" Version="1.12.1" />
10+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftAspNetCoreMvcTestingVersion)" />
11+
<PackageReference Include="Microsoft.Identity.Lab.Api" Version="$(MicrosoftIdentityLabApiVersion)" />
12+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
13+
<PackageReference Include="Microsoft.Playwright" Version="$(MicrosoftPlaywrightVersion)" />
14+
<PackageReference Include="System.Management" Version="$(SystemManagementVersion)" />
15+
<PackageReference Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
16+
<PackageReference Include="xunit" Version="$(XunitVersion)" />
17+
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioVersion)">
18+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
19+
<PrivateAssets>all</PrivateAssets>
20+
</PackageReference>
21+
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
22+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
23+
<PrivateAssets>all</PrivateAssets>
24+
</PackageReference>
25+
</ItemGroup>
26+
27+
<ItemGroup>
28+
<ProjectReference Include="..\Common\Common.csproj" />
29+
</ItemGroup>
30+
31+
</Project>

‎UiTests/Common/UiTestHelpers.cs

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ private static string GetApplicationWorkingDirectory(string testAssemblyLocation
235235
/// <param name="testAssemblyLocation">The path to the test's directory</param>
236236
/// <param name="appLocation">The path to the processes directory</param>
237237
/// <returns>The path to the directory for the given app</returns>
238-
private static string GetAppsettingsDirectory(string testAssemblyLocation, string appLocation)
238+
private static string GetAbsoluteAppDirectory(string testAssemblyLocation, string appLocation)
239239
{
240240
string testedAppLocation = Path.GetDirectoryName(testAssemblyLocation)!;
241241
// e.g. microsoft-identity-web\tests\E2E Tests\WebAppUiTests\bin\Debug\net6.0
@@ -380,7 +380,7 @@ public static void InstallPlaywrightBrowser()
380380
/// <param name="keyvaultSecretName">The name of the secret</param>
381381
/// <returns>The value of the secret from key vault</returns>
382382
/// <exception cref="ArgumentNullException">Throws if no secret name is provided</exception>
383-
internal static async Task<string> GetValueFromKeyvaultWitDefaultCreds(Uri keyvaultUri, string keyvaultSecretName, TokenCredential creds)
383+
public static async Task<string> GetValueFromKeyvaultWitDefaultCreds(Uri keyvaultUri, string keyvaultSecretName, TokenCredential creds)
384384
{
385385
if (string.IsNullOrEmpty(keyvaultSecretName))
386386
{
@@ -528,7 +528,7 @@ private static void BuildSolution(string solutionPath)
528528
process.WaitForExit();
529529
}
530530

531-
Console.WriteLine("Solution rebuild initiated.");
531+
Console.WriteLine("Solution build initiated.");
532532
}
533533

534534
/// <summary>
@@ -538,23 +538,29 @@ private static void BuildSolution(string solutionPath)
538538
/// <param name="sampleRelPath">Relative path to the sample app to build starting at the repo's root, does not include appsettings filename</param>
539539
/// <param name="testAppsettingsRelPath">Relative path to the test appsettings file starting at the repo's root, includes appsettings filename</param>
540540
/// <param name="solutionFileName">Filename for the sln file to build</param>
541-
public static void BuildSampleWithTestAppsettings(
541+
public static void BuildSampleUsingTestAppsettings(
542542
string testAssemblyLocation,
543543
string sampleRelPath,
544544
string testAppsettingsRelPath,
545545
string solutionFileName
546546
)
547547
{
548-
string appsettingsDirectory = GetAppsettingsDirectory(testAssemblyLocation, sampleRelPath);
548+
string appsettingsDirectory = GetAbsoluteAppDirectory(testAssemblyLocation, sampleRelPath);
549549
string appsettingsAbsPath = Path.Combine(appsettingsDirectory, TestConstants.AppSetttingsDotJson);
550-
string testAppsettingsAbsPath = GetAppsettingsDirectory(testAssemblyLocation, testAppsettingsRelPath);
550+
string testAppsettingsAbsPath = GetAbsoluteAppDirectory(testAssemblyLocation, testAppsettingsRelPath);
551551

552552
SwapFiles(appsettingsAbsPath, testAppsettingsAbsPath);
553553

554-
try { BuildSolution(appsettingsDirectory + solutionFileName); }
554+
try { BuildSolution(Path.Combine(appsettingsDirectory, solutionFileName)); }
555555
catch (Exception) { throw; }
556556
finally { SwapFiles(appsettingsAbsPath, testAppsettingsAbsPath); }
557557
}
558+
559+
public static void BuildSampleUsingSampleAppsettings(string testAssemblyLocation, string sampleRelPath, string solutionFileName)
560+
{
561+
string appsDirectory = GetAbsoluteAppDirectory(testAssemblyLocation, sampleRelPath);
562+
BuildSolution(Path.Combine(appsDirectory, solutionFileName));
563+
}
558564
}
559565

560566
/// <summary>
@@ -597,4 +603,3 @@ public ProcessStartOptions(
597603
}
598604
}
599605
}
600-

‎UiTests/Directory.Build.props

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
<TargetFrameworks>net8.0</TargetFrameworks>
66
<IsPackable>false</IsPackable>
77
<EnablePackageValidation>false</EnablePackageValidation>
8+
<Nullable>enable</Nullable>
89
</PropertyGroup>
910

1011
<PropertyGroup Label="Common dependency versions">
@@ -14,7 +15,7 @@
1415
<MicrosoftNetTestSdkVersion>17.11.1</MicrosoftNetTestSdkVersion>
1516
<MicrosoftPlaywrightVersion>1.47.0</MicrosoftPlaywrightVersion>
1617
<SystemManagementVersion>8.0.0</SystemManagementVersion>
17-
<SystemTextJsonVersion>8.0.4</SystemTextJsonVersion>
18+
<SystemTextJsonVersion>8.0.5</SystemTextJsonVersion>
1819
<XunitAssertVersion>2.9.1</XunitAssertVersion>
1920
<XunitExtensibilityCoreVersion>2.9.1</XunitExtensibilityCoreVersion>
2021
<XunitRunnerVisualStudioVersion>2.8.2</XunitRunnerVisualStudioVersion>

‎UiTests/UiTests.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1212
Directory.Build.props = Directory.Build.props
1313
EndProjectSection
1414
EndProject
15+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B2CUiTest", "B2CUiTest\B2CUiTest.csproj", "{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}"
16+
EndProject
1517
Global
1618
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1719
Debug|Any CPU = Debug|Any CPU
@@ -26,6 +28,10 @@ Global
2628
{3074B729-52E8-408E-8BBC-815FE9217385}.Debug|Any CPU.Build.0 = Debug|Any CPU
2729
{3074B729-52E8-408E-8BBC-815FE9217385}.Release|Any CPU.ActiveCfg = Release|Any CPU
2830
{3074B729-52E8-408E-8BBC-815FE9217385}.Release|Any CPU.Build.0 = Release|Any CPU
31+
{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
32+
{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Debug|Any CPU.Build.0 = Debug|Any CPU
33+
{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Release|Any CPU.ActiveCfg = Release|Any CPU
34+
{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Release|Any CPU.Build.0 = Release|Any CPU
2935
EndGlobalSection
3036
GlobalSection(SolutionProperties) = preSolution
3137
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)
Please sign in to comment.