Skip to content
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

restricting logs, output to appdata local directory #488

Merged
merged 11 commits into from
Dec 3, 2024
2 changes: 1 addition & 1 deletion docs/CommandInput.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `Microsoft\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'. |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ public ScreenshotFunctionTests()
public void ScreenshotFunctionThrowsOnInvalidResultDirectoryTest()
{
MockSingleTestInstanceState.Setup(x => x.GetTestResultsDirectory()).Returns("");
MockFileSystem.Setup(x => x.IsValidFilePath(It.IsAny<string>())).Returns(false);
MockFileSystem.Setup(x => x.IsNonUNCDirectoryPath(It.IsAny<string>())).Returns(false);
LoggingTestHelper.SetupMock(MockLogger);
var screenshotFunction = new ScreenshotFunction(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLogger.Object);
Assert.Throws<InvalidOperationException>(() => screenshotFunction.Execute(FormulaValue.New("screenshot.png")));
Expand All @@ -50,7 +50,7 @@ public void ScreenshotFunctionThrowsOnInvalidScreenshotNameTest(string screensho
{
var testResultDirectory = "C:\\testResults";
MockSingleTestInstanceState.Setup(x => x.GetTestResultsDirectory()).Returns(testResultDirectory);
MockFileSystem.Setup(x => x.IsValidFilePath(It.IsAny<string>())).Returns(true);
MockFileSystem.Setup(x => x.IsNonUNCDirectoryPath(It.IsAny<string>())).Returns(true);
LoggingTestHelper.SetupMock(MockLogger);
var screenshotFunction = new ScreenshotFunction(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLogger.Object);
Assert.Throws<ArgumentException>(() => screenshotFunction.Execute(FormulaValue.New(screenshotName)));
Expand All @@ -62,7 +62,7 @@ public void ScreenshotFunctionThrowsOnNonRelativeFilePathTest()
{
var testResultDirectory = "C:\\testResults";
MockSingleTestInstanceState.Setup(x => x.GetTestResultsDirectory()).Returns(testResultDirectory);
MockFileSystem.Setup(x => x.IsValidFilePath(It.IsAny<string>())).Returns(true);
MockFileSystem.Setup(x => x.IsNonUNCDirectoryPath(It.IsAny<string>())).Returns(true);
LoggingTestHelper.SetupMock(MockLogger);
var screenshotFunction = new ScreenshotFunction(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLogger.Object);
Assert.Throws<ArgumentException>(() => screenshotFunction.Execute(FormulaValue.New(Path.Combine(Path.GetFullPath(Directory.GetCurrentDirectory()), "screeshot.jpg"))));
Expand All @@ -76,14 +76,14 @@ public void ScreenshotFunctionTest(string screenshotName)
{
var testResultDirectory = "C:\\testResults";
MockSingleTestInstanceState.Setup(x => x.GetTestResultsDirectory()).Returns(testResultDirectory);
MockFileSystem.Setup(x => x.IsValidFilePath(It.IsAny<string>())).Returns(true);
MockFileSystem.Setup(x => x.IsNonUNCDirectoryPath(It.IsAny<string>())).Returns(true);
MockTestInfraFunctions.Setup(x => x.ScreenshotAsync(It.IsAny<string>())).Returns(Task.CompletedTask);
LoggingTestHelper.SetupMock(MockLogger);
var screenshotFunction = new ScreenshotFunction(MockTestInfraFunctions.Object, MockSingleTestInstanceState.Object, MockFileSystem.Object, MockLogger.Object);
screenshotFunction.Execute(FormulaValue.New(screenshotName));

MockSingleTestInstanceState.Verify(x => x.GetTestResultsDirectory(), Times.Once());
MockFileSystem.Verify(x => x.IsValidFilePath(testResultDirectory), Times.Once());
MockFileSystem.Verify(x => x.IsNonUNCDirectoryPath(testResultDirectory), Times.Once());
MockTestInfraFunctions.Verify(x => x.ScreenshotAsync(Path.Combine(testResultDirectory, screenshotName)), Times.Once());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ public async Task ExecuteScreenshotFunctionTest()
MockTestState.Setup(x => x.OnAfterTestStepExecuted(It.IsAny<TestStepEventArgs>()));

MockSingleTestInstanceState.Setup(x => x.GetTestResultsDirectory()).Returns("C:\\testResults");
MockFileSystem.Setup(x => x.IsValidFilePath(It.IsAny<string>())).Returns(true);
MockFileSystem.Setup(x => x.IsNonUNCDirectoryPath(It.IsAny<string>())).Returns(true);
MockTestInfraFunctions.Setup(x => x.ScreenshotAsync(It.IsAny<string>())).Returns(Task.CompletedTask);
var powerFxExpression = "Screenshot(\"1.jpg\")";
var powerFxEngine = new PowerFxEngine(MockTestInfraFunctions.Object, MockTestWebProvider.Object, MockSingleTestInstanceState.Object, MockTestState.Object, MockFileSystem.Object);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public void WriteToLogsFileThrowsOnInvalidPathTest()
MockFileSystem.Setup(x => x.Exists(It.IsAny<string>())).Returns(false);
MockFileSystem.Setup(x => x.IsValidFilePath(It.IsAny<string>())).Returns(false);
MockFileSystem.Setup(x => x.CreateDirectory(It.IsAny<string>()));
MockFileSystem.Setup(x => x.GetDefaultRootTestEngine()).Returns("");
MockFileSystem.Setup(x => x.WriteTextToFile(It.IsAny<string>(), It.IsAny<string[]>())).Callback((string filePath, string[] logs) =>
{
createdLogs.Add(filePath, logs);
Expand Down
212 changes: 207 additions & 5 deletions src/Microsoft.PowerApps.TestEngine.Tests/System/FileSystemTests.cs
snamilikonda marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -1,13 +1,26 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
using Microsoft.PowerApps.TestEngine.System;
using Moq;
using Xunit;

namespace Microsoft.PowerApps.TestEngine.Tests.System
{
public class FileSystemTests
{
private FileSystem fileSystem;
public FileSystemTests()
{
fileSystem = new FileSystem();
}

[Theory]
[InlineData("file.txt", true)]
[InlineData("C:/folder/file.txt", true)]
Expand All @@ -16,11 +29,39 @@ public class FileSystemTests
[InlineData("", false)]
[InlineData(null, false)]
[InlineData("C:/fold:er", false)]
public void IsValidFilePathTest(string? filePath, bool expectedResult)
[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)]
[InlineData("\\\\RandomUNC", false)]
[InlineData(@"\\?\C:\folder", true)]
[InlineData(@"C:\CON\a.txt", false)]
[InlineData(@"C:\a\a.txt.", false)]
[InlineData(@"C:\CON", false)]
[InlineData(@"C:\folder\AUX", false)]
[InlineData(@"C:\folder\PRN.txt", false)]
[InlineData(@"C:\\WINDOWS\\system32", false)]
public void IsValidFilePathTest_Windows(string? filePath, bool expectedResult)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
var result = fileSystem.IsValidFilePath(filePath);
Assert.Equal(expectedResult, result);
}
}

[Theory]
[InlineData(@"/usr/local/abc.txt", true)]
[InlineData(@"/root/", false)]
public void IsValidFilePathTest_Linux(string? filePath, bool expectedResult)
{
var fileSystem = new FileSystem();
var result = fileSystem.IsValidFilePath(filePath);
Assert.Equal(expectedResult, result);
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
var result = fileSystem.IsValidFilePath(filePath);
Assert.Equal(expectedResult, result);
}
}

// Some inline data is commented because invalid characters can vary by file system.
Expand All @@ -33,9 +74,170 @@ public void IsValidFilePathTest(string? filePath, bool expectedResult)
// [InlineData("tem|<p", "temp")]
public void RemoveInvalidFileNameCharsTest(string inputFileName, string expectedFileName)
{
var fileSystem = new FileSystem();
var result = fileSystem.RemoveInvalidFileNameChars(inputFileName);
Assert.Equal(expectedFileName, result);
}

[Fact]
public void IsWritePermittedFilePath_ValidRootedPath_ReturnsTrue()
{
var validPath = Path.Combine(fileSystem.GetDefaultRootTestEngine(), "testfile.txt");
Assert.True(fileSystem.IsWritePermittedFilePath(validPath));
}

[Fact]
public void IsWritePermittedFilePath_SameAsRootedPath_ReturnsFalse()
{
var validPath = Path.Combine(fileSystem.GetDefaultRootTestEngine(), "");
Assert.False(fileSystem.IsWritePermittedFilePath(validPath));
}

[Fact]
public void IsWritePermittedFilePath_RelativePath_ReturnsFalse()
{
var relativePath = @"..\testfile.txt";
Assert.False(fileSystem.IsWritePermittedFilePath(relativePath));
}

[Fact]
public void IsWritePermittedFilePath_InvalidRootedPath_ReturnsFalse()
{
var invalidPath = Path.Combine(fileSystem.GetTempPath(), "invalidfolder", "testfile.txt");
Assert.False(fileSystem.IsWritePermittedFilePath(invalidPath));
}

[Fact]
public void IsWritePermittedFilePath_NullPath_ReturnsFalse()
{
Assert.False(fileSystem.IsWritePermittedFilePath(null));
}

[Fact]
public void IsWritePermittedFilePath_ValidPathWithParentDirectoryTraversal_ReturnsFalse()
{
var pathWithParentTraversal = fileSystem.GetDefaultRootTestEngine() + @"..\testfile.txt";
Assert.False(fileSystem.IsWritePermittedFilePath(pathWithParentTraversal));
}

[Fact]
public void IsWritePermittedFilePath_UNCPath_ReturnsFalse()
{
var validPath = "\\\\RandomUNC";
Assert.False(fileSystem.IsWritePermittedFilePath(validPath));
}

[Fact]
public void WriteTextToFile_UnpermittedFilePath_ThrowsInvalidOperationException()
{
var invalidFilePath = fileSystem.GetDefaultRootTestEngine() + @"..\testfile.txt";
var exception = Assert.Throws<InvalidOperationException>(() => fileSystem.WriteTextToFile(invalidFilePath, ""));
Assert.Contains(invalidFilePath, exception.Message);
}

[Fact]
public void WriteTextToFile_ArrayText_UnpermittedFilePath_ThrowsInvalidOperationException()
{
var invalidFilePath = fileSystem.GetDefaultRootTestEngine() + @"..\testfile.txt";
var exception = Assert.Throws<InvalidOperationException>(() => fileSystem.WriteTextToFile(invalidFilePath, new string[] { "This should fail." }));
Assert.Contains(invalidFilePath, exception.Message);
}

[Fact]
public void WriteFile_ArrayText_UnpermittedFilePath_ThrowsInvalidOperationException()
{
var invalidFilePath = fileSystem.GetDefaultRootTestEngine() + @"..\testfile.txt";
var exception = Assert.Throws<InvalidOperationException>(() => fileSystem.WriteFile(invalidFilePath, Encoding.UTF8.GetBytes("This should fail.")));
Assert.Contains(invalidFilePath, exception.Message);
}

[Theory]
[MemberData(nameof(DirectoryPathTestDataWindows))]
public void IsNonUNCDirectoryPath_Windows_ReturnsValidity(string path, bool validity)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Assert.Equal(fileSystem.IsNonUNCDirectoryPath(path), validity);
}
}

[Theory]
[MemberData(nameof(DirectoryPathTestDataLinux))]
public void IsNonUNCDirectoryPath_Linux_ReturnsValidity(string path, bool validity)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Assert.Equal(fileSystem.IsNonUNCDirectoryPath(path), validity);
}
}

public static IEnumerable<object[]> DirectoryPathTestDataWindows()
{
return new List<object[]>
{
new object[] { @"C:\Valid\Directory", true }, // Valid absolute Windows path
new object[] { @"relative\directory", true }, // Valid relative Windows path
new object[] { @"\\network\share\directory", false }, // UNC path (network)
new object[] { @"C:\ ", false }, // Ends with a space
new object[] { @"C:\.", false }, // Ends with a period
new object[] { @"", false }, // Empty string
new object[] { @" ", false }, // Whitespace string
new object[] { @"C:\Valid\..\Directory", true }, // Valid path with `..` (resolved)
new object[] { @"\\?\C:\Very\Long\Path", true }, // Long path prefix
new object[] { @"C:\folder\" + new string('a', 250), true }, // Valid length
new object[] { @"C:\フォルダー", true }, // Valid Unicode path
new object[] { @"file:///C:/Valid/Directory", true }, // Valid Unicode path
};
}

public static IEnumerable<object[]> DirectoryPathTestDataLinux()
{
return new List<object[]>
{
new object[] { @"/valid/directory", true }, // Valid absolute Linux path
new object[] { @"./relative/directory", true }, // Valid relative Linux path
new object[] { @"\\network\share\directory", false }, // UNC path (network)
new object[] { @"", false }, // Empty string
new object[] { @" ", false }, // Whitespace string
new object[] { @"../relative/dir", true }, // relative Linux path
};
}

[Theory]
// Valid cases (no reserved names)
[InlineData(@"C:\folder\my.file", false)]
[InlineData(@"C:\folder\my.context", false)]
[InlineData(@"C:\folder\subfolder\file.txt", false)]
[InlineData(@"C:\myfolder\subfolder.ext", false)]
[InlineData(@"C:\myfolderCON\subfolder.ext", false)]
[InlineData(@"C:\myfolder CON\subfolder.ext", false)]
[InlineData(@"C:\folder\file.com", false)] // Extension should not match reserved names

// Invalid cases (reserved names in path)
[InlineData(@"C:\CON", true)] // Reserved root folder
[InlineData(@"C:\folder\AUX", true)] // Reserved folder
[InlineData(@"C:\folder\PRN.txt", true)] // Reserved file name (extension ignored)
[InlineData(@"C:\folder\COM1", true)] // Reserved COM name
[InlineData(@"C:\LPT2\file.txt", true)] // Reserved folder in path
[InlineData(@"C:\CLOCK$\file.txt", true)] // Reserved CLOCK$ folder
[InlineData(@"C:\folder\subfolder\NUL", true)] // Reserved name deep in path
[InlineData(@"C:\myfolder\COM9.file", true)] // File with reserved name

public void WindowsReservedFolderNamesExistInFilePath_ReturnsValidity(string fileFullPath, bool reservedExists)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Assert.Equal(fileSystem.WindowsReservedFolderNamesExistInFilePath(fileFullPath), reservedExists);
}
}

[Theory]
[InlineData(@"/usr/local/bin", false)]
public void WindowsReservedFolderNamesExistInFilePath_OSLinx_ReturnsValidity(string fileFullPath, bool reservedExists)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Assert.Equal(fileSystem.WindowsReservedFolderNamesExistInFilePath(fileFullPath), reservedExists);
}
}
}
}
Loading
Loading