Skip to content

Commit b898513

Browse files
authored
Token cache UI test (#798)
* Added UI test for the 2-2 Token Cache sample
1 parent a42cad9 commit b898513

File tree

8 files changed

+295
-2
lines changed

8 files changed

+295
-2
lines changed

UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds
8282
Assert.Fail(TC.WebAppCrashedString + " " + runningProcesses.ToString());
8383
}
8484

85-
LabResponse labResponse = await LabUserHelper.GetSpecificUserAsync(TC.OIDCUser);
85+
LabResponse labResponse = await LabUserHelper.GetSpecificUserAsync(TC.MsidLab3User);
8686

8787
// Initial sign in
8888
_output.WriteLine("Starting web app sign-in flow.");

UiTests/Common/Common.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
<ItemGroup>
1111
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftAspNetCoreMvcTestingVersion)" />
12+
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
1213
<PackageReference Include="Microsoft.Identity.Lab.Api" Version="$(MicrosoftIdentityLabApiVersion)" />
1314
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
1415
<PackageReference Include="Microsoft.Playwright" Version="$(MicrosoftPlaywrightVersion)" />

UiTests/Common/TestConstants.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ public static class TestConstants
2020
public const string HttpsStarColon = "https://*:";
2121
public const string KestrelEndpointEnvVar = "Kestrel:Endpoints:Http:Url";
2222
public const string LocalhostUrl = @"https://localhost:";
23-
public const string OIDCUser = "[email protected]";
23+
public const string MsidLab3User = "[email protected]";
24+
public const string MsidLab4User = "[email protected]";
2425
public const string PasswordText = "Password";
2526
public const string ServerFilePrefix = "server_";
2627
public const string TodoTitle1 = "Testing create todo item";

UiTests/Common/UiTestHelpers.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using Azure.Core;
55
using Azure.Security.KeyVault.Secrets;
6+
using Microsoft.Data.SqlClient;
67
using Microsoft.Playwright;
78
using System.Diagnostics;
89
using System.Management;
@@ -556,11 +557,94 @@ string solutionFileName
556557
finally { SwapFiles(appsettingsAbsPath, testAppsettingsAbsPath); }
557558
}
558559

560+
/// <summary>
561+
/// Builds the sample app using the appsettings.json file in the sample app's directory.
562+
/// </summary>
563+
/// <param name="testAssemblyLocation">Absolute path to the current test's working directory</param>
564+
/// <param name="sampleRelPath">Relative path to the sample app to build starting at the repo's root, does not include appsettings filename</param>
565+
/// <param name="solutionFileName">Filename for the sln file to build</param>
559566
public static void BuildSampleUsingSampleAppsettings(string testAssemblyLocation, string sampleRelPath, string solutionFileName)
560567
{
561568
string appsDirectory = GetAbsoluteAppDirectory(testAssemblyLocation, sampleRelPath);
562569
BuildSolution(Path.Combine(appsDirectory, solutionFileName));
563570
}
571+
572+
/// <summary>
573+
/// Checks to see if the specified database and token cache table exist in the given server and creates them if they do not.
574+
/// </summary>
575+
/// <param name="serverConnectionString">The string representing the server location</param>
576+
/// <param name="databaseName">Name of the database where the Token Cache will be held</param>
577+
/// <param name="tableName">Name of the table that holds the token cache</param>
578+
/// <param name="output">Enables writing to the test's output</param>
579+
public static void EnsureDatabaseAndTokenCacheTableExist(string serverConnectionString, string databaseName, string tableName, ITestOutputHelper output)
580+
{
581+
using (SqlConnection connection = new SqlConnection(serverConnectionString))
582+
{
583+
connection.Open();
584+
585+
// Check if database exists and create it if it does not
586+
if (DatabaseExists(connection, databaseName))
587+
{
588+
output.WriteLine("Database already exists.");
589+
}
590+
else
591+
{
592+
CreateDatabase(connection, databaseName);
593+
output.WriteLine("Database created.");
594+
}
595+
596+
// Switch to the database
597+
connection.ChangeDatabase(databaseName);
598+
599+
// Check if table exists and create it if it does not
600+
if (TableExists(connection, tableName))
601+
{
602+
output.WriteLine("Table already exists.");
603+
}
604+
else
605+
{
606+
CreateTokenCacheTable(connection, tableName);
607+
output.WriteLine("Table created.");
608+
}
609+
}
610+
}
611+
612+
private static bool DatabaseExists(SqlConnection connection, string databaseName)
613+
{
614+
string checkDatabaseQuery = $"SELECT database_id FROM sys.databases WHERE name = '{databaseName}'";
615+
using SqlCommand command = new SqlCommand(checkDatabaseQuery, connection);
616+
object result = command.ExecuteScalar();
617+
return result != null;
618+
}
619+
620+
private static void CreateDatabase(SqlConnection connection, string databaseName)
621+
{
622+
string createDatabaseQuery = $"CREATE DATABASE {databaseName}";
623+
using SqlCommand createCommand = new SqlCommand(createDatabaseQuery, connection);
624+
createCommand.ExecuteNonQuery();
625+
}
626+
627+
private static bool TableExists(SqlConnection connection, string tableName)
628+
{
629+
string checkTableQuery = $"SELECT object_id('{tableName}', 'U')";
630+
using SqlCommand command = new SqlCommand(checkTableQuery, connection);
631+
object result = command.ExecuteScalar();
632+
return result.GetType() != typeof(DBNull);
633+
}
634+
635+
private static void CreateTokenCacheTable(SqlConnection connection, string tableName)
636+
{
637+
string createCacheTableQuery = $@"
638+
CREATE TABLE [dbo].[{tableName}] (
639+
[Id] NVARCHAR(449) NOT NULL PRIMARY KEY,
640+
[Value] VARBINARY(MAX) NOT NULL,
641+
[ExpiresAtTime] DATETIMEOFFSET NOT NULL,
642+
[SlidingExpirationInSeconds] BIGINT NULL,
643+
[AbsoluteExpiration] DATETIMEOFFSET NULL
644+
)";
645+
using SqlCommand createCommand = new SqlCommand(createCacheTableQuery, connection);
646+
createCommand.ExecuteNonQuery();
647+
}
564648
}
565649

566650
/// <summary>
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<TargetFramework>net8.0</TargetFramework>
5+
<ImplicitUsings>enable</ImplicitUsings>
6+
<IsPackable>false</IsPackable>
7+
<Nullable>enable</Nullable>
8+
</PropertyGroup>
9+
10+
<ItemGroup>
11+
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftAspNetCoreMvcTestingVersion)" />
12+
<PackageReference Include="Microsoft.Identity.Lab.Api" Version="$(MicrosoftIdentityLabApiVersion)" />
13+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
14+
<PackageReference Include="Microsoft.Playwright" Version="$(MicrosoftPlaywrightVersion)" />
15+
<PackageReference Include="System.Management" Version="$(SystemManagementVersion)" />
16+
<PackageReference Include="System.Text.Json" Version="$(SystemTextJsonVersion)" />
17+
<PackageReference Include="xunit" Version="$(XunitVersion)" />
18+
<PackageReference Include="xunit.runner.visualstudio" Version="$(XunitRunnerVisualStudioVersion)">
19+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
20+
<PrivateAssets>all</PrivateAssets>
21+
</PackageReference>
22+
<PackageReference Include="coverlet.collector" Version="$(CoverletCollectorVersion)">
23+
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
24+
<PrivateAssets>all</PrivateAssets>
25+
</PackageReference>
26+
</ItemGroup>
27+
28+
<ItemGroup>
29+
<ProjectReference Include="..\Common\Common.csproj" />
30+
</ItemGroup>
31+
32+
</Project>
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using Common;
5+
using Microsoft.Identity.Lab.Api;
6+
using Microsoft.Playwright;
7+
using System.Text;
8+
using Xunit;
9+
using Xunit.Abstractions;
10+
using Process = System.Diagnostics.Process;
11+
using TC = Common.TestConstants;
12+
13+
14+
namespace GraphUserTokenCacheTest
15+
{
16+
public class GraphUserTokenCacheTest
17+
{
18+
private const uint ClientPort = 44321;
19+
private const uint NumProcessRetries = 3;
20+
private const string SampleSlnFileName = "2-2-TokenCache.sln";
21+
private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut";
22+
private const string SqlDbName = "MY_TOKEN_CACHE_DATABASE";
23+
private const string SqlServerConnectionString = "Server=(localdb)\\mssqllocaldb;Integrated Security=true";
24+
private const string SqlTableName = "TokenCache";
25+
private const string TraceFileClassName = "GraphUserTokenCacheTest";
26+
private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 };
27+
private readonly ITestOutputHelper _output;
28+
private readonly string _sampleAppPath = "2-WebApp-graph-user" + Path.DirectorySeparatorChar + "2-2-TokenCache" + Path.DirectorySeparatorChar.ToString();
29+
private readonly string _testAppsettingsPath = "UiTests" + Path.DirectorySeparatorChar + "GraphUserTokenCache" + Path.DirectorySeparatorChar.ToString() + TC.AppSetttingsDotJson;
30+
private readonly string _testAssemblyLocation = typeof(GraphUserTokenCacheTest).Assembly.Location;
31+
32+
public GraphUserTokenCacheTest(ITestOutputHelper output)
33+
{
34+
_output = output;
35+
}
36+
37+
[Fact]
38+
public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_LoginLogoutAsync()
39+
{
40+
// Setup web app and api environmental variables.
41+
var clientEnvVars = new Dictionary<string, string>
42+
{
43+
{"ASPNETCORE_ENVIRONMENT", "Development"},
44+
{TC.KestrelEndpointEnvVar, TC.HttpsStarColon + ClientPort}
45+
};
46+
47+
Dictionary<string, Process>? processes = null;
48+
49+
// Arrange Playwright setup, to see the browser UI set Headless = false.
50+
const string TraceFileName = TraceFileClassName + "_LoginLogout";
51+
using IPlaywright playwright = await Playwright.CreateAsync();
52+
IBrowser browser = await playwright.Chromium.LaunchAsync(new() { Headless = true });
53+
IBrowserContext context = await browser.NewContextAsync(new BrowserNewContextOptions { IgnoreHTTPSErrors = true });
54+
await context.Tracing.StartAsync(new() { Screenshots = true, Snapshots = true, Sources = true });
55+
IPage page = await context.NewPageAsync();
56+
string uriWithPort = TC.LocalhostUrl + ClientPort;
57+
58+
try
59+
{
60+
// Make sure database and table for cache exist, if not they will be created.
61+
UiTestHelpers.EnsureDatabaseAndTokenCacheTableExist(SqlServerConnectionString, SqlDbName, SqlTableName, _output);
62+
63+
// Build the sample app with correct appsettings file.
64+
UiTestHelpers.BuildSampleUsingTestAppsettings(_testAssemblyLocation, _sampleAppPath, _testAppsettingsPath, SampleSlnFileName);
65+
66+
// Start the web app and api processes.
67+
// The delay before starting client prevents transient devbox issue where the client fails to load the first time after rebuilding
68+
var clientProcessOptions = new ProcessStartOptions(_testAssemblyLocation, _sampleAppPath, TC.s_oidcWebAppExe, clientEnvVars);
69+
70+
bool areProcessesRunning = UiTestHelpers.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries);
71+
72+
if (!areProcessesRunning)
73+
{
74+
_output.WriteLine($"Process not started after {NumProcessRetries} attempts.");
75+
StringBuilder runningProcesses = new();
76+
foreach (var process in processes)
77+
{
78+
#pragma warning disable CA1305 // Specify IFormatProvider
79+
runningProcesses.AppendLine($"Is {process.Key} running: {UiTestHelpers.ProcessIsAlive(process.Value)}");
80+
#pragma warning restore CA1305 // Specify IFormatProvider
81+
}
82+
Assert.Fail(TC.WebAppCrashedString + " " + runningProcesses.ToString());
83+
}
84+
85+
LabResponse labResponse = await LabUserHelper.GetSpecificUserAsync(TC.MsidLab4User);
86+
87+
// Initial sign in
88+
_output.WriteLine("Starting web app sign-in flow.");
89+
string email = labResponse.User.Upn;
90+
await UiTestHelpers.NavigateToWebApp(uriWithPort, page);
91+
await UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPassword(page, email, labResponse.User.GetOrFetchPassword());
92+
await Assertions.Expect(page.GetByText("Integrating Azure AD V2")).ToBeVisibleAsync(_assertVisibleOptions);
93+
await Assertions.Expect(page.GetByText(email)).ToBeVisibleAsync(_assertVisibleOptions);
94+
_output.WriteLine("Web app sign-in flow successful.");
95+
96+
// Sign out
97+
_output.WriteLine("Starting web app sign-out flow.");
98+
await page.GetByRole(AriaRole.Link, new() { Name = "Sign out" }).ClickAsync();
99+
await UiTestHelpers.PerformSignOut_MicrosoftIdFlow(page, email, TC.LocalhostUrl + ClientPort + SignOutPageUriPath, _output);
100+
_output.WriteLine("Web app sign out successful.");
101+
}
102+
catch (Exception ex)
103+
{
104+
// Adding guid in case of multiple test runs. This will allow screenshots to be matched to their appropriate test runs.
105+
var guid = Guid.NewGuid().ToString();
106+
try
107+
{
108+
if (page != null)
109+
{
110+
await page.ScreenshotAsync(new PageScreenshotOptions() { Path = $"ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_TodoAppFunctionsCorrectlyScreenshotFail{guid}.png", FullPage = true });
111+
}
112+
}
113+
catch
114+
{
115+
_output.WriteLine("No Screenshot.");
116+
}
117+
118+
string runningProcesses = UiTestHelpers.GetRunningProcessAsString(processes);
119+
Assert.Fail($"the UI automation failed: {ex} output: {ex.Message}.\n{runningProcesses}\nTest run: {guid}");
120+
}
121+
finally
122+
{
123+
// Make sure all processes and their children are stopped.
124+
UiTestHelpers.EndProcesses(processes);
125+
126+
// Stop tracing and export it into a zip archive.
127+
string path = UiTestHelpers.GetTracePath(_testAssemblyLocation, TraceFileName);
128+
await context.Tracing.StopAsync(new() { Path = path });
129+
_output.WriteLine($"Trace data for {TraceFileName} recorded to {path}.");
130+
131+
// Close the browser and stop Playwright.
132+
await browser.CloseAsync();
133+
playwright.Dispose();
134+
}
135+
}
136+
}
137+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"AzureAd": {
3+
"Instance": "https://login.microsoftonline.com/",
4+
"Domain": "msidlab4.onmicrosoft.com",
5+
"TenantId": "f645ad92-e38d-4d1a-b510-d1b09a74a8ca",
6+
"ClientId": "9a192b78-6580-4f8a-aace-f36ffea4f7be",
7+
"CallbackPath": "/signin-oidc",
8+
"SignedOutCallbackPath": "/signout-callback-oidc",
9+
"ClientCertificates": [
10+
{
11+
"SourceType": "KeyVault",
12+
"KeyVaultUrl": "https://webappsapistests.vault.azure.net",
13+
"KeyVaultCertificateName": "Self-Signed-5-5-22"
14+
}
15+
]
16+
},
17+
"DownstreamApi": {
18+
"BaseUrl": "https://graph.microsoft.com/v1.0",
19+
"Scopes": "user.read"
20+
},
21+
"ConnectionStrings": {
22+
"TokenCacheDbConnStr": "Data Source=(LocalDb)\\MSSQLLocalDB;Database=MY_TOKEN_CACHE_DATABASE;Trusted_Connection=True;"
23+
},
24+
"Logging": {
25+
"LogLevel": {
26+
"Default": "Information",
27+
"Microsoft": "Warning",
28+
"Microsoft.Hosting.Lifetime": "Information"
29+
}
30+
},
31+
"AllowedHosts": "*"
32+
}

UiTests/UiTests.sln

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1414
EndProject
1515
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "B2CUiTest", "B2CUiTest\B2CUiTest.csproj", "{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}"
1616
EndProject
17+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphUserTokenCache", "GraphUserTokenCache\GraphUserTokenCache.csproj", "{B083D288-AB6E-4849-9AC2-E1DA1F727483}"
18+
EndProject
1719
Global
1820
GlobalSection(SolutionConfigurationPlatforms) = preSolution
1921
Debug|Any CPU = Debug|Any CPU
@@ -32,6 +34,10 @@ Global
3234
{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Debug|Any CPU.Build.0 = Debug|Any CPU
3335
{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Release|Any CPU.ActiveCfg = Release|Any CPU
3436
{BF7D9973-9B92-4BED-ADE2-09087DDA9C85}.Release|Any CPU.Build.0 = Release|Any CPU
37+
{B083D288-AB6E-4849-9AC2-E1DA1F727483}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38+
{B083D288-AB6E-4849-9AC2-E1DA1F727483}.Debug|Any CPU.Build.0 = Debug|Any CPU
39+
{B083D288-AB6E-4849-9AC2-E1DA1F727483}.Release|Any CPU.ActiveCfg = Release|Any CPU
40+
{B083D288-AB6E-4849-9AC2-E1DA1F727483}.Release|Any CPU.Build.0 = Release|Any CPU
3541
EndGlobalSection
3642
GlobalSection(SolutionProperties) = preSolution
3743
HideSolutionNode = FALSE

0 commit comments

Comments
 (0)