diff --git a/docs/CommandInput.md b/docs/CommandInput.md index 4a97113ec..bff056806 100644 --- a/docs/CommandInput.md +++ b/docs/CommandInput.md @@ -9,7 +9,7 @@ The executable can take in inputs defined in `config.dev.json`, or as command li | EnvironmentId | Environment that the Power Apps app you are testing is located in. For more information about environments, please view [this](https://docs.microsoft.com/en-us/power-platform/admin/environments-overview) | | TenantId | Tenant that the Power Apps app is located in. | | TestPlanFile | Path to the test plan that you wish to run | -| OutputDirectory | Path to folder the test results will be placed. Optional. If this is not provided, it will be placed in the `TestOutput` folder. | +| OutputDirectory | Relative path to folder the test results will be placed. Optional. If this is not provided, it will be placed in the `TestOutput` folder. All results and logs will be placed under file system's designated `TestEngine` location under user's temp directory. | | LogLevel | Level for logging (Folllows [this](https://docs.microsoft.com/en-us/dotnet/api/microsoft.extensions.logging.loglevel?view=dotnet-plat-ext-6.0)). Optional. If this is not provided, Information level logs and higher will be logged | | QueryParams | Specify query parameters to be added to the Power Apps URL. | | Domain | Specify what URL domain your app uses. This is optional; if not set, it will default to 'apps.powerapps.com'. | diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/Reporting/TestLoggerTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/Reporting/TestLoggerTests.cs index bcd796d57..92ec67e3f 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/Reporting/TestLoggerTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/Reporting/TestLoggerTests.cs @@ -63,6 +63,8 @@ public void WriteToLogsFileThrowsOnInvalidPathTest() MockFileSystem.Setup(x => x.Exists(It.IsAny())).Returns(false); MockFileSystem.Setup(x => x.IsValidFilePath(It.IsAny())).Returns(false); MockFileSystem.Setup(x => x.CreateDirectory(It.IsAny())); + MockFileSystem.Setup(x => x.GetTempPath()).Returns(""); + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns(""); MockFileSystem.Setup(x => x.WriteTextToFile(It.IsAny(), It.IsAny())).Callback((string filePath, string[] logs) => { createdLogs.Add(filePath, logs); diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/System/FileSystemTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/System/FileSystemTests.cs index 6c5aa243b..d56defac6 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/System/FileSystemTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/System/FileSystemTests.cs @@ -1,8 +1,12 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. +using System.IO; +using System; using Microsoft.PowerApps.TestEngine.System; using Xunit; +using System.Linq; +using Moq; namespace Microsoft.PowerApps.TestEngine.Tests.System { @@ -15,7 +19,12 @@ public class FileSystemTests [InlineData("C:\\folder", true)] [InlineData("", false)] [InlineData(null, false)] - [InlineData("C:/fold:er", false)] + [InlineData("C:/fold>er/fg", false)] + [InlineData("C:/folder/f>g", false)] + [InlineData("C:/folder/f:g", false)] + [InlineData("C:/folder/fg/", false)] + [InlineData("../folder/fg", true)] + [InlineData("../folder/f:g", false)] public void IsValidFilePathTest(string? filePath, bool expectedResult) { var fileSystem = new FileSystem(); @@ -37,5 +46,70 @@ public void RemoveInvalidFileNameCharsTest(string inputFileName, string expected var result = fileSystem.RemoveInvalidFileNameChars(inputFileName); Assert.Equal(expectedFileName, result); } + + [Fact] + public void IsWritePermittedFilePath_ValidRootedPath_ReturnsTrue() + { + var _fileSystem = new FileSystem(); + var validPath = Path.Combine(_fileSystem.GetDefaultRootTestEngine(), "testfile.txt"); + Assert.True(_fileSystem.IsWritePermittedFilePath(validPath)); + } + [Fact] + public void IsWritePermittedFilePath_SameAsRootedPath_ReturnsFalse() + { + var _fileSystem = new FileSystem(); + var validPath = Path.Combine(_fileSystem.GetDefaultRootTestEngine(), ""); + Assert.False(_fileSystem.IsWritePermittedFilePath(validPath)); + } + + + [Fact] + public void IsWritePermittedFilePath_RelativePath_ReturnsFalse() + { + var _fileSystem = new FileSystem(); + var relativePath = @"..\testfile.txt"; + Assert.False(_fileSystem.IsWritePermittedFilePath(relativePath)); + } + + [Fact] + public void IsWritePermittedFilePath_InvalidRootedPath_ReturnsFalse() + { + var _fileSystem = new FileSystem(); + var invalidPath = Path.Combine(_fileSystem.GetTempPath(), "invalidfolder", "testfile.txt"); + Assert.False(_fileSystem.IsWritePermittedFilePath(invalidPath)); + } + + [Fact] + public void IsWritePermittedFilePath_NullPath_ReturnsFalse() + { + var _fileSystem = new FileSystem(); + Assert.False(_fileSystem.IsWritePermittedFilePath(null)); + } + + [Fact] + public void IsWritePermittedFilePath_ValidPathWithParentDirectoryTraversal_ReturnsFalse() + { + var _fileSystem = new FileSystem(); + var pathWithParentTraversal = _fileSystem.GetDefaultRootTestEngine() + Path.DirectorySeparatorChar + @"..\testfile.txt"; + Assert.False(_fileSystem.IsWritePermittedFilePath(pathWithParentTraversal)); + } + + [Fact] + public void WriteTextToFile_UnpermittedFilePath_ThrowsInvalidOperationException() + { + var _fileSystem = new FileSystem(); + var invalidFilePath = "C:\\InvalidFolder\\testfile.txt"; + var exception = Assert.Throws(() => _fileSystem.WriteTextToFile(invalidFilePath, "")); + Assert.Contains(invalidFilePath, exception.Message); + } + + [Fact] + public void WriteTextToFile_ArrayText_UnpermittedFilePath_ThrowsInvalidOperationException() + { + var _fileSystem = new FileSystem(); + var invalidFilePath = "C:\\InvalidFolder\\testfile.txt"; + var exception = Assert.Throws(() => _fileSystem.WriteTextToFile(invalidFilePath, new string[] { "This should fail." })); + Assert.Contains(invalidFilePath, exception.Message); + } } } diff --git a/src/Microsoft.PowerApps.TestEngine.Tests/TestEngineTests.cs b/src/Microsoft.PowerApps.TestEngine.Tests/TestEngineTests.cs index e81eb52ad..c19770d88 100644 --- a/src/Microsoft.PowerApps.TestEngine.Tests/TestEngineTests.cs +++ b/src/Microsoft.PowerApps.TestEngine.Tests/TestEngineTests.cs @@ -80,6 +80,7 @@ public async Task TestEngineWithDefaultParamsTest() var environmentId = "defaultEnviroment"; var tenantId = new Guid("a01af035-a529-4aaf-aded-011ad676f976"); var outputDirectory = new DirectoryInfo("TestOutput"); + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns(outputDirectory.FullName); var testRunId = Guid.NewGuid().ToString(); var expectedOutputDirectory = outputDirectory.FullName; var testRunDirectory = Path.Combine(expectedOutputDirectory, testRunId.Substring(0, 6)); @@ -116,6 +117,8 @@ public async Task TestEngineWithInvalidLocaleTest() var environmentId = "defaultEnviroment"; var tenantId = new Guid("a01af035-a529-4aaf-aded-011ad676f976"); var outputDirectory = new DirectoryInfo("TestOutput"); + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns(outputDirectory.FullName); + var testRunId = Guid.NewGuid().ToString(); var expectedOutputDirectory = outputDirectory.FullName; var testRunDirectory = Path.Combine(expectedOutputDirectory, testRunId.Substring(0, 6)); @@ -154,6 +157,7 @@ public async Task TestEngineWithUnspecifiedLocaleShowsWarning() var environmentId = "defaultEnviroment"; var tenantId = new Guid("a01af035-a529-4aaf-aded-011ad676f976"); var outputDirectory = new DirectoryInfo("TestOutput"); + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns(outputDirectory.FullName); var testRunId = Guid.NewGuid().ToString(); var expectedOutputDirectory = outputDirectory.FullName; var testRunDirectory = Path.Combine(expectedOutputDirectory, testRunId.Substring(0, 6)); @@ -203,6 +207,7 @@ public async Task TestEngineWithMultipleBrowserConfigTest() var expectedTestReportPath = "C:\\test.trx"; SetupMocks(expectedOutputDirectory, testSettings, testSuiteDefinition, testRunId, expectedTestReportPath); + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns(outputDirectory.FullName); var testEngine = new TestEngine(MockState.Object, ServiceProvider, MockTestReporter.Object, MockFileSystem.Object, MockLoggerFactory.Object, MockTestEngineEventHandler.Object); var testReportPath = await testEngine.RunTestAsync(testConfigFile, environmentId, tenantId, outputDirectory, domain, ""); @@ -241,6 +246,7 @@ public async Task TestEngineTest(DirectoryInfo outputDirectory, string domain, T var tenantId = new Guid("a01af035-a529-4aaf-aded-011ad676f976"); var testRunId = Guid.NewGuid().ToString(); + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns(outputDirectory.FullName); var expectedOutputDirectory = outputDirectory; if (expectedOutputDirectory == null) { @@ -347,6 +353,32 @@ public async Task TestEngineThrowsOnNullArguments(string? testConfigFilePath, st await Assert.ThrowsAsync(async () => await testEngine.RunTestAsync(testConfigFile, environmentId, tenantId, outputDirectory, domain, "")); } + [Theory] + [InlineData("C:\\testPath")] + [InlineData("testPath")] + [InlineData("..\\testPath")] + public async Task TestEngineExceptionOnNotPermittedOutputPath(string outputDirLoc) + { + var testConfigFile = new FileInfo("C:\\testPlan.fx.yaml"); + var environmentId = "defaultEnviroment"; + var tenantId = new Guid("a01af035-a529-4aaf-aded-011ad676f976"); + var domain = "apps.powerapps.com"; + + MockTestReporter.Setup(x => x.CreateTestRun(It.IsAny(), It.IsAny())).Returns("abcdef"); + MockTestReporter.Setup(x => x.StartTestRun(It.IsAny())); + MockLoggerFactory.Setup(x => x.CreateLogger(It.IsAny())).Returns(MockLogger.Object); + LoggingTestHelper.SetupMock(MockLogger); + MockTestLoggerProvider.Setup(x => x.CreateLogger(It.IsAny())).Returns(MockLogger.Object); + MockTestEngineEventHandler.Setup(x => x.EncounteredException(It.IsAny())); + + var testEngine = new TestEngine(MockState.Object, ServiceProvider, MockTestReporter.Object, MockFileSystem.Object, MockLoggerFactory.Object, MockTestEngineEventHandler.Object); + var outputDirectory = new DirectoryInfo(outputDirLoc); + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns("C:\\testPath" + Path.DirectorySeparatorChar); + var testResultsDirectory = await testEngine.RunTestAsync(testConfigFile, environmentId, tenantId, outputDirectory, domain, ""); + // UserInput Exception is handled within TestEngineEventHandler, and then returns the test results directory path + MockTestEngineEventHandler.Verify(x => x.EncounteredException(It.IsAny()), Times.Once()); + } + [Fact] public async Task TestEngineReturnsPathOnUserInputErrors() { @@ -370,6 +402,7 @@ public async Task TestEngineReturnsPathOnUserInputErrors() var testEngine = new TestEngine(MockState.Object, ServiceProvider, MockTestReporter.Object, MockFileSystem.Object, MockLoggerFactory.Object, MockTestEngineEventHandler.Object); var outputDirectory = new DirectoryInfo("TestOutput"); + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns(outputDirectory.FullName); var testResultsDirectory = await testEngine.RunTestAsync(testConfigFile, environmentId, tenantId, outputDirectory, domain, ""); // UserInput Exception is handled within TestEngineEventHandler, and then returns the test results directory path diff --git a/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj b/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj index dce06d359..43634c2e5 100644 --- a/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj +++ b/src/Microsoft.PowerApps.TestEngine/Microsoft.PowerApps.TestEngine.csproj @@ -29,7 +29,7 @@ - + diff --git a/src/Microsoft.PowerApps.TestEngine/Reporting/TestLogger.cs b/src/Microsoft.PowerApps.TestEngine/Reporting/TestLogger.cs index e4b0f5153..f5c5ef37d 100644 --- a/src/Microsoft.PowerApps.TestEngine/Reporting/TestLogger.cs +++ b/src/Microsoft.PowerApps.TestEngine/Reporting/TestLogger.cs @@ -55,9 +55,7 @@ public void WriteToLogsFile(string directoryPath, string filter) { if (!_fileSystem.Exists(directoryPath)) { - var assemblyLocation = Assembly.GetExecutingAssembly().Location; - var assemblyDirectory = Path.GetDirectoryName(assemblyLocation); - directoryPath = Path.Combine(assemblyDirectory, "logs"); + directoryPath = Path.Combine(_fileSystem.GetDefaultRootTestEngine(), "logs"); _fileSystem.CreateDirectory(directoryPath); } diff --git a/src/Microsoft.PowerApps.TestEngine/System/FileSystem.cs b/src/Microsoft.PowerApps.TestEngine/System/FileSystem.cs index 609c3d7e5..428f6a5d7 100644 --- a/src/Microsoft.PowerApps.TestEngine/System/FileSystem.cs +++ b/src/Microsoft.PowerApps.TestEngine/System/FileSystem.cs @@ -1,86 +1,143 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT license. - -using System.Text.RegularExpressions; - -namespace Microsoft.PowerApps.TestEngine.System -{ - /// - /// Wrapper for any System.IO methods needed - /// - public class FileSystem : IFileSystem - { - public void CreateDirectory(string directoryName) - { - Directory.CreateDirectory(directoryName); - } - - public bool Exists(string directoryName) - { - return Directory.Exists(directoryName); - } - - public bool FileExists(string fileName) - { - return File.Exists(fileName); - } - - public string[] GetFiles(string directoryName) - { - return Directory.GetFiles(directoryName); - } - - public void WriteTextToFile(string filePath, string text) - { - if (File.Exists(filePath)) - { - File.AppendAllText(filePath, text); - } - else - { - File.WriteAllText(filePath, text); - } - } - - public void WriteTextToFile(string filePath, string[] text) - { - if (File.Exists(filePath)) - { - File.AppendAllLines(filePath, text); - } - else - { - File.WriteAllLines(filePath, text); - } - } - - public bool IsValidFilePath(string filePath) - { - if (string.IsNullOrEmpty(filePath)) +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +using System.ComponentModel.Composition; +using System.IO; +using System.Text.RegularExpressions; + +namespace Microsoft.PowerApps.TestEngine.System +{ + /// + /// Wrapper for any System.IO methods needed + /// + [Export(typeof(IFileSystem))] + public class FileSystem : IFileSystem + { + public void CreateDirectory(string directoryName) + { + Directory.CreateDirectory(directoryName); + } + + public bool Exists(string directoryName) + { + return Directory.Exists(directoryName); + } + + public bool FileExists(string fileName) + { + return File.Exists(fileName); + } + + public string[] GetFiles(string directoryName) + { + return Directory.GetFiles(directoryName); + } + + public string[] GetFiles(string directoryName, string searchPattern) + { + return Directory.GetFiles(directoryName, searchPattern); + } + + public void WriteTextToFile(string filePath, string text) + { + if (IsWritePermittedFilePath(filePath)) + { + if (File.Exists(filePath)) + { + File.AppendAllText(filePath, text); + } + else + { + File.WriteAllText(filePath, text); + } + } + else + { + throw new InvalidOperationException(string.Format("Write to path: {0} not permitted, ensure path is rooted in {1}.", filePath, GetDefaultRootTestEngine())); + } + } + + public void WriteTextToFile(string filePath, string[] text) + { + if (IsWritePermittedFilePath(filePath)) + { + if (File.Exists(filePath)) + { + File.AppendAllLines(filePath, text); + } + else + { + File.WriteAllLines(filePath, text); + } + } + else { + throw new InvalidOperationException(string.Format("Write to path: {0} not permitted, ensure path is rooted in {1}.", filePath, GetDefaultRootTestEngine())); + } + } + + public bool IsValidFilePath(string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) return false; - } - if (filePath.Length < 3) + filePath = filePath.Trim(); + + try + { + // Get the full normalized path + string fullPath = Path.GetFullPath(filePath); + + //just get this to check if its a valid file path, if its not then it throws + var g = new FileInfo(fullPath).IsReadOnly; + string fileName = Path.GetFileName(filePath); + if (fileName.IndexOfAny(Path.GetInvalidFileNameChars()) >= 0) + return false; + if (string.IsNullOrWhiteSpace(fileName) || fileName.EndsWith(Path.DirectorySeparatorChar.ToString())) + return false; + + return true; + } + catch (Exception) { return false; - } - string invalidPathChars = new string(Path.GetInvalidPathChars()); - Regex invalidPathCharsRegex = new Regex($"[{Regex.Escape($"{invalidPathChars}:?*\"")}]"); - if (invalidPathCharsRegex.IsMatch(filePath.Substring(3, filePath.Length - 3))) - { - return false; - } - return true; + } + } + + public string ReadAllText(string filePath) + { + return File.ReadAllText(filePath); } - public string ReadAllText(string filePath) - { - return File.ReadAllText(filePath); - } - - public string RemoveInvalidFileNameChars(string fileName) - { - return Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c.ToString(), string.Empty)); - } - } -} + public string RemoveInvalidFileNameChars(string fileName) + { + return Path.GetInvalidFileNameChars().Aggregate(fileName, (current, c) => current.Replace(c.ToString(), string.Empty)); + } + + public string GetTempPath() + { + return Path.GetTempPath(); + } + + public string GetDefaultRootTestEngine() + { + return Path.Combine(GetTempPath(), "TestEngine") + Path.DirectorySeparatorChar; + } + + public bool IsWritePermittedFilePath(string filePath) + { + if (string.IsNullOrWhiteSpace(filePath)) + return false; + filePath = filePath.Trim(); + if (IsValidFilePath(filePath) && Path.IsPathRooted(filePath)) + { + var fullPathUri = new Uri(Path.GetFullPath(filePath)); + var baseUri = new Uri(GetDefaultRootTestEngine()); + if (baseUri.IsBaseOf(fullPathUri)) + { + return true; + } + } + return false; + } + } +} diff --git a/src/Microsoft.PowerApps.TestEngine/System/IFileSystem.cs b/src/Microsoft.PowerApps.TestEngine/System/IFileSystem.cs index 9691a7355..dba588eb2 100644 --- a/src/Microsoft.PowerApps.TestEngine/System/IFileSystem.cs +++ b/src/Microsoft.PowerApps.TestEngine/System/IFileSystem.cs @@ -33,6 +33,14 @@ public interface IFileSystem /// Array of files in directory public string[] GetFiles(string directoryName); + /// + /// Gets files in a directory matching search pattern + /// + /// Directory name + /// Directory name + /// Array of files in directory + public string[] GetFiles(string directoryName, string searchPattern); + /// /// Writes text to file /// @@ -67,5 +75,24 @@ public interface IFileSystem /// File name /// File name with all valid characters public string RemoveInvalidFileNameChars(string fileName); + + /// + /// Returns temporary path for local machine + /// + /// Location of the temporary path + public string GetTempPath(); + + /// + /// Returns default root locaiton of all testengine artifacts + /// + /// Location of the root folder for test engine output and log files + public string GetDefaultRootTestEngine(); + + /// + /// Checks whether file path is permitted for write operations + /// + /// Path to check + /// True if it is permitted + public bool IsWritePermittedFilePath(string filePath); } } diff --git a/src/Microsoft.PowerApps.TestEngine/TestEngine.cs b/src/Microsoft.PowerApps.TestEngine/TestEngine.cs index 313be2aa7..843025755 100644 --- a/src/Microsoft.PowerApps.TestEngine/TestEngine.cs +++ b/src/Microsoft.PowerApps.TestEngine/TestEngine.cs @@ -85,14 +85,24 @@ public async Task RunTestAsync(FileInfo testConfigFile, string environme throw new ArgumentNullException(nameof(tenantId)); } + if (string.IsNullOrEmpty(domain)) + { + throw new ArgumentNullException(nameof(domain)); + } + if (outputDirectory == null) { throw new ArgumentNullException(nameof(outputDirectory)); } - - if (string.IsNullOrEmpty(domain)) + else { - throw new ArgumentNullException(nameof(domain)); + if (!new Uri(_fileSystem.GetDefaultRootTestEngine()).IsBaseOf(new Uri(outputDirectory.FullName))) + { + var wrongLocationError = $"Please ensure {nameof(outputDirectory)} is set to a value resolving to a location inside folder {_fileSystem.GetDefaultRootTestEngine()}."; + Logger.LogError(wrongLocationError); + _eventHandler.EncounteredException(new UserInputException(string.Format(" [Critical Error]: {0}", wrongLocationError))); + return "InvalidOutputDirectory"; + } } if (string.IsNullOrEmpty(queryParams)) diff --git a/src/PowerAppsTestEngine/Program.cs b/src/PowerAppsTestEngine/Program.cs index 7425df636..6ce922813 100644 --- a/src/PowerAppsTestEngine/Program.cs +++ b/src/PowerAppsTestEngine/Program.cs @@ -1,8 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT license. -using System.ComponentModel.Composition; -using System.ComponentModel.Composition.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; @@ -213,13 +211,22 @@ DirectoryInfo outputDirectory; const string DefaultOutputDirectory = "TestOutput"; + var _fileSystem = serviceProvider.GetRequiredService(); if (!string.IsNullOrEmpty(inputOptions.OutputDirectory)) { - outputDirectory = new DirectoryInfo(inputOptions.OutputDirectory); + if (Path.IsPathRooted(inputOptions.OutputDirectory.Trim())) + { + Console.WriteLine("[Critical Error]: Please provide a relative path for the output."); + return; + } + else + { + outputDirectory = new DirectoryInfo(Path.Combine(_fileSystem.GetDefaultRootTestEngine(), inputOptions.OutputDirectory.Trim())); + } } else { - outputDirectory = new DirectoryInfo(DefaultOutputDirectory); + outputDirectory = new DirectoryInfo(Path.Combine(_fileSystem.GetDefaultRootTestEngine(), DefaultOutputDirectory.Trim())); } if (!string.IsNullOrEmpty(inputOptions.QueryParams)) diff --git a/src/testengine.auth.certificatestore.tests/CertificateStoreProviderTests.cs b/src/testengine.auth.certificatestore.tests/CertificateStoreProviderTests.cs index 3320d0723..394ccb6f7 100644 --- a/src/testengine.auth.certificatestore.tests/CertificateStoreProviderTests.cs +++ b/src/testengine.auth.certificatestore.tests/CertificateStoreProviderTests.cs @@ -3,6 +3,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; +using Microsoft.PowerApps.TestEngine.System; using Moq; using Xunit; @@ -11,10 +12,12 @@ namespace testengine.auth.certificatestore.tests public class CertificateStoreProviderTests { private readonly CertificateStoreProvider provider; + private Mock MockFileSystem; public CertificateStoreProviderTests() { - provider = new CertificateStoreProvider(); + MockFileSystem = new Mock(MockBehavior.Strict); + provider = new CertificateStoreProvider(MockFileSystem.Object); } [Fact] diff --git a/src/testengine.auth.certificatestore/CertificateStoreProvider.cs b/src/testengine.auth.certificatestore/CertificateStoreProvider.cs index f7308a036..662dc5145 100644 --- a/src/testengine.auth.certificatestore/CertificateStoreProvider.cs +++ b/src/testengine.auth.certificatestore/CertificateStoreProvider.cs @@ -7,6 +7,7 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.System; namespace testengine.auth { @@ -20,7 +21,8 @@ public class CertificateStoreProvider : IUserCertificateProvider public string Name { get { return "certstore"; } } - public CertificateStoreProvider() + [ImportingConstructor] + public CertificateStoreProvider(IFileSystem fileSystem) { } @@ -39,7 +41,7 @@ public CertificateStoreProvider() { foreach (X509Certificate2 certificate in store.Certificates) { - if (certificate.SubjectName.Name != null && certificate.SubjectName.Name.Contains(userIdentifier, StringComparison.OrdinalIgnoreCase)) + if (certificate.SubjectName.Name != null && certificate.SubjectName.Name.Equals(userIdentifier, StringComparison.OrdinalIgnoreCase)) { return certificate; } diff --git a/src/testengine.auth.localcertificate.tests/LocalUserCertificateProviderTests.cs b/src/testengine.auth.localcertificate.tests/LocalUserCertificateProviderTests.cs index e0584f612..44216e9c2 100644 --- a/src/testengine.auth.localcertificate.tests/LocalUserCertificateProviderTests.cs +++ b/src/testengine.auth.localcertificate.tests/LocalUserCertificateProviderTests.cs @@ -1,16 +1,30 @@ using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; +using Microsoft.Playwright; +using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.System; +using Microsoft.PowerApps.TestEngine.TestInfra; using Moq; namespace testengine.auth.tests { public class LocalUserCertificateProviderTests { + private Mock MockFileSystem; + + public LocalUserCertificateProviderTests() + { + MockFileSystem = new Mock(MockBehavior.Strict); + } + [Fact] public void NameProperty_ShouldReturnLocalCert() { // Arrange - var provider = new LocalUserCertificateProvider(); + MockFileSystem.Setup(x => x.GetTempPath()).Returns(""); + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns(""); + MockFileSystem.Setup(x => x.Exists(It.IsAny())).Returns(false); + var provider = new LocalUserCertificateProvider(MockFileSystem.Object); // Act var name = provider.Name; @@ -23,33 +37,50 @@ public void NameProperty_ShouldReturnLocalCert() public void Constructor_ShouldLoadCertificatesFromDirectory() { // Arrange - var certDir = "LocalCertificates"; - Directory.CreateDirectory(certDir); - var pfxFilePath = Path.Combine(certDir, "testcert.pfx"); - - // Create a test certificate - using (var rsa = RSA.Create(2048)) + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns(""); + var certDir = Path.Combine(MockFileSystem.Object.GetDefaultRootTestEngine(), "LocalCertificates"); + MockFileSystem.Setup(x => x.Exists(certDir)).Returns(true); + try { - var request = new CertificateRequest($"CN=testcert", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); - var certificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); - File.WriteAllBytes(pfxFilePath, certificate.Export(X509ContentType.Pfx)); - } + Directory.CreateDirectory(certDir); + var pfxFilePath = Path.Combine(certDir, "testcert.pfx"); - // Act - var provider = new LocalUserCertificateProvider(); + // Create a test certificate + using (var rsa = RSA.Create(2048)) + { + var request = new CertificateRequest($"CN=testcert", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1); + var certificate = request.CreateSelfSigned(DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1)); + File.WriteAllBytes(pfxFilePath, certificate.Export(X509ContentType.Pfx)); + } + MockFileSystem.Setup(x => x.GetFiles(certDir, "*.pfx")).Returns(new string[] {pfxFilePath}); - // Assert - Assert.NotNull(provider.RetrieveCertificateForUser("CN=testcert")); + // Act + var provider = new LocalUserCertificateProvider(MockFileSystem.Object); + + // Assert + Assert.NotNull(provider.RetrieveCertificateForUser("CN=testcert")); - // Cleanup - Directory.Delete(certDir, true); + // Cleanup + Directory.Delete(certDir, true); + } + catch + { + if (Directory.Exists(certDir)) + { + Directory.Delete(certDir, true); + } + } } [Fact] public void RetrieveCertificateForUser_ShouldReturnNullForNonExistingUser() { + MockFileSystem.Setup(x => x.GetTempPath()).Returns(""); + MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns(""); + MockFileSystem.Setup(x => x.Exists(It.IsAny())).Returns(true); + MockFileSystem.Setup(x => x.GetFiles(It.IsAny(), It.IsAny())).Returns(new string[] { }); // Arrange - var provider = new LocalUserCertificateProvider(); + var provider = new LocalUserCertificateProvider(MockFileSystem.Object); // Act var cert = provider.RetrieveCertificateForUser("nonexistinguser"); diff --git a/src/testengine.auth.localcertificate/LocalUserCertificateProvider.cs b/src/testengine.auth.localcertificate/LocalUserCertificateProvider.cs index 393176566..d7b3ada5f 100644 --- a/src/testengine.auth.localcertificate/LocalUserCertificateProvider.cs +++ b/src/testengine.auth.localcertificate/LocalUserCertificateProvider.cs @@ -4,6 +4,7 @@ using System.ComponentModel.Composition; using System.Security.Cryptography.X509Certificates; using Microsoft.PowerApps.TestEngine.Config; +using Microsoft.PowerApps.TestEngine.System; namespace testengine.auth { @@ -15,16 +16,19 @@ public class LocalUserCertificateProvider : IUserCertificateProvider { public string Name { get { return "localcert"; } } + private readonly IFileSystem _fileSystem; + private Dictionary emailCertificateDict = new Dictionary(); - public LocalUserCertificateProvider() + [ImportingConstructor] + public LocalUserCertificateProvider(IFileSystem fileSystem) { - var certDir = "LocalCertificates"; + _fileSystem = fileSystem; + var certDir = Path.Combine(_fileSystem.GetDefaultRootTestEngine(), "LocalCertificates"); var password = ""; - if (Directory.Exists(certDir)) + if (_fileSystem.Exists(certDir)) { - string[] pfxFiles = Directory.GetFiles(certDir, "*.pfx"); - + string[] pfxFiles = _fileSystem.GetFiles(certDir, "*.pfx"); foreach (var pfxFile in pfxFiles) { // Load the certificate diff --git a/src/testengine.module.mda/testengine.module.mda.csproj b/src/testengine.module.mda/testengine.module.mda.csproj index 7ddcf0db6..28a46c922 100644 --- a/src/testengine.module.mda/testengine.module.mda.csproj +++ b/src/testengine.module.mda/testengine.module.mda.csproj @@ -28,8 +28,8 @@ - - + + diff --git a/src/testengine.module.pause/testengine.module.pause.csproj b/src/testengine.module.pause/testengine.module.pause.csproj index 434c29cfd..bb2608e39 100644 --- a/src/testengine.module.pause/testengine.module.pause.csproj +++ b/src/testengine.module.pause/testengine.module.pause.csproj @@ -29,7 +29,7 @@ - + diff --git a/src/testengine.user.browser.tests/testengine.user.browser.tests.csproj b/src/testengine.user.browser.tests/testengine.user.browser.tests.csproj index 9f9332139..8e9127a87 100644 --- a/src/testengine.user.browser.tests/testengine.user.browser.tests.csproj +++ b/src/testengine.user.browser.tests/testengine.user.browser.tests.csproj @@ -31,9 +31,9 @@ - + - + diff --git a/src/testengine.user.browser/testengine.user.browser.csproj b/src/testengine.user.browser/testengine.user.browser.csproj index 0d3ab8d08..4bebe293d 100644 --- a/src/testengine.user.browser/testengine.user.browser.csproj +++ b/src/testengine.user.browser/testengine.user.browser.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/testengine.user.certificate.tests/testengine.user.certificate.tests.csproj b/src/testengine.user.certificate.tests/testengine.user.certificate.tests.csproj index 4f783fb2c..37dc6db8d 100644 --- a/src/testengine.user.certificate.tests/testengine.user.certificate.tests.csproj +++ b/src/testengine.user.certificate.tests/testengine.user.certificate.tests.csproj @@ -31,10 +31,10 @@ - + - + \ No newline at end of file diff --git a/src/testengine.user.certificate/CertificateUserManagerModule.cs b/src/testengine.user.certificate/CertificateUserManagerModule.cs index 09677b157..dabdeb00a 100644 --- a/src/testengine.user.certificate/CertificateUserManagerModule.cs +++ b/src/testengine.user.certificate/CertificateUserManagerModule.cs @@ -32,7 +32,7 @@ public class CertificateUserManagerModule : IUserManager public bool UseStaticContext { get { return false; } } - public string Location { get; set; } = string.Empty; + public string Location { get; set; } = "CertificateContext"; public IPage? Page { get; set; } @@ -263,7 +263,6 @@ internal async Task HandleRequest(IRoute route, X509Certificate2 cert, ILogger l { var request = route.Request; - Console.WriteLine($"Intercepted request: {request.Method} {request.Url}"); if (request.Method == "POST") { try @@ -279,7 +278,7 @@ internal async Task HandleRequest(IRoute route, X509Certificate2 cert, ILogger l await route.FulfillAsync(new RouteFulfillOptions { - ContentType = "text/html", + ContentType = "text/html; charset=utf-8", Status = (int)response.StatusCode, Headers = headers, Body = await response.Content.ReadAsStringAsync() diff --git a/src/testengine.user.certificate/testengine.user.certificate.csproj b/src/testengine.user.certificate/testengine.user.certificate.csproj index 07fbe6fef..8619ed3ff 100644 --- a/src/testengine.user.certificate/testengine.user.certificate.csproj +++ b/src/testengine.user.certificate/testengine.user.certificate.csproj @@ -16,7 +16,7 @@ - + diff --git a/src/testengine.user.environment.tests/testengine.user.environment.tests.csproj b/src/testengine.user.environment.tests/testengine.user.environment.tests.csproj index 31f39de44..bc2523633 100644 --- a/src/testengine.user.environment.tests/testengine.user.environment.tests.csproj +++ b/src/testengine.user.environment.tests/testengine.user.environment.tests.csproj @@ -31,9 +31,9 @@ - + - + diff --git a/src/testengine.user.environment/testengine.user.environment.csproj b/src/testengine.user.environment/testengine.user.environment.csproj index 79bb90b59..482274d89 100644 --- a/src/testengine.user.environment/testengine.user.environment.csproj +++ b/src/testengine.user.environment/testengine.user.environment.csproj @@ -16,7 +16,7 @@ - +