From 5e76a82a219511ca5b54c9b7c62bb9217329458c Mon Sep 17 00:00:00 2001 From: Noah Lerner Date: Sun, 2 Feb 2020 14:49:34 +0200 Subject: [PATCH] Improved relative path checking based on file existence (#411) * Improved relative path checking based on file existence If the file exists at the relative path, then use it. If not, then use the path as is. * Apply File.Exists logic to ReadResponseBodyAsString as well * Make path handling more robust since path is user defined * Unit tests for relative path feature * Replace all back and forward slashes with system dependent DirectorySeparatorChar * Attempt fix broken directory separator chars for Unix platforms * Revert wrapping GetMappingFolder with CleanPath * Move CleanPath logic to its own class * Remove whitespace * Remove more whitespace * Improve CleanPath method * Move PathUtils tests to separate class Add another test to ResponseWithBodyFromFileTests * Fix Response_ProvideResponse_WithBodyFromFile_InAdminMappingFolder * Debug Linux CI build * Debug Linux CI * print all files from admin mappings folder * Debug CleanPath * Fix removed leading directory separator char in Linux breaks file path Remove debugging statements * Move combine to PathUtils * PathUtils + PathUtilsTests * Remove replicated (3x) tests throughout ResponseWithBodyFromFileTests Co-authored-by: Stef Heyenrath --- .../Handlers/LocalFileSystemHandler.cs | 11 +-- src/WireMock.Net/Util/PathUtils.cs | 36 ++++++++++ .../ResponseWithBodyFromFileTests.cs | 71 ++++++++++++++++--- .../WireMock.Net.Tests/Util/PathUtilsTests.cs | 44 ++++++++++++ .../WireMock.Net.Tests.csproj | 3 + .../mappings/subdirectory/MyXmlResponse.xml | 3 + 6 files changed, 155 insertions(+), 13 deletions(-) create mode 100644 src/WireMock.Net/Util/PathUtils.cs create mode 100644 test/WireMock.Net.Tests/Util/PathUtilsTests.cs create mode 100644 test/WireMock.Net.Tests/__admin/mappings/subdirectory/MyXmlResponse.xml diff --git a/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs b/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs index 57bedc64..2dd7e5e0 100644 --- a/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs +++ b/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.IO; +using WireMock.Util; using WireMock.Validation; namespace WireMock.Handlers @@ -80,20 +81,20 @@ namespace WireMock.Handlers public byte[] ReadResponseBodyAsFile(string path) { Check.NotNullOrEmpty(path, nameof(path)); - - // In case the path is a filename, the path will be adjusted to the MappingFolder. + path = PathUtils.CleanPath(path); + // If the file exists at the given path relative to the MappingsFolder, then return that. // Else the path will just be as-is. - return File.ReadAllBytes(Path.GetFileName(path) == path ? Path.Combine(GetMappingFolder(), path) : path); + return File.ReadAllBytes(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path); } /// public string ReadResponseBodyAsString(string path) { Check.NotNullOrEmpty(path, nameof(path)); - + path = PathUtils.CleanPath(path); // In case the path is a filename, the path will be adjusted to the MappingFolder. // Else the path will just be as-is. - return File.ReadAllText(Path.GetFileName(path) == path ? Path.Combine(GetMappingFolder(), path) : path); + return File.ReadAllText(File.Exists(PathUtils.Combine(GetMappingFolder(), path)) ? PathUtils.Combine(GetMappingFolder(), path) : path); } /// diff --git a/src/WireMock.Net/Util/PathUtils.cs b/src/WireMock.Net/Util/PathUtils.cs new file mode 100644 index 00000000..951c9d47 --- /dev/null +++ b/src/WireMock.Net/Util/PathUtils.cs @@ -0,0 +1,36 @@ +using System.IO; + +namespace WireMock.Util +{ + internal static class PathUtils + { + /// + /// Robust handling of the user defined path. + /// Also supports Unix and Windows platforms + /// + /// The path to clean + public static string CleanPath(string path) + { + return path?.Replace('/', Path.DirectorySeparatorChar).Replace('\\', Path.DirectorySeparatorChar); + } + + /// + /// Removes leading directory separator chars from the filepath, which could break Path.Combine + /// + /// The path to remove the loading DirectorySeparatorChars + public static string RemoveLeadingDirectorySeparators(string path) + { + return path?.TrimStart(new[] { Path.DirectorySeparatorChar }); + } + + /// + /// Combine two paths + /// + /// The root path + /// The path + public static string Combine(string root, string path) + { + return Path.Combine(root, RemoveLeadingDirectorySeparators(path)); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithBodyFromFileTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithBodyFromFileTests.cs index 56eaefb3..b841fda3 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithBodyFromFileTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithBodyFromFileTests.cs @@ -1,4 +1,6 @@ using FluentAssertions; +using System; +using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; @@ -13,11 +15,10 @@ namespace WireMock.Net.Tests.ResponseBuilders public class ResponseWithBodyFromFileTests { [Fact] - public async Task Response_ProvideResponse_WithBodyFromFile() + public async Task Response_ProvideResponse_WithBodyFromFile_AbsolutePath() { // Arrange var server = WireMockServer.Start(); - string path = Path.Combine(Directory.GetCurrentDirectory(), "__admin", "mappings", "MyXmlResponse.xml"); server @@ -36,14 +37,68 @@ namespace WireMock.Net.Tests.ResponseBuilders ); // Act - var response1 = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + "/v1/content"); - var response2 = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + "/v1/content"); - var response3 = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + "/v1/content"); + var response = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + "/v1/content"); // Assert - response1.Should().Contain("world"); - response2.Should().Contain("world"); - response3.Should().Contain("world"); + response.Should().Contain("world"); + } + + [Fact] + public async Task Response_ProvideResponse_WithBodyFromFile_InSubDirectory() + { + // Arrange + var server = WireMockServer.Start(); + string path = @"subdirectory/MyXmlResponse.xml"; + + server + .Given( + Request + .Create() + .UsingGet() + .WithPath("/v1/content") + ) + .RespondWith( + Response + .Create() + .WithStatusCode(HttpStatusCode.OK) + .WithHeader("Content-Type", "application/xml") + .WithBodyFromFile(path) + ); + + // Act + var response = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + "/v1/content"); + + // Assert + response.Should().Contain("world"); + } + + [Fact] + public async Task Response_ProvideResponse_WithBodyFromFile_InAdminMappingFolder() + { + // Arrange + var server = WireMockServer.Start(); + string path = @"MyXmlResponse.xml"; + + server + .Given( + Request + .Create() + .UsingGet() + .WithPath("/v1/content") + ) + .RespondWith( + Response + .Create() + .WithStatusCode(HttpStatusCode.OK) + .WithHeader("Content-Type", "application/xml") + .WithBodyFromFile(path) + ); + + // Act + var response = await new HttpClient().GetStringAsync("http://localhost:" + server.Ports[0] + "/v1/content"); + + // Assert + response.Should().Contain("world"); } } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Util/PathUtilsTests.cs b/test/WireMock.Net.Tests/Util/PathUtilsTests.cs new file mode 100644 index 00000000..fe33da8d --- /dev/null +++ b/test/WireMock.Net.Tests/Util/PathUtilsTests.cs @@ -0,0 +1,44 @@ +using NFluent; +using System.IO; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.Util +{ + public class PathUtilsTests + { + [Theory] + [InlineData(@"subdirectory/MyXmlResponse.xml")] + [InlineData(@"subdirectory\MyXmlResponse.xml")] + public void PathUtils_CleanPath(string path) + { + // Act + var cleanPath = PathUtils.CleanPath(path); + + // Assert + Check.That(cleanPath).Equals("subdirectory" + Path.DirectorySeparatorChar + "MyXmlResponse.xml"); + } + + [Theory] + [InlineData(null, null)] + [InlineData("", "")] + [InlineData("a", "a")] + [InlineData(@"/", "")] + [InlineData(@"//", "")] + [InlineData(@"//a", "a")] + [InlineData(@"\", "")] + [InlineData(@"\\", "")] + [InlineData(@"\\a", "a")] + public void PathUtils_CleanPath_RemoveLeadingDirectorySeparators(string path, string expected) + { + // Arrange + var cleanPath = PathUtils.CleanPath(path); + + // Act + var withoutDirectorySeparators = PathUtils.RemoveLeadingDirectorySeparators(cleanPath); + + // Assert + Check.That(withoutDirectorySeparators).Equals(expected); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj index 68ec5c76..4f341c22 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -83,6 +83,9 @@ PreserveNewest + + PreserveNewest + diff --git a/test/WireMock.Net.Tests/__admin/mappings/subdirectory/MyXmlResponse.xml b/test/WireMock.Net.Tests/__admin/mappings/subdirectory/MyXmlResponse.xml new file mode 100644 index 00000000..24fd28d3 --- /dev/null +++ b/test/WireMock.Net.Tests/__admin/mappings/subdirectory/MyXmlResponse.xml @@ -0,0 +1,3 @@ + +world + \ No newline at end of file