Skip to content

Commit e0d2314

Browse files
kashish2508guptakashish
andauthored
feat(playwrighttesting): showing using warning message if access token will expire within 7 days. (Azure#47748)
* feat(playwrighttesting): showing using warning message if access token will expire within 7 days. * updated and added test * update --------- Co-authored-by: guptakashish <[email protected]>
1 parent f518454 commit e0d2314

File tree

3 files changed

+154
-1
lines changed

3 files changed

+154
-1
lines changed

sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Constants.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,13 +216,21 @@ internal class Constants
216216
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.";
217217
internal static readonly string s_invalid_service_endpoint_error_message = "The service endpoint provided is invalid. Please verify the endpoint URL and try again.";
218218
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.";
219+
internal static readonly string s_token_expiry_warning_template =
220+
"Warning: The access token used for this test run will expire in {0} days on {1}. " +
221+
"Generate a new token from the portal to avoid failures. " +
222+
"For a simpler, more secure solution, switch to Microsoft Entra ID and eliminate token management. " +
223+
"https://learn.microsoft.com/en-us/entra/identity/";
219224

220225
internal static readonly string s_playwright_service_disable_scalable_execution_environment_variable = "_MPT_DISABLE_SCALABLE_EXECUTION";
221226
internal static readonly string s_playwright_service_reporting_url_environment_variable = "_MPT_REPORTING_URL";
222227
internal static readonly string s_playwright_service_workspace_id_environment_variable = "_MPT_WORKSPACE_ID";
223228
internal static readonly string s_playwright_service_auth_type_environment_variable = "_MPT_AUTH_TYPE";
229+
internal static readonly string s_playwright_service_one_time_operation_flag_environment_variable = "_MPT_ONE_TIME_OPERATION_FLAG";
224230

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

228236
internal class OSConstants

sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/PlaywrightService.cs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33

44
using Azure.Core;
5+
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Implementation;
56
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Interface;
67
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Model;
78
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Utility;
@@ -138,6 +139,7 @@ public string? ExposeNetwork
138139
private readonly EntraLifecycle? _entraLifecycle;
139140
private readonly JsonWebTokenHandler? _jsonWebTokenHandler;
140141
private IFrameworkLogger? _frameworkLogger;
142+
private IConsoleWriter? _consoleWriter;
141143

142144
/// <summary>
143145
/// Initializes a new instance of the <see cref="PlaywrightService"/> class.
@@ -175,17 +177,19 @@ public PlaywrightService(OSPlatform? os = null, string? runId = null, string? ex
175177
_frameworkLogger = frameworkLogger;
176178
_entraLifecycle = new EntraLifecycle(tokenCredential: credential, frameworkLogger: _frameworkLogger);
177179
_jsonWebTokenHandler = new JsonWebTokenHandler();
180+
_consoleWriter = new ConsoleWriter();
178181
InitializePlaywrightServiceEnvironmentVariables(GetServiceCompatibleOs(os), runId, exposeNetwork, serviceAuth, useCloudHostedBrowsers);
179182
}
180183

181-
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)
184+
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)
182185
{
183186
if (string.IsNullOrEmpty(ServiceEndpoint))
184187
return;
185188
_frameworkLogger = frameworkLogger;
186189
_jsonWebTokenHandler = jsonWebTokenHandler ?? new JsonWebTokenHandler();
187190
_entraLifecycle = entraLifecycle ?? new EntraLifecycle(credential, _jsonWebTokenHandler, _frameworkLogger);
188191
_frameworkLogger = frameworkLogger;
192+
_consoleWriter = consoleWriter ?? new ConsoleWriter();
189193
InitializePlaywrightServiceEnvironmentVariables(GetServiceCompatibleOs(os), runId, exposeNetwork, serviceAuth, useCloudHostedBrowsers);
190194
}
191195

@@ -257,6 +261,7 @@ public async Task InitializeAsync(CancellationToken cancellationToken = default)
257261
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceUri, null);
258262
return;
259263
}
264+
PerformOneTimeOperation();
260265
// If default auth mechanism is Access token and token is available in the environment variable, no need to setup rotation handler
261266
if (ServiceAuth == ServiceAuthType.AccessToken)
262267
{
@@ -277,6 +282,20 @@ public void Cleanup()
277282
_frameworkLogger?.Info("Cleaning up Playwright service resources.");
278283
RotationTimer?.Dispose();
279284
}
285+
internal void PerformOneTimeOperation()
286+
{
287+
var oneTimeOperationFlag = Environment.GetEnvironmentVariable(Constants.s_playwright_service_one_time_operation_flag_environment_variable) == "true";
288+
289+
if (oneTimeOperationFlag)
290+
return;
291+
292+
Environment.SetEnvironmentVariable(Constants.s_playwright_service_one_time_operation_flag_environment_variable, "true");
293+
294+
if (ServiceAuth == ServiceAuthType.AccessToken)
295+
{
296+
WarnIfAccessTokenCloseToExpiry();
297+
}
298+
}
280299

281300
internal async void RotationHandlerAsync(object? _)
282301
{
@@ -344,6 +363,29 @@ private void InitializePlaywrightServiceEnvironmentVariables(string? os = null,
344363
}
345364
SetReportingUrlAndWorkspaceId();
346365
}
366+
internal virtual void WarnIfAccessTokenCloseToExpiry()
367+
{
368+
string accessToken = GetAuthToken()!;
369+
JsonWebToken jsonWebToken = _jsonWebTokenHandler!.ReadJsonWebToken(accessToken) ?? throw new Exception(Constants.s_invalid_mpt_pat_error);
370+
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
371+
long exp = new DateTimeOffset(jsonWebToken.ValidTo).ToUnixTimeMilliseconds();
372+
if (PlaywrightService.IsTokenExpiringSoon(exp, currentTime))
373+
{
374+
WarnAboutTokenExpiry(exp, currentTime);
375+
}
376+
}
377+
internal static bool IsTokenExpiringSoon(long expirationTime, long currentTime)
378+
{
379+
return expirationTime - currentTime <= Constants.s_sevenDaysInMs;
380+
}
381+
382+
internal virtual void WarnAboutTokenExpiry(long expirationTime, long currentTime)
383+
{
384+
int daysToExpiration = (int)Math.Ceiling((expirationTime - currentTime) / (double)Constants.s_oneDayInMs);
385+
string expirationDate = DateTimeOffset.FromUnixTimeMilliseconds(expirationTime).UtcDateTime.ToString("d");
386+
string expirationWarning = string.Format(Constants.s_token_expiry_warning_template, daysToExpiration, expirationDate);
387+
_consoleWriter?.WriteLine(expirationWarning);
388+
}
347389

348390
internal static string GetDefaultRunId()
349391
{

sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/tests/PlaywrightServiceTests.cs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
using System.Threading;
99
using System.Threading.Tasks;
1010
using Azure.Core;
11+
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Implementation;
12+
using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Interface;
1113
using Azure.Identity;
1214
using Microsoft.IdentityModel.JsonWebTokens;
1315
using Microsoft.IdentityModel.Tokens;
@@ -43,6 +45,7 @@ public void Setup()
4345
Environment.SetEnvironmentVariable(Constants.s_playwright_service_workspace_id_environment_variable, null);
4446
Environment.SetEnvironmentVariable(Constants.s_playwright_service_disable_scalable_execution_environment_variable, null);
4547
Environment.SetEnvironmentVariable(Constants.s_playwright_service_auth_type_environment_variable, null);
48+
Environment.SetEnvironmentVariable(Constants.s_playwright_service_one_time_operation_flag_environment_variable, null);
4649
}
4750
[TearDown]
4851
public void TearDown()
@@ -57,6 +60,7 @@ public void TearDown()
5760
Environment.SetEnvironmentVariable(Constants.s_playwright_service_workspace_id_environment_variable, null);
5861
Environment.SetEnvironmentVariable(Constants.s_playwright_service_disable_scalable_execution_environment_variable, null);
5962
Environment.SetEnvironmentVariable(Constants.s_playwright_service_auth_type_environment_variable, null);
63+
Environment.SetEnvironmentVariable(Constants.s_playwright_service_one_time_operation_flag_environment_variable, null);
6064
}
6165

6266
[Test]
@@ -814,4 +818,103 @@ public void SetReportingUrlAndWorkspaceId_WhenReportingServiceEndpointAndWorkspa
814818
Assert.That(Environment.GetEnvironmentVariable(Constants.s_playwright_service_workspace_id_environment_variable), Is.EqualTo("sample-id"));
815819
});
816820
}
821+
822+
[Test]
823+
public void ShouldNotCallWarnIfAccessTokenCloseToExpiry_WhenOneTimeOperationFlagIsSet_True()
824+
{
825+
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, "access_token");
826+
Environment.SetEnvironmentVariable(Constants.s_playwright_service_reporting_url_environment_variable, "https://playwright.microsoft.com");
827+
Environment.SetEnvironmentVariable(Constants.s_playwright_service_one_time_operation_flag_environment_variable, "true");
828+
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
829+
defaultAzureCredentialMock
830+
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
831+
.ThrowsAsync(new Exception());
832+
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler(), null);
833+
var service = new Mock<PlaywrightService>(new PlaywrightServiceOptions(), null,null);
834+
service.Object.PerformOneTimeOperation();
835+
service.Verify(x => x.WarnIfAccessTokenCloseToExpiry(), Times.Never);
836+
}
837+
838+
[Test]
839+
public void ShouldCallWarnIfAccessTokenCloseToExpiry_WhenOneTimeOperationFlagIsSet_True()
840+
{
841+
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, "access_token");
842+
Environment.SetEnvironmentVariable(Constants.s_playwright_service_reporting_url_environment_variable, "https://playwright.microsoft.com");
843+
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
844+
defaultAzureCredentialMock
845+
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
846+
.ThrowsAsync(new Exception());
847+
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler(), null);
848+
var serviceMock = new Mock<PlaywrightService>(new PlaywrightServiceOptions(serviceAuth: ServiceAuthType.AccessToken), null, null);
849+
serviceMock.Object.PerformOneTimeOperation();
850+
serviceMock.Verify(x => x.WarnIfAccessTokenCloseToExpiry(), Times.Once);
851+
}
852+
853+
[Test]
854+
public void ShouldReturnTrue_IfAccessTokenIs_CloseToExpiry()
855+
{
856+
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, "access_token");
857+
Environment.SetEnvironmentVariable(Constants.s_playwright_service_reporting_url_environment_variable, "https://playwright.microsoft.com");
858+
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
859+
defaultAzureCredentialMock
860+
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
861+
.ThrowsAsync(new Exception());
862+
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler(), null);
863+
var serviceMock = new Mock<PlaywrightService>(new PlaywrightServiceOptions(), null, null);
864+
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
865+
long expirationTime = DateTimeOffset.UtcNow.AddDays(4).ToUnixTimeMilliseconds();
866+
bool isExpiringSoon = PlaywrightService.IsTokenExpiringSoon(expirationTime, currentTime);
867+
Assert.IsTrue(isExpiringSoon);
868+
}
869+
870+
[Test]
871+
public void ShouldReturnFalse_IfAccessTokenIs_NotCloseToExpiry()
872+
{
873+
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, "access_token");
874+
Environment.SetEnvironmentVariable(Constants.s_playwright_service_reporting_url_environment_variable, "https://playwright.microsoft.com");
875+
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
876+
defaultAzureCredentialMock
877+
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
878+
.ThrowsAsync(new Exception());
879+
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler(), null);
880+
var serviceMock = new Mock<PlaywrightService>(new PlaywrightServiceOptions(), null, null);
881+
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
882+
long expirationTime = DateTimeOffset.UtcNow.AddDays(20).ToUnixTimeMilliseconds();
883+
bool isExpiringSoon = PlaywrightService.IsTokenExpiringSoon(expirationTime, currentTime);
884+
Assert.IsFalse(isExpiringSoon);
885+
}
886+
887+
[Test]
888+
public void ShouldLogWarning_IfWarnAboutTokenExpiry_IsCalled()
889+
{
890+
Environment.SetEnvironmentVariable(ServiceEnvironmentVariable.PlaywrightServiceAccessToken, "access_token");
891+
Environment.SetEnvironmentVariable(Constants.s_playwright_service_reporting_url_environment_variable, "https://playwright.microsoft.com");
892+
var defaultAzureCredentialMock = new Mock<DefaultAzureCredential>();
893+
defaultAzureCredentialMock
894+
.Setup(x => x.GetTokenAsync(It.IsAny<TokenRequestContext>(), It.IsAny<CancellationToken>()))
895+
.ThrowsAsync(new Exception());
896+
897+
var entraLifecycleMock = new Mock<EntraLifecycle>(defaultAzureCredentialMock.Object, new JsonWebTokenHandler(), null);
898+
var consoleWriterMock = new Mock<IConsoleWriter>();
899+
var serviceMock = new Mock<PlaywrightService>(
900+
null,
901+
null,
902+
null,
903+
null,
904+
null,
905+
null,
906+
null,
907+
null,
908+
null,
909+
consoleWriterMock.Object
910+
);
911+
serviceMock.CallBase = true;
912+
long currentTime = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
913+
long expirationTime = DateTimeOffset.UtcNow.AddDays(4).ToUnixTimeMilliseconds();
914+
int daysToExpiration = (int)Math.Ceiling((expirationTime - currentTime) / (double)Constants.s_oneDayInMs);
915+
string expirationDate = DateTimeOffset.FromUnixTimeMilliseconds(expirationTime).UtcDateTime.ToString("d");
916+
string expectedWarning = string.Format(Constants.s_token_expiry_warning_template, daysToExpiration, expirationDate);
917+
serviceMock.Object.WarnAboutTokenExpiry(expirationTime, currentTime);
918+
consoleWriterMock.Verify(c => c.WriteLine(expectedWarning), Times.Once);
919+
}
817920
}

0 commit comments

Comments
 (0)