diff --git a/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Implementation/BlobService.cs b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Implementation/BlobService.cs new file mode 100644 index 000000000000..6b436c45f9ff --- /dev/null +++ b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Implementation/BlobService.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Text; +using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Interface; +using Azure.Storage.Blobs; +using System.Threading.Tasks; +using System.IO; + +namespace Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Implementation +{ + internal class BlobService:IBlobService + { + private readonly ILogger _logger; + + public BlobService(ILogger? logger) + { + _logger = logger ?? new Logger(); + } + + public async Task UploadBufferAsync(string uri, string buffer, string fileRelativePath) + { + try + { + string cloudFilePath = GetCloudFilePath(uri, fileRelativePath); + BlobClient blobClient = new(new Uri(cloudFilePath)); + byte[] bufferBytes = Encoding.UTF8.GetBytes(buffer); + await blobClient.UploadAsync(new BinaryData(bufferBytes), overwrite: true).ConfigureAwait(false); + _logger.Info($"Uploaded buffer to {fileRelativePath}"); + } + catch (Exception ex) + { + _logger.Error($"Failed to upload buffer: {ex}"); + } + } + public void UploadBlobFile(string uri, string fileRelativePath, string filePath) + { + string cloudFilePath = GetCloudFilePath(uri, fileRelativePath); + BlobClient blobClient = new(new Uri(cloudFilePath)); + blobClient.Upload(filePath, overwrite: true); + _logger.Info($"Uploaded file {filePath} to {fileRelativePath}"); + } + public string GetCloudFilePath(string uri, string fileRelativePath) + { + string[] parts = uri.Split(new string[] { ReporterConstants.s_sASUriSeparator }, StringSplitOptions.None); + string containerUri = parts[0]; + string sasToken = parts.Length > 1 ? parts[1] : string.Empty; + + return $"{containerUri}/{fileRelativePath}?{sasToken}"; + } + public string? GetCloudFileName(string filePath, string testExecutionId) + { + var fileName = Path.GetFileName(filePath); + if (fileName == null) + { + return null; + } + return $"{testExecutionId}/{fileName}"; // TODO check if we need to add {Guid.NewGuid()} for file with same name + } + } +} diff --git a/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Interface/IBlobService.cs b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Interface/IBlobService.cs new file mode 100644 index 000000000000..587903f1418f --- /dev/null +++ b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Interface/IBlobService.cs @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Threading.Tasks; +namespace Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Interface +{ + internal interface IBlobService + { + /// + /// + /// + /// + /// + /// + /// A representing the result of the asynchronous operation. + Task UploadBufferAsync(string uri, string buffer, string fileRelativePath); + string GetCloudFilePath(string uri, string fileRelativePath); + void UploadBlobFile(string uri, string fileRelativePath, string filePath); + public string? GetCloudFileName(string filePath, string testExecutionId); + } +} diff --git a/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Processor/TestProcessor.cs b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Processor/TestProcessor.cs index d90affe987a6..6bf1c260b2a1 100644 --- a/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Processor/TestProcessor.cs +++ b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Processor/TestProcessor.cs @@ -7,6 +7,7 @@ using System.IO; using System.Text; using System.Text.Json; +using System.Threading.Tasks; using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Implementation; using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Interface; using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Model; @@ -28,6 +29,7 @@ internal class TestProcessor : ITestProcessor private readonly IConsoleWriter _consoleWriter; private readonly CIInfo _cIInfo; private readonly CloudRunMetadata _cloudRunMetadata; + private readonly IBlobService _blobService; // Test Metadata internal int TotalTestCount { get; set; } = 0; @@ -42,7 +44,7 @@ internal class TestProcessor : ITestProcessor internal TestRunShardDto? _testRunShard; internal TestResultsUri? _testResultsSasUri; - public TestProcessor(CloudRunMetadata cloudRunMetadata, CIInfo cIInfo, ILogger? logger = null, IDataProcessor? dataProcessor = null, ICloudRunErrorParser? cloudRunErrorParser = null, IServiceClient? serviceClient = null, IConsoleWriter? consoleWriter = null) + public TestProcessor(CloudRunMetadata cloudRunMetadata, CIInfo cIInfo, ILogger? logger = null, IDataProcessor? dataProcessor = null, ICloudRunErrorParser? cloudRunErrorParser = null, IServiceClient? serviceClient = null, IConsoleWriter? consoleWriter = null, IBlobService? blobService = null) { _cloudRunMetadata = cloudRunMetadata; _cIInfo = cIInfo; @@ -51,6 +53,7 @@ public TestProcessor(CloudRunMetadata cloudRunMetadata, CIInfo cIInfo, ILogger? _cloudRunErrorParser = cloudRunErrorParser ?? new CloudRunErrorParser(_logger); _serviceClient = serviceClient ?? new ServiceClient(_cloudRunMetadata, _cloudRunErrorParser); _consoleWriter = consoleWriter ?? new ConsoleWriter(); + _blobService = blobService ?? new BlobService(_logger); } public void TestRunStartHandler(object? sender, TestRunStartEventArgs e) @@ -171,7 +174,7 @@ public void TestRunCompleteHandler(object? sender, TestRunCompleteEventArgs e) // Upload rawResult to blob storage using sasUri var rawTestResultJson = JsonSerializer.Serialize(rawResult); var filePath = $"{testResult.TestExecutionId}/rawTestResult.json"; - UploadBuffer(sasUri!.Uri!, rawTestResultJson, filePath); + _blobService.UploadBufferAsync(sasUri!.Uri!, rawTestResultJson, filePath); } else { @@ -215,9 +218,9 @@ private void UploadAttachment(TestResultEventArgs e, string testExecutionId) { // get file size var fileSize = new FileInfo(filePath).Length; - var cloudFileName = ReporterUtils.GetCloudFileName(filePath, testExecutionId); + var cloudFileName = _blobService.GetCloudFileName(filePath, testExecutionId); if (cloudFileName != null) { - UploadBlobFile(_testResultsSasUri!.Uri!, cloudFileName, filePath); + _blobService.UploadBlobFile(_testResultsSasUri!.Uri!, cloudFileName, filePath); TotalArtifactCount++; TotalArtifactSizeInBytes = TotalArtifactSizeInBytes + (int)fileSize; } @@ -237,7 +240,7 @@ private void UploadAttachment(TestResultEventArgs e, string testExecutionId) } } - private TestResultsUri? CheckAndRenewSasUri() + internal TestResultsUri? CheckAndRenewSasUri() { var reporterUtils = new ReporterUtils(); if (_testResultsSasUri == null || !reporterUtils.IsTimeGreaterThanCurrentPlus10Minutes(_testResultsSasUri.Uri)) @@ -270,31 +273,6 @@ private void EndTestRun(TestRunCompleteEventArgs e) } _cloudRunErrorParser.DisplayMessages(); } - private static string GetCloudFilePath(string uri, string fileRelativePath) - { - string[] parts = uri.Split(new string[] { ReporterConstants.s_sASUriSeparator }, StringSplitOptions.None); - string containerUri = parts[0]; - string sasToken = parts.Length > 1 ? parts[1] : string.Empty; - - return $"{containerUri}/{fileRelativePath}?{sasToken}"; - } - private void UploadBuffer(string uri, string buffer, string fileRelativePath) - { - string cloudFilePath = GetCloudFilePath(uri, fileRelativePath); - BlobClient blobClient = new(new Uri(cloudFilePath)); - byte[] bufferBytes = Encoding.UTF8.GetBytes(buffer); - blobClient.Upload(new BinaryData(bufferBytes), overwrite: true); - _logger.Info($"Uploaded buffer to {fileRelativePath}"); - } - - private void UploadBlobFile(string uri, string fileRelativePath, string filePath) - { - string cloudFilePath = GetCloudFilePath(uri, fileRelativePath); - BlobClient blobClient = new(new Uri(cloudFilePath)); - // Upload filePath to Blob - blobClient.Upload(filePath, overwrite: true); - _logger.Info($"Uploaded file {filePath} to {fileRelativePath}"); - } private TestRunShardDto GetTestRunEndShard(TestRunCompleteEventArgs e) { diff --git a/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Utility/ReporterUtils.cs b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Utility/ReporterUtils.cs index d17aa019b488..40390871d682 100644 --- a/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Utility/ReporterUtils.cs +++ b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/src/Utility/ReporterUtils.cs @@ -115,15 +115,6 @@ internal static string GetCurrentOS() else return OSConstants.s_wINDOWS; } - internal static string? GetCloudFileName(string filePath, string testExecutionId) - { - var fileName = Path.GetFileName(filePath); - if (fileName == null) - { - return null; - } - return $"{testExecutionId}/{fileName}"; // TODO check if we need to add {Guid.NewGuid()} for file with same name - } internal TokenDetails ParseWorkspaceIdFromAccessToken(JsonWebTokenHandler? jsonWebTokenHandler, string? accessToken) { diff --git a/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/tests/Implementation/BlobServiceTests.cs b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/tests/Implementation/BlobServiceTests.cs new file mode 100644 index 000000000000..eaa6006b8459 --- /dev/null +++ b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/tests/Implementation/BlobServiceTests.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Text; +using System.Threading.Tasks; +using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Implementation; +using Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Interface; +using Moq; +using NUnit.Framework; + +namespace Azure.Developer.MicrosoftPlaywrightTesting.TestLogger.Tests.Implementation +{ + [TestFixture] + [Parallelizable(ParallelScope.Self)] + public class BlobServiceTests + { + private Mock? _loggerMock; + private BlobService? _blobService; + + [SetUp] + public void Setup() + { + _loggerMock = new Mock(); + _blobService = new BlobService(_loggerMock.Object); + } + [Test] + public async Task UploadBufferAsync_WithException_LogsError() + { + string uri = "invalid_uri"; + string buffer = "Test buffer"; + string fileRelativePath = "test/path"; + + await _blobService!.UploadBufferAsync(uri, buffer, fileRelativePath); + + _loggerMock!.Verify(logger => logger.Error(It.IsAny()), Times.Once); + } + + [Test] + public void GetCloudFilePath_WithValidParameters_ReturnsCorrectPath() + { + string uri = "https://example.com/container"; + string fileRelativePath = "test/path"; + string expectedPath = "https://example.com/container/test/path?"; + + string? result = _blobService?.GetCloudFilePath(uri, fileRelativePath); + + Assert.AreEqual(expectedPath, result); + } + + [Test] + public void GetCloudFilePath_WithSasUri_ReturnsCorrectPath() + { + string uri = "https://example.com/container?sasToken"; + string fileRelativePath = "test/path"; + string expectedPath = "https://example.com/container/test/path?sasToken"; + + string? result = _blobService?.GetCloudFilePath(uri, fileRelativePath); + + Assert.AreEqual(expectedPath, result); + } + } +} diff --git a/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/tests/Processor/TestProcessorTests.cs b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/tests/Processor/TestProcessorTests.cs index 652f73195b3e..2778c673da27 100644 --- a/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/tests/Processor/TestProcessorTests.cs +++ b/sdk/playwrighttesting/Azure.Developer.MicrosoftPlaywrightTesting.TestLogger/tests/Processor/TestProcessorTests.cs @@ -580,5 +580,38 @@ public void TestRunCompleteHandler_EnableResultPublishSetToFalse_DisplaysMessage serviceClientMock.Verify(c => c.PostTestRunShardInfo(It.IsAny()), Times.Never); cloudRunErrorParserMock.Verify(c => c.DisplayMessages(), Times.Exactly(1)); } + [Test] + public void CheckAndRenewSasUri_WhenUriExpired_FetchesNewSasUri() + { + var loggerMock = new Mock(); + var dataProcessorMock = new Mock(); + var consoleWriterMock = new Mock(); + var cloudRunErrorParserMock = new Mock(); + var serviceClientMock = new Mock(); + var testProcessor = new TestProcessor(_cloudRunMetadata, _cIInfo, loggerMock.Object, dataProcessorMock.Object, cloudRunErrorParserMock.Object, serviceClientMock.Object, consoleWriterMock.Object); + var expiredTestResultsSasUri = new TestResultsUri { Uri = "http://example.com", ExpiresAt = DateTime.UtcNow.AddMinutes(-5).ToString(), AccessLevel = AccessLevel.Read }; + var newTestResultsSasUri = new TestResultsUri { Uri = "http://newexample.com", ExpiresAt = DateTime.UtcNow.AddHours(1).ToString(), AccessLevel = AccessLevel.Read }; + testProcessor._testResultsSasUri = expiredTestResultsSasUri; + serviceClientMock.Setup(sc => sc.GetTestRunResultsUri()).Returns(newTestResultsSasUri); + TestResultsUri? result = testProcessor.CheckAndRenewSasUri(); + Assert.AreEqual(newTestResultsSasUri, result); + loggerMock.Verify(l => l.Info(It.IsAny()), Times.AtLeastOnce); + } + [Test] + public void CheckAndRenewSasUri_WhenUriNotExpired_DoesNotFetchNewSasUri() + { + var loggerMock = new Mock(); + var dataProcessorMock = new Mock(); + var consoleWriterMock = new Mock(); + var cloudRunErrorParserMock = new Mock(); + var serviceClientMock = new Mock(); + var testProcessor = new TestProcessor(_cloudRunMetadata, _cIInfo, loggerMock.Object, dataProcessorMock.Object, cloudRunErrorParserMock.Object, serviceClientMock.Object, consoleWriterMock.Object); + var validTestResultsSasUri = new TestResultsUri { Uri = "http://example.com?se=" + DateTime.UtcNow.AddMinutes(15).ToString("o"), ExpiresAt = DateTime.UtcNow.AddMinutes(15).ToString(), AccessLevel = AccessLevel.Read }; + testProcessor._testResultsSasUri = validTestResultsSasUri; + TestResultsUri? result = testProcessor.CheckAndRenewSasUri(); + Assert.AreEqual(validTestResultsSasUri, result); + serviceClientMock.Verify(sc => sc.GetTestRunResultsUri(), Times.Never); + loggerMock.Verify(l => l.Info(It.IsAny()), Times.Never); + } } }