Skip to content

Token cache UI test #798

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 4 commits into from
Oct 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 UiTests/AnyOrgOrPersonalUiTest/AnyOrgOrPersonalTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds
Assert.Fail(TC.WebAppCrashedString + " " + runningProcesses.ToString());
}

LabResponse labResponse = await LabUserHelper.GetSpecificUserAsync(TC.OIDCUser);
LabResponse labResponse = await LabUserHelper.GetSpecificUserAsync(TC.MsidLab3User);

// Initial sign in
_output.WriteLine("Starting web app sign-in flow.");
Expand Down
1 change: 1 addition & 0 deletions UiTests/Common/Common.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

<ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="$(MicrosoftAspNetCoreMvcTestingVersion)" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.2" />
<PackageReference Include="Microsoft.Identity.Lab.Api" Version="$(MicrosoftIdentityLabApiVersion)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="$(MicrosoftNetTestSdkVersion)" />
<PackageReference Include="Microsoft.Playwright" Version="$(MicrosoftPlaywrightVersion)" />
Expand Down
3 changes: 2 additions & 1 deletion UiTests/Common/TestConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ public static class TestConstants
public const string HttpsStarColon = "https://*:";
public const string KestrelEndpointEnvVar = "Kestrel:Endpoints:Http:Url";
public const string LocalhostUrl = @"https://localhost:";
public const string OIDCUser = "[email protected]";
public const string MsidLab3User = "[email protected]";
public const string MsidLab4User = "[email protected]";
public const string PasswordText = "Password";
public const string ServerFilePrefix = "server_";
public const string TodoTitle1 = "Testing create todo item";
Expand Down
84 changes: 84 additions & 0 deletions UiTests/Common/UiTestHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Azure.Core;
using Azure.Security.KeyVault.Secrets;
using Microsoft.Data.SqlClient;
using Microsoft.Playwright;
using System.Diagnostics;
using System.Management;
Expand Down Expand Up @@ -556,11 +557,94 @@ string solutionFileName
finally { SwapFiles(appsettingsAbsPath, testAppsettingsAbsPath); }
}

/// <summary>
/// Builds the sample app using the appsettings.json file in the sample app's directory.
/// </summary>
/// <param name="testAssemblyLocation">Absolute path to the current test's working directory</param>
/// <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="solutionFileName">Filename for the sln file to build</param>
public static void BuildSampleUsingSampleAppsettings(string testAssemblyLocation, string sampleRelPath, string solutionFileName)
{
string appsDirectory = GetAbsoluteAppDirectory(testAssemblyLocation, sampleRelPath);
BuildSolution(Path.Combine(appsDirectory, solutionFileName));
}

/// <summary>
/// Checks to see if the specified database and token cache table exist in the given server and creates them if they do not.
/// </summary>
/// <param name="serverConnectionString">The string representing the server location</param>
/// <param name="databaseName">Name of the database where the Token Cache will be held</param>
/// <param name="tableName">Name of the table that holds the token cache</param>
/// <param name="output">Enables writing to the test's output</param>
public static void EnsureDatabaseAndTokenCacheTableExist(string serverConnectionString, string databaseName, string tableName, ITestOutputHelper output)
{
using (SqlConnection connection = new SqlConnection(serverConnectionString))
{
connection.Open();

// Check if database exists and create it if it does not
if (DatabaseExists(connection, databaseName))
{
output.WriteLine("Database already exists.");
}
else
{
CreateDatabase(connection, databaseName);
output.WriteLine("Database created.");
}

// Switch to the database
connection.ChangeDatabase(databaseName);

// Check if table exists and create it if it does not
if (TableExists(connection, tableName))
{
output.WriteLine("Table already exists.");
}
else
{
CreateTokenCacheTable(connection, tableName);
output.WriteLine("Table created.");
}
}
}

private static bool DatabaseExists(SqlConnection connection, string databaseName)
{
string checkDatabaseQuery = $"SELECT database_id FROM sys.databases WHERE name = '{databaseName}'";
using SqlCommand command = new SqlCommand(checkDatabaseQuery, connection);
object result = command.ExecuteScalar();
return result != null;
}

private static void CreateDatabase(SqlConnection connection, string databaseName)
{
string createDatabaseQuery = $"CREATE DATABASE {databaseName}";
using SqlCommand createCommand = new SqlCommand(createDatabaseQuery, connection);
createCommand.ExecuteNonQuery();
}

private static bool TableExists(SqlConnection connection, string tableName)
{
string checkTableQuery = $"SELECT object_id('{tableName}', 'U')";
using SqlCommand command = new SqlCommand(checkTableQuery, connection);
object result = command.ExecuteScalar();
return result.GetType() != typeof(DBNull);
}

private static void CreateTokenCacheTable(SqlConnection connection, string tableName)
{
string createCacheTableQuery = $@"
CREATE TABLE [dbo].[{tableName}] (
[Id] NVARCHAR(449) NOT NULL PRIMARY KEY,
[Value] VARBINARY(MAX) NOT NULL,
[ExpiresAtTime] DATETIMEOFFSET NOT NULL,
[SlidingExpirationInSeconds] BIGINT NULL,
[AbsoluteExpiration] DATETIMEOFFSET NULL
)";
using SqlCommand createCommand = new SqlCommand(createCacheTableQuery, connection);
createCommand.ExecuteNonQuery();
}
}

/// <summary>
Expand Down
32 changes: 32 additions & 0 deletions UiTests/GraphUserTokenCache/GraphUserTokenCache.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<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>
137 changes: 137 additions & 0 deletions UiTests/GraphUserTokenCache/GraphUserTokenCacheTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Common;
using Microsoft.Identity.Lab.Api;
using Microsoft.Playwright;
using System.Text;
using Xunit;
using Xunit.Abstractions;
using Process = System.Diagnostics.Process;
using TC = Common.TestConstants;


namespace GraphUserTokenCacheTest
{
public class GraphUserTokenCacheTest
{
private const uint ClientPort = 44321;
private const uint NumProcessRetries = 3;
private const string SampleSlnFileName = "2-2-TokenCache.sln";
private const string SignOutPageUriPath = @"/MicrosoftIdentity/Account/SignedOut";
private const string SqlDbName = "MY_TOKEN_CACHE_DATABASE";
private const string SqlServerConnectionString = "Server=(localdb)\\mssqllocaldb;Integrated Security=true";
private const string SqlTableName = "TokenCache";
private const string TraceFileClassName = "GraphUserTokenCacheTest";
private readonly LocatorAssertionsToBeVisibleOptions _assertVisibleOptions = new() { Timeout = 25000 };
private readonly ITestOutputHelper _output;
private readonly string _sampleAppPath = "2-WebApp-graph-user" + Path.DirectorySeparatorChar + "2-2-TokenCache" + Path.DirectorySeparatorChar.ToString();
private readonly string _testAppsettingsPath = "UiTests" + Path.DirectorySeparatorChar + "GraphUserTokenCache" + Path.DirectorySeparatorChar.ToString() + TC.AppSetttingsDotJson;
private readonly string _testAssemblyLocation = typeof(GraphUserTokenCacheTest).Assembly.Location;

public GraphUserTokenCacheTest(ITestOutputHelper output)
{
_output = output;
}

[Fact]
public async Task ChallengeUser_MicrosoftIdFlow_LocalApp_ValidEmailPasswordCreds_LoginLogoutAsync()
{
// 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
{
// Make sure database and table for cache exist, if not they will be created.
UiTestHelpers.EnsureDatabaseAndTokenCacheTableExist(SqlServerConnectionString, SqlDbName, SqlTableName, _output);

// 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, TC.s_oidcWebAppExe, clientEnvVars);

bool areProcessesRunning = UiTestHelpers.StartAndVerifyProcessesAreRunning([clientProcessOptions], out processes, NumProcessRetries);

if (!areProcessesRunning)
{
_output.WriteLine($"Process not started after {NumProcessRetries} attempts.");
StringBuilder runningProcesses = new();
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 UiTestHelpers.FirstLogin_MicrosoftIdFlow_ValidEmailPassword(page, email, labResponse.User.GetOrFetchPassword());
await Assertions.Expect(page.GetByText("Integrating Azure AD V2")).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();
}
}
}
}
32 changes: 32 additions & 0 deletions UiTests/GraphUserTokenCache/appsettings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"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"
}
]
},
"DownstreamApi": {
"BaseUrl": "https://graph.microsoft.com/v1.0",
"Scopes": "user.read"
},
"ConnectionStrings": {
"TokenCacheDbConnStr": "Data Source=(LocalDb)\\MSSQLLocalDB;Database=MY_TOKEN_CACHE_DATABASE;Trusted_Connection=True;"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"AllowedHosts": "*"
}
6 changes: 6 additions & 0 deletions UiTests/UiTests.sln
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "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}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand All @@ -32,6 +34,10 @@ Global
{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
{B083D288-AB6E-4849-9AC2-E1DA1F727483}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{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
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Loading