Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
Original file line number Diff line number Diff line change
Expand Up @@ -216,13 +216,21 @@ internal class Constants
internal static readonly string s_workspace_mismatch_error = "The provided access token does not match the specified workspace URL. Please verify that both values are correct.";
internal static readonly string s_invalid_service_endpoint_error_message = "The service endpoint provided is invalid. Please verify the endpoint URL and try again.";
internal static readonly string s_playwright_service_runId_length_exceeded_error_message = "Error: The Run Id you provided exceeds 200 characters. Please provide a shorter Run ID.";
internal static readonly string s_token_expiry_warning_template =
"Warning: The access token used for this test run will expire in {0} days on {1}. " +
"Generate a new token from the portal to avoid failures. " +
"For a simpler, more secure solution, switch to Microsoft Entra ID and eliminate token management. " +
"https://learn.microsoft.com/en-us/entra/identity/";

internal static readonly string s_playwright_service_disable_scalable_execution_environment_variable = "_MPT_DISABLE_SCALABLE_EXECUTION";
internal static readonly string s_playwright_service_reporting_url_environment_variable = "_MPT_REPORTING_URL";
internal static readonly string s_playwright_service_workspace_id_environment_variable = "_MPT_WORKSPACE_ID";
internal static readonly string s_playwright_service_auth_type_environment_variable = "_MPT_AUTH_TYPE";
internal static readonly string s_playwright_service_one_time_operation_flag_environment_variable = "_MPT_ONE_TIME_OPERATION_FLAG";

internal static readonly string s_playwright_service_runName_truncated_warning = "WARNING: Run name exceeds the maximum limit of 200 characters and will be truncated.";
internal static readonly int s_sevenDaysInMs = 7 * 24 * 60 * 60 * 1000;
internal static readonly int s_oneDayInMs = 24 * 60 * 60 * 1000;
}

internal class OSConstants
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.

using Azure.Core;
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Implementation;
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Interface;
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Model;
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Utility;
Expand Down Expand Up @@ -41,7 +42,23 @@ public string ServiceAuth
_serviceAuth = value;
}
}
/// <summary>
/// handles one time perform operation
/// </summary>
internal void PerformOneTimeOperation()
{
var oneTimeOperationFlag = Environment.GetEnvironmentVariable(Constants.s_playwright_service_one_time_operation_flag_environment_variable) == "true";

if (oneTimeOperationFlag)
return;

Environment.SetEnvironmentVariable(Constants.s_playwright_service_one_time_operation_flag_environment_variable, "true");

if (ServiceAuth == ServiceAuthType.AccessToken)
{
WarnIfAccessTokenCloseToExpiry();
}
}
/// <summary>
/// Gets or sets the rotation timer for Playwright service.
/// </summary>
Expand Down Expand Up @@ -138,6 +155,7 @@ public string? ExposeNetwork
private readonly EntraLifecycle? _entraLifecycle;
private readonly JsonWebTokenHandler? _jsonWebTokenHandler;
private IFrameworkLogger? _frameworkLogger;
private IConsoleWriter? _consoleWriter;

/// <summary>
/// Initializes a new instance of the <see cref="PlaywrightService"/> class.
Expand Down Expand Up @@ -175,17 +193,19 @@ public PlaywrightService(OSPlatform? os = null, string? runId = null, string? ex
_frameworkLogger = frameworkLogger;
_entraLifecycle = new EntraLifecycle(tokenCredential: credential, frameworkLogger: _frameworkLogger);
_jsonWebTokenHandler = new JsonWebTokenHandler();
_consoleWriter = new ConsoleWriter();
InitializePlaywrightServiceEnvironmentVariables(GetServiceCompatibleOs(os), runId, exposeNetwork, serviceAuth, useCloudHostedBrowsers);
}

internal PlaywrightService(OSPlatform? os = null, string? runId = null, string? exposeNetwork = null, string? serviceAuth = null, bool? useCloudHostedBrowsers = null, EntraLifecycle? entraLifecycle = null, JsonWebTokenHandler? jsonWebTokenHandler = null, TokenCredential? credential = null, IFrameworkLogger? frameworkLogger = null)
internal PlaywrightService(OSPlatform? os = null, string? runId = null, string? exposeNetwork = null, string? serviceAuth = null, bool? useCloudHostedBrowsers = null, EntraLifecycle? entraLifecycle = null, JsonWebTokenHandler? jsonWebTokenHandler = null, TokenCredential? credential = null, IFrameworkLogger? frameworkLogger = null, IConsoleWriter? consoleWriter = null)
{
if (string.IsNullOrEmpty(ServiceEndpoint))
return;
_frameworkLogger = frameworkLogger;
_jsonWebTokenHandler = jsonWebTokenHandler ?? new JsonWebTokenHandler();
_entraLifecycle = entraLifecycle ?? new EntraLifecycle(credential, _jsonWebTokenHandler, _frameworkLogger);
_frameworkLogger = frameworkLogger;
_consoleWriter = consoleWriter ?? new ConsoleWriter();
InitializePlaywrightServiceEnvironmentVariables(GetServiceCompatibleOs(os), runId, exposeNetwork, serviceAuth, useCloudHostedBrowsers);
}

Expand Down Expand Up @@ -257,6 +277,7 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default)
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceUri, null);
return;
}
PerformOneTimeOperation();
// If default auth mechanism is Access token and token is available in the environment variable, no need to setup rotation handler
if (ServiceAuth == ServiceAuthType.AccessToken)
{
Expand Down Expand Up @@ -344,6 +365,29 @@ private void InitializePlaywrightServiceEnvironmentVariables(string? os = null,
}
SetReportingUrlAndWorkspaceId();
}
internal virtual void WarnIfAccessTokenCloseToExpiry()
{
string accessToken = GetAuthToken()!;
JsonWebToken jsonWebToken = _jsonWebTokenHandler!.ReadJsonWebToken(accessToken) ?? throw new Exception(Constants.s_invalid_mpt_pat_error);
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long exp = new DateTimeOffset(jsonWebToken.ValidTo).ToUnixTimeMilliseconds();
if (PlaywrightService.IsTokenExpiringSoon(exp, currentTime))
{
WarnAboutTokenExpiry(exp, currentTime);
}
}
internal static bool IsTokenExpiringSoon(long expirationTime, long currentTime)
{
return expirationTime - currentTime <= Constants.s_sevenDaysInMs;
}

internal virtual void WarnAboutTokenExpiry(long expirationTime, long currentTime)
{
int daysToExpiration = (int)Math.Ceiling((expirationTime - currentTime) / (double)Constants.s_oneDayInMs);
string expirationDate = DateTimeOffset.FromUnixTimeMilliseconds(expirationTime).UtcDateTime.ToString("d");
string expirationWarning = string.Format(Constants.s_token_expiry_warning_template, daysToExpiration, expirationDate);
_consoleWriter?.WriteLine(expirationWarning);
}

internal static string GetDefaultRunId()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Implementation;
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Interface;
using Azure.Identity;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
Expand Down Expand Up @@ -43,6 +45,7 @@ public void Setup()
Environment.SetEnvironmentVariable(Constants.s_playwright_service_workspace_id_environment_variable, null);
Environment.SetEnvironmentVariable(Constants.s_playwright_service_disable_scalable_execution_environment_variable, null);
Environment.SetEnvironmentVariable(Constants.s_playwright_service_auth_type_environment_variable, null);
Environment.SetEnvironmentVariable(Constants.s_playwright_service_one_time_operation_flag_environment_variable, null);
}
[TearDown]
public void TearDown()
Expand All @@ -57,6 +60,7 @@ public void TearDown()
Environment.SetEnvironmentVariable(Constants.s_playwright_service_workspace_id_environment_variable, null);
Environment.SetEnvironmentVariable(Constants.s_playwright_service_disable_scalable_execution_environment_variable, null);
Environment.SetEnvironmentVariable(Constants.s_playwright_service_auth_type_environment_variable, null);
Environment.SetEnvironmentVariable(Constants.s_playwright_service_one_time_operation_flag_environment_variable, null);
}

[Test]
Expand Down Expand Up @@ -814,4 +818,103 @@ public void SetReportingUrlAndWorkspaceId_WhenReportingServiceEndpointAndWorkspa
Assert.That(Environment.GetEnvironmentVariable(Constants.s_playwright_service_workspace_id_environment_variable), Is.EqualTo("sample-id"));
});
}

[Test]
public void ShouldNotCallWarnIfAccessTokenCloseToExpiry_WhenOneTimeOperationFlagIsSet_True()
{
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, "access_token");
Environment.SetEnvironmentVariable(Constants.s_playwright_service_reporting_url_environment_variable, "https://playwright.microsoft.com");
Environment.SetEnvironmentVariable(Constants.s_playwright_service_one_time_operation_flag_environment_variable, "true");
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
defaultAzureCredentialMock
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception());
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler(), null);
var service = new Mock<PlaywrightService>(new PlaywrightServiceOptions(), null,null);
service.Object.PerformOneTimeOperation();
service.Verify(x => x.WarnIfAccessTokenCloseToExpiry(), Times.Never);
}

[Test]
public void ShouldCallWarnIfAccessTokenCloseToExpiry_WhenOneTimeOperationFlagIsSet_True()
{
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, "access_token");
Environment.SetEnvironmentVariable(Constants.s_playwright_service_reporting_url_environment_variable, "https://playwright.microsoft.com");
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
defaultAzureCredentialMock
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception());
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler(), null);
var serviceMock = new Mock<PlaywrightService>(new PlaywrightServiceOptions(serviceAuth: ServiceAuthType.AccessToken), null, null);
serviceMock.Object.PerformOneTimeOperation();
serviceMock.Verify(x => x.WarnIfAccessTokenCloseToExpiry(), Times.Once);
}

[Test]
public void ShouldReturnTrue_IfAccessTokenIs_CloseToExpiry()
{
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, "access_token");
Environment.SetEnvironmentVariable(Constants.s_playwright_service_reporting_url_environment_variable, "https://playwright.microsoft.com");
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
defaultAzureCredentialMock
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception());
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler(), null);
var serviceMock = new Mock<PlaywrightService>(new PlaywrightServiceOptions(), null, null);
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long expirationTime = DateTimeOffset.UtcNow.AddDays(4).ToUnixTimeMilliseconds();
bool isExpiringSoon = PlaywrightService.IsTokenExpiringSoon(expirationTime, currentTime);
Assert.IsTrue(isExpiringSoon);
}

[Test]
public void ShouldReturnFalse_IfAccessTokenIs_NotCloseToExpiry()
{
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, "access_token");
Environment.SetEnvironmentVariable(Constants.s_playwright_service_reporting_url_environment_variable, "https://playwright.microsoft.com");
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
defaultAzureCredentialMock
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception());
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler(), null);
var serviceMock = new Mock<PlaywrightService>(new PlaywrightServiceOptions(), null, null);
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long expirationTime = DateTimeOffset.UtcNow.AddDays(20).ToUnixTimeMilliseconds();
bool isExpiringSoon = PlaywrightService.IsTokenExpiringSoon(expirationTime, currentTime);
Assert.IsFalse(isExpiringSoon);
}

[Test]
public void ShouldLogWarning_IfWarnAboutTokenExpiry_IsCalled()
{
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, "access_token");
Environment.SetEnvironmentVariable(Constants.s_playwright_service_reporting_url_environment_variable, "https://playwright.microsoft.com");
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
defaultAzureCredentialMock
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
.ThrowsAsync(new Exception());

var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler(), null);
var consoleWriterMock = new Mock<IConsoleWriter>();
var serviceMock = new Mock<PlaywrightService>(
null,
null,
null,
null,
null,
null,
null,
null,
null,
consoleWriterMock.Object
);
serviceMock.CallBase = true;
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
long expirationTime = DateTimeOffset.UtcNow.AddDays(4).ToUnixTimeMilliseconds();
int daysToExpiration = (int)Math.Ceiling((expirationTime - currentTime) / (double)Constants.s_oneDayInMs);
string expirationDate = DateTimeOffset.FromUnixTimeMilliseconds(expirationTime).UtcDateTime.ToString("d");
string expectedWarning = string.Format(Constants.s_token_expiry_warning_template, daysToExpiration, expirationDate);
serviceMock.Object.WarnAboutTokenExpiry(expirationTime, currentTime);
consoleWriterMock.Verify(c => c.WriteLine(expectedWarning), Times.Once);
}
}
Loading