diff --git a/Directory.Build.props b/Directory.Build.props index 08a194ea..25a48291 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -4,7 +4,7 @@ - 1.0.16 + 1.0.17 diff --git a/GitHubReleaseNotes.txt b/GitHubReleaseNotes.txt index 9d19bc80..c48b3317 100644 --- a/GitHubReleaseNotes.txt +++ b/GitHubReleaseNotes.txt @@ -1,3 +1,3 @@ https://github.com/StefH/GitHubReleaseNotes -GitHubReleaseNotes.exe --output CHANGELOG.md --skip-empty-releases --version 1.0.16.0 \ No newline at end of file +GitHubReleaseNotes.exe --output CHANGELOG.md --skip-empty-releases --version 1.0.17.0 \ No newline at end of file diff --git a/examples/WireMock.Net.Console.Net452.Classic/CustomFileSystemFileHandler.cs b/examples/WireMock.Net.Console.Net452.Classic/CustomFileSystemFileHandler.cs index fcad7619..699ec4b4 100644 --- a/examples/WireMock.Net.Console.Net452.Classic/CustomFileSystemFileHandler.cs +++ b/examples/WireMock.Net.Console.Net452.Classic/CustomFileSystemFileHandler.cs @@ -50,6 +50,12 @@ namespace WireMock.Net.ConsoleApplication return File.ReadAllBytes(Path.GetFileName(path) == path ? Path.Combine(GetMappingFolder(), path) : path); } + /// + public string ReadResponseBodyAsString(string path) + { + return File.ReadAllText(Path.GetFileName(path) == path ? Path.Combine(GetMappingFolder(), path) : path); + } + /// public bool FileExists(string path) { diff --git a/src/WireMock.Net/Handlers/IFileSystemHandler.cs b/src/WireMock.Net/Handlers/IFileSystemHandler.cs index 5ef5ef5c..d01e0469 100644 --- a/src/WireMock.Net/Handlers/IFileSystemHandler.cs +++ b/src/WireMock.Net/Handlers/IFileSystemHandler.cs @@ -49,12 +49,19 @@ namespace WireMock.Handlers void WriteMappingFile([NotNull] string path, [NotNull] string text); /// - /// Read a response body file as text. + /// Read a response body file as byte[]. /// /// The path or filename from the file to read. /// The file content as bytes. byte[] ReadResponseBodyAsFile([NotNull] string path); + /// + /// Read a response body file as text. + /// + /// The path or filename from the file to read. + /// The file content as text. + string ReadResponseBodyAsString([NotNull] string path); + /// /// Delete a file. /// diff --git a/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs b/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs index d70bdc53..eeb957e8 100644 --- a/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs +++ b/src/WireMock.Net/Handlers/LocalFileSystemHandler.cs @@ -68,6 +68,16 @@ namespace WireMock.Handlers return File.ReadAllBytes(Path.GetFileName(path) == path ? Path.Combine(GetMappingFolder(), path) : path); } + /// + public string ReadResponseBodyAsString(string path) + { + Check.NotNullOrEmpty(path, nameof(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); + } + /// public bool FileExists(string filename) { diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 82ba0852..6e2fd7cf 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -21,7 +21,8 @@ namespace WireMock.ResponseBuilders /// public class Response : IResponseBuilder { - private readonly IFileSystemHandler _fileSystemHandler = new LocalFileSystemHandler(); + private readonly IFileSystemHandler _fileSystemHandler; + private readonly ResponseMessageTransformer _responseMessageTransformer; private HttpClient _httpClientForProxy; /// @@ -93,6 +94,9 @@ namespace WireMock.ResponseBuilders private Response(ResponseMessage responseMessage) { ResponseMessage = responseMessage; + + _fileSystemHandler = new LocalFileSystemHandler(); + _responseMessageTransformer = new ResponseMessageTransformer(_fileSystemHandler); } /// @@ -417,7 +421,7 @@ namespace WireMock.ResponseBuilders if (UseTransformer) { - return ResponseMessageTransformer.Transform(requestMessage, ResponseMessage); + return _responseMessageTransformer.Transform(requestMessage, ResponseMessage); } // Just return normal defined ResponseMessage diff --git a/src/WireMock.Net/Transformers/HandleBarsFile.cs b/src/WireMock.Net/Transformers/HandleBarsFile.cs new file mode 100644 index 00000000..51a35ccf --- /dev/null +++ b/src/WireMock.Net/Transformers/HandleBarsFile.cs @@ -0,0 +1,41 @@ +using HandlebarsDotNet; +using System; +using WireMock.Handlers; +using WireMock.Validation; + +namespace WireMock.Transformers +{ + internal static class HandleBarsFile + { + public static void Register(IHandlebars handlebarsContext, IFileSystemHandler fileSystemHandler) + { + handlebarsContext.RegisterHelper("File", (writer, context, arguments) => + { + string value = ParseArgumentAndReadFileFragment(handlebarsContext, context, fileSystemHandler, arguments); + writer.Write(value); + }); + + handlebarsContext.RegisterHelper("File", (writer, options, context, arguments) => + { + string value = ParseArgumentAndReadFileFragment(handlebarsContext, context, fileSystemHandler, arguments); + options.Template(writer, value); + }); + } + + private static string ParseArgumentAndReadFileFragment(IHandlebars handlebarsContext, dynamic context, IFileSystemHandler fileSystemHandler, object[] arguments) + { + Check.Condition(arguments, args => args.Length == 1, nameof(arguments)); + Check.NotNull(arguments[0], "arguments[0]"); + + switch (arguments[0]) + { + case string path: + var templateFunc = handlebarsContext.Compile(path); + string transformed = templateFunc(context); + return fileSystemHandler.ReadResponseBodyAsString(transformed); + } + + throw new NotSupportedException($"The value '{arguments[0]}' with type '{arguments[0]?.GetType()}' cannot be used in Handlebars File."); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/HandleBarsHelpers.cs b/src/WireMock.Net/Transformers/HandleBarsHelpers.cs index b40c5bae..e7f63af5 100644 --- a/src/WireMock.Net/Transformers/HandleBarsHelpers.cs +++ b/src/WireMock.Net/Transformers/HandleBarsHelpers.cs @@ -1,10 +1,11 @@ using HandlebarsDotNet; +using WireMock.Handlers; namespace WireMock.Transformers { internal static class HandlebarsHelpers { - public static void Register(IHandlebars handlebarsContext) + public static void Register(IHandlebars handlebarsContext, IFileSystemHandler fileSystemHandler) { HandleBarsRegex.Register(handlebarsContext); @@ -15,6 +16,8 @@ namespace WireMock.Transformers HandleBarsRandom.Register(handlebarsContext); HandleBarsXeger.Register(handlebarsContext); + + HandleBarsFile.Register(handlebarsContext, fileSystemHandler); } } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs b/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs index e2c742dc..5861e0d8 100644 --- a/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs +++ b/src/WireMock.Net/Transformers/ResponseMessageTransformer.cs @@ -1,14 +1,17 @@ using HandlebarsDotNet; +using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; +using WireMock.Handlers; using WireMock.Util; +using WireMock.Validation; namespace WireMock.Transformers { - internal static class ResponseMessageTransformer + internal class ResponseMessageTransformer { private static readonly HandlebarsConfiguration HandlebarsConfiguration = new HandlebarsConfiguration { @@ -17,12 +20,14 @@ namespace WireMock.Transformers private static readonly IHandlebars HandlebarsContext = Handlebars.Create(HandlebarsConfiguration); - static ResponseMessageTransformer() + public ResponseMessageTransformer([NotNull] IFileSystemHandler fileSystemHandler) { - HandlebarsHelpers.Register(HandlebarsContext); + Check.NotNull(fileSystemHandler, nameof(fileSystemHandler)); + + HandlebarsHelpers.Register(HandlebarsContext, fileSystemHandler); } - public static ResponseMessage Transform(RequestMessage requestMessage, ResponseMessage original) + public ResponseMessage Transform(RequestMessage requestMessage, ResponseMessage original) { var responseMessage = new ResponseMessage { StatusCode = original.StatusCode }; @@ -90,14 +95,14 @@ namespace WireMock.Transformers }; } - private static void WalkNode(JToken node, object template) + private static void WalkNode(JToken node, object context) { if (node.Type == JTokenType.Object) { // In case of Object, loop all children. Do a ToArray() to avoid `Collection was modified` exceptions. foreach (JProperty child in node.Children().ToArray()) { - WalkNode(child.Value, template); + WalkNode(child.Value, context); } } else if (node.Type == JTokenType.Array) @@ -105,7 +110,7 @@ namespace WireMock.Transformers // In case of Array, loop all items. Do a ToArray() to avoid `Collection was modified` exceptions. foreach (JToken child in node.Children().ToArray()) { - WalkNode(child, template); + WalkNode(child, context); } } else if (node.Type == JTokenType.String) @@ -118,7 +123,7 @@ namespace WireMock.Transformers } var templateForStringValue = HandlebarsContext.Compile(stringValue); - string transformedString = templateForStringValue(template); + string transformedString = templateForStringValue(context); if (!string.Equals(stringValue, transformedString)) { ReplaceNodeValue(node, transformedString); diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index 607cf856..355acfcf 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -62,7 +62,7 @@ runtime; build; native; contentfiles; analyzers - + diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsFileTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsFileTests.cs new file mode 100644 index 00000000..27eff07b --- /dev/null +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsFileTests.cs @@ -0,0 +1,102 @@ +using Moq; +using Newtonsoft.Json.Linq; +using NFluent; +using System; +using System.Threading.Tasks; +using WireMock.Handlers; +using WireMock.Models; +using WireMock.ResponseBuilders; +using WireMock.Transformers; +using Xunit; + +namespace WireMock.Net.Tests.ResponseBuilders +{ + public class ResponseWithHandlebarsFileTests + { + private readonly Mock _filesystemHandlerMock; + private const string ClientIp = "::1"; + + public ResponseWithHandlebarsFileTests() + { + _filesystemHandlerMock = new Mock(MockBehavior.Strict); + _filesystemHandlerMock.Setup(fs => fs.ReadResponseBodyAsString(It.IsAny())).Returns("abc"); + } + + [Fact] + public async Task Response_ProvideResponseAsync_Handlebars_File() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "GET", ClientIp); + + var response = Response.Create() + .WithBodyAsJson(new + { + Data = "{{File \"x.json\"}}" + }) + .WithTransformer(); + + response.SetPrivateFieldValue("_fileSystemHandler", _filesystemHandlerMock.Object); + response.SetPrivateFieldValue("_responseMessageTransformer", new ResponseMessageTransformer(_filesystemHandlerMock.Object)); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyData.BodyAsJson); + Check.That(j["Data"].Value()).Equals("abc"); + + // Verify + _filesystemHandlerMock.Verify(fs => fs.ReadResponseBodyAsString("x.json"), Times.Once); + _filesystemHandlerMock.VerifyNoOtherCalls(); + } + + [Fact] + public async Task Response_ProvideResponseAsync_Handlebars_File_Replace() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost:1234?id=x"), "GET", ClientIp); + + var response = Response.Create() + .WithBodyAsJson(new + { + Data = "{{File \"{{request.query.id}}.json\"}}" + }) + .WithTransformer(); + + response.SetPrivateFieldValue("_fileSystemHandler", _filesystemHandlerMock.Object); + response.SetPrivateFieldValue("_responseMessageTransformer", new ResponseMessageTransformer(_filesystemHandlerMock.Object)); + + // Act + var responseMessage = await response.ProvideResponseAsync(request); + + // Assert + JObject j = JObject.FromObject(responseMessage.BodyData.BodyAsJson); + Check.That(j["Data"].Value()).Equals("abc"); + + // Verify + _filesystemHandlerMock.Verify(fs => fs.ReadResponseBodyAsString("x.json"), Times.Once); + _filesystemHandlerMock.VerifyNoOtherCalls(); + } + + [Fact] + public void Response_ProvideResponseAsync_Handlebars_File_WithMissingArgument_ThrowsArgumentOutOfRangeException() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "GET", ClientIp); + + var response = Response.Create() + .WithBodyAsJson(new + { + Data = "{{File}}" + }) + .WithTransformer(); + + // Act + Check.ThatAsyncCode(() => response.ProvideResponseAsync(request)).Throws(); + + // Verify + _filesystemHandlerMock.Verify(fs => fs.ReadResponseBodyAsString(It.IsAny()), Times.Never); + _filesystemHandlerMock.VerifyNoOtherCalls(); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/TestUtils.cs b/test/WireMock.Net.Tests/TestUtils.cs index 5cd05a10..bd804944 100644 --- a/test/WireMock.Net.Tests/TestUtils.cs +++ b/test/WireMock.Net.Tests/TestUtils.cs @@ -1,4 +1,5 @@ -using System.Reflection; +using System; +using System.Reflection; namespace WireMock.Net.Tests { @@ -10,5 +11,53 @@ namespace WireMock.Net.Tests return (T)field.GetValue(obj); } + + /// + /// Set a _private_ Field Value on a given Object + /// + /// Type of the Property + /// Object from where the Property Value is returned + /// Property name as string. + /// the value to set + public static void SetPrivateFieldValue(this object obj, string propertyName, T value) + { + if (obj == null) + { + throw new ArgumentNullException(nameof(obj)); + } + + Type t = obj.GetType(); + FieldInfo fi = null; + while (fi == null && t != null) + { + fi = t.GetField(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); + t = t.BaseType; + } + + if (fi == null) + { + throw new ArgumentOutOfRangeException(nameof(propertyName), $"Field {propertyName} was not found in Type {obj.GetType().FullName}"); + } + + fi.SetValue(obj, value); + } + + /// + /// Sets a _private_ Property Value from a given Object. + /// + /// Type of the Property + /// Object from where the Property Value is set + /// Property name as string. + /// Value to set. + public static void SetPrivatePropertyValue(this object obj, string propertyName, T value) + { + Type t = obj.GetType(); + if (t.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance) == null) + { + throw new ArgumentOutOfRangeException(nameof(propertyName), $"Property {propertyName} was not found in Type {obj.GetType().FullName}"); + } + + t.InvokeMember(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.SetProperty | BindingFlags.Instance, null, obj, new object[] { value }); + } } } \ 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 e9d198e1..6936f5e1 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -36,6 +36,8 @@ + + @@ -50,6 +52,7 @@ all runtime; build; native; contentfiles; analyzers +