diff --git a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs index e2daeea7..5fa993c5 100644 --- a/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs +++ b/examples/WireMock.Net.Console.Net452.Classic/MainApp.cs @@ -1,19 +1,17 @@ -using Newtonsoft.Json; -using HandlebarsDotNet; using System; -using System.Collections.Generic; using System.Globalization; using System.IO; using System.Net; +using System.Threading.Tasks; +using Newtonsoft.Json; using WireMock.Logging; using WireMock.Matchers; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; using WireMock.Settings; -using WireMock.Util; -using System.Threading.Tasks; using WireMock.Types; +using WireMock.Util; namespace WireMock.Net.ConsoleApplication { @@ -359,7 +357,7 @@ namespace WireMock.Net.ConsoleApplication .WithHeader("Transformed-Postman-Token", "token is {{request.headers.Postman-Token}}") .WithHeader("xyz_{{request.headers.Postman-Token}}", "token is {{request.headers.Postman-Token}}") .WithBody(@"{""msg"": ""Hello world CATCH-ALL on /*, {{request.path}}, add={{Math.Add request.query.start.[0] 42}} bykey={{request.query.start}}, bykey={{request.query.stop}}, byidx0={{request.query.stop.[0]}}, byidx1={{request.query.stop.[1]}}"" }") - .WithTransformer(TransformerType.Handlebars) + .WithTransformer(TransformerType.Handlebars, true, ReplaceNodeOptions.None) .WithDelay(TimeSpan.FromMilliseconds(100)) ); diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs index c3acc208..f9cbc218 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; namespace WireMock.Admin.Mappings { @@ -64,10 +64,15 @@ namespace WireMock.Admin.Mappings public string TransformerType { get; set; } /// - /// Use the Handlerbars transformer for the content from the referenced BodyAsFile. + /// Use the Handlebars transformer for the content from the referenced BodyAsFile. /// public bool? UseTransformerForBodyAsFile { get; set; } + /// + /// The ReplaceNodeOptions to use when transforming a JSON node. + /// + public string TransformerReplaceNodeOptions { get; set; } + /// /// Gets or sets the headers. /// diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/WebhookRequestModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/WebhookRequestModel.cs index c8e7e3b7..d74a1775 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/WebhookRequestModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/WebhookRequestModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System.Collections.Generic; +using WireMock.Types; namespace WireMock.Admin.Mappings { @@ -42,5 +43,10 @@ namespace WireMock.Admin.Mappings /// Gets the type of the transformer. /// public string TransformerType { get; set; } + + /// + /// The ReplaceNodeOptions to use when transforming a JSON node. + /// + public string TransformerReplaceNodeOptions { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Models/IWebhook.cs b/src/WireMock.Net.Abstractions/Models/IWebhook.cs index 67174d9e..a4838c55 100644 --- a/src/WireMock.Net.Abstractions/Models/IWebhook.cs +++ b/src/WireMock.Net.Abstractions/Models/IWebhook.cs @@ -1,13 +1,13 @@ -namespace WireMock.Models -{ +namespace WireMock.Models +{ /// /// IWebhook - /// - public interface IWebhook + /// + public interface IWebhook { /// /// Request - /// - IWebhookRequest Request { get; set; } - } + /// + IWebhookRequest Request { get; set; } + } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Models/IWebhookRequest.cs b/src/WireMock.Net.Abstractions/Models/IWebhookRequest.cs index bfd4f07f..0f6e9be5 100644 --- a/src/WireMock.Net.Abstractions/Models/IWebhookRequest.cs +++ b/src/WireMock.Net.Abstractions/Models/IWebhookRequest.cs @@ -1,42 +1,47 @@ -using System.Collections.Generic; -using WireMock.Types; -using WireMock.Util; - -namespace WireMock.Models -{ - /// - /// IWebhookRequest - /// - public interface IWebhookRequest - { - /// - /// The Webhook Url. - /// - string Url { get; set; } - - /// - /// The method to use. - /// - string Method { get; set; } - - /// - /// The Headers to send. - /// - IDictionary> Headers { get; } - - /// - /// The body to send. - /// +using System.Collections.Generic; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Models +{ + /// + /// IWebhookRequest + /// + public interface IWebhookRequest + { + /// + /// The Webhook Url. + /// + string Url { get; set; } + + /// + /// The method to use. + /// + string Method { get; set; } + + /// + /// The Headers to send. + /// + IDictionary> Headers { get; } + + /// + /// The body to send. + /// IBodyData BodyData { get; set; } - /// - /// Use Transformer. - /// - bool? UseTransformer { get; set; } - - /// - /// The transformer type. - /// - TransformerType TransformerType { get; set; } - } + /// + /// Use Transformer. + /// + bool? UseTransformer { get; set; } + + /// + /// The transformer type. + /// + TransformerType TransformerType { get; set; } + + /// + /// The ReplaceNodeOptions to use when transforming a JSON node. + /// + ReplaceNodeOptions TransformerReplaceNodeOptions { get; set; } + } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Types/ReplaceNodeOptions.cs b/src/WireMock.Net.Abstractions/Types/ReplaceNodeOptions.cs new file mode 100644 index 00000000..cc70e57f --- /dev/null +++ b/src/WireMock.Net.Abstractions/Types/ReplaceNodeOptions.cs @@ -0,0 +1,36 @@ +using System; + +namespace WireMock.Types +{ + /// + /// Flags to use when replace a JSON node using the Transformer. + /// + [Flags] + public enum ReplaceNodeOptions + { + /// + /// Default + /// + None = 0 + + ///// + ///// Replace boolean string value to a real boolean value. (This is used by default to maintain backward compatibility.) + ///// + //Bool = 0b00000001, + + ///// + ///// Replace integer string value to a real integer value. + ///// + //Integer = 0b00000010, + + ///// + ///// Replace long string value to a real long value. + ///// + //Long = 0b00000100, + + ///// + ///// Replace all string values to a real values. + ///// + //All = Bool | Integer | Long + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Http/WebhookSender.cs b/src/WireMock.Net/Http/WebhookSender.cs index fc827bf4..0a054c33 100644 --- a/src/WireMock.Net/Http/WebhookSender.cs +++ b/src/WireMock.Net/Http/WebhookSender.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Net.Http; @@ -55,7 +55,7 @@ namespace WireMock.Http throw new NotImplementedException($"TransformerType '{request.TransformerType}' is not supported."); } - (bodyData, headers) = responseMessageTransformer.Transform(originalRequestMessage, originalResponseMessage, request.BodyData, request.Headers); + (bodyData, headers) = responseMessageTransformer.Transform(originalRequestMessage, originalResponseMessage, request.BodyData, request.Headers, request.TransformerReplaceNodeOptions); } else { diff --git a/src/WireMock.Net/Models/WebhookRequest.cs b/src/WireMock.Net/Models/WebhookRequest.cs index 077d84fa..bd6a59e8 100644 --- a/src/WireMock.Net/Models/WebhookRequest.cs +++ b/src/WireMock.Net/Models/WebhookRequest.cs @@ -1,4 +1,4 @@ -using System.Collections.Generic; +using System.Collections.Generic; using WireMock.Types; using WireMock.Util; @@ -26,5 +26,8 @@ namespace WireMock.Models /// public TransformerType TransformerType { get; set; } + + /// + public ReplaceNodeOptions TransformerReplaceNodeOptions { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Owin/WireMockMiddleware.cs b/src/WireMock.Net/Owin/WireMockMiddleware.cs index ca8f3a57..94ed7bca 100644 --- a/src/WireMock.Net/Owin/WireMockMiddleware.cs +++ b/src/WireMock.Net/Owin/WireMockMiddleware.cs @@ -1,13 +1,13 @@ using System; using System.Threading.Tasks; -using WireMock.Logging; using System.Linq; +using Stef.Validation; +using WireMock.Logging; using WireMock.Matchers; using WireMock.Http; using WireMock.Owin.Mappers; using WireMock.Serialization; using WireMock.Types; -using Stef.Validation; using WireMock.ResponseBuilders; using WireMock.Settings; #if !USE_ASPNETCORE diff --git a/src/WireMock.Net/ResponseBuilders/ITransformResponseBuilder.cs b/src/WireMock.Net/ResponseBuilders/ITransformResponseBuilder.cs index e1cfcaf0..b903401d 100644 --- a/src/WireMock.Net/ResponseBuilders/ITransformResponseBuilder.cs +++ b/src/WireMock.Net/ResponseBuilders/ITransformResponseBuilder.cs @@ -1,4 +1,4 @@ -using WireMock.Types; +using WireMock.Types; namespace WireMock.ResponseBuilders { @@ -13,7 +13,15 @@ namespace WireMock.ResponseBuilders /// /// The . /// - IResponseBuilder WithTransformer(bool transformContentFromBodyAsFile = false); + IResponseBuilder WithTransformer(bool transformContentFromBodyAsFile); + + /// + /// Use the Handlebars.Net ResponseMessage transformer. + /// + /// + /// The . + /// + IResponseBuilder WithTransformer(ReplaceNodeOptions options); /// /// Use a specific ResponseMessage transformer. @@ -21,6 +29,6 @@ namespace WireMock.ResponseBuilders /// /// The . /// - IResponseBuilder WithTransformer(TransformerType transformerType, bool transformContentFromBodyAsFile = false); + IResponseBuilder WithTransformer(TransformerType transformerType = TransformerType.Handlebars, bool transformContentFromBodyAsFile = false, ReplaceNodeOptions options = ReplaceNodeOptions.None); } } \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index e8633d9d..65f9da32 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using JetBrains.Annotations; +using Stef.Validation; using WireMock.Proxy; using WireMock.ResponseProviders; using WireMock.Settings; @@ -16,7 +17,6 @@ using WireMock.Transformers.Handlebars; using WireMock.Transformers.Scriban; using WireMock.Types; using WireMock.Util; -using Stef.Validation; namespace WireMock.ResponseBuilders { @@ -68,10 +68,15 @@ namespace WireMock.ResponseBuilders public TransformerType TransformerType { get; private set; } /// - /// Gets a value indicating whether to use the Handlerbars transformer for the content from the referenced BodyAsFile. + /// Gets a value indicating whether to use the Handlebars transformer for the content from the referenced BodyAsFile. /// public bool UseTransformerForBodyAsFile { get; private set; } + /// + /// Gets the ReplaceNodeOptions to use when transforming a JSON node. + /// + public ReplaceNodeOptions TransformerReplaceNodeOptions { get; private set; } + /// /// Gets the response message. /// @@ -330,20 +335,26 @@ namespace WireMock.ResponseBuilders } /// - public IResponseBuilder WithTransformer(bool transformContentFromBodyAsFile = false) + public IResponseBuilder WithTransformer(bool transformContentFromBodyAsFile) { - UseTransformer = true; - TransformerType = TransformerType.Handlebars; - UseTransformerForBodyAsFile = transformContentFromBodyAsFile; - return this; + return WithTransformer(TransformerType.Handlebars, transformContentFromBodyAsFile); } - /// - public IResponseBuilder WithTransformer(TransformerType transformerType, bool transformContentFromBodyAsFile = false) + /// + public IResponseBuilder WithTransformer(ReplaceNodeOptions options) + { + return WithTransformer(TransformerType.Handlebars, false, options); + } + +#pragma warning disable CS1574 + /// +#pragma warning restore CS1574 + public IResponseBuilder WithTransformer(TransformerType transformerType, bool transformContentFromBodyAsFile = false, ReplaceNodeOptions options = ReplaceNodeOptions.None) { UseTransformer = true; TransformerType = transformerType; UseTransformerForBodyAsFile = transformContentFromBodyAsFile; + TransformerReplaceNodeOptions = options; return this; } @@ -458,7 +469,7 @@ namespace WireMock.ResponseBuilders throw new NotImplementedException($"TransformerType '{TransformerType}' is not supported."); } - return (responseMessageTransformer.Transform(requestMessage, responseMessage, UseTransformerForBodyAsFile), null); + return (responseMessageTransformer.Transform(requestMessage, responseMessage, UseTransformerForBodyAsFile, TransformerReplaceNodeOptions), null); } if (!UseTransformer && ResponseMessage.BodyData?.BodyAsFileIsCached == true) diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index ebeaaaf5..26f334fb 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -1,6 +1,6 @@ -using System; using System.Collections.Generic; using System.Linq; +using Stef.Validation; using WireMock.Admin.Mappings; using WireMock.Matchers.Request; using WireMock.RequestBuilders; @@ -16,7 +16,7 @@ namespace WireMock.Serialization public MappingConverter(MatcherMapper mapper) { - _mapper = mapper ?? throw new ArgumentNullException(nameof(mapper)); + _mapper = Guard.NotNull(mapper, nameof(mapper)); } public MappingModel ToMappingModel(IMapping mapping) @@ -130,6 +130,7 @@ namespace WireMock.Serialization mappingModel.Response.UseTransformer = null; mappingModel.Response.TransformerType = null; mappingModel.Response.UseTransformerForBodyAsFile = null; + mappingModel.Response.TransformerReplaceNodeOptions = null; mappingModel.Response.BodyEncoding = null; mappingModel.Response.ProxyUrl = response.ProxyAndRecordSettings.Url; mappingModel.Response.Fault = null; @@ -150,6 +151,7 @@ namespace WireMock.Serialization { mappingModel.Response.UseTransformer = response.UseTransformer; mappingModel.Response.TransformerType = response.TransformerType.ToString(); + mappingModel.Response.TransformerReplaceNodeOptions = response.TransformerReplaceNodeOptions.ToString(); } if (response.UseTransformerForBodyAsFile) diff --git a/src/WireMock.Net/Serialization/WebhookMapper.cs b/src/WireMock.Net/Serialization/WebhookMapper.cs index 08f19f14..d944e453 100644 --- a/src/WireMock.Net/Serialization/WebhookMapper.cs +++ b/src/WireMock.Net/Serialization/WebhookMapper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using WireMock.Admin.Mappings; @@ -31,6 +31,13 @@ namespace WireMock.Serialization transformerType = TransformerType.Handlebars; } webhook.Request.TransformerType = transformerType; + + if (!Enum.TryParse(model.Request.TransformerReplaceNodeOptions, out var option)) + { + option = ReplaceNodeOptions.None; + } + + webhook.Request.TransformerReplaceNodeOptions = option; } IEnumerable contentTypeHeader = null; @@ -76,7 +83,8 @@ namespace WireMock.Serialization Method = webhook.Request.Method, Headers = webhook.Request.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()), UseTransformer = webhook.Request.UseTransformer, - TransformerType = webhook.Request.UseTransformer == true ? webhook.Request.TransformerType.ToString() : null + TransformerType = webhook.Request.UseTransformer == true ? webhook.Request.TransformerType.ToString() : null, + TransformerReplaceNodeOptions = webhook.Request.TransformerReplaceNodeOptions.ToString() } }; @@ -93,6 +101,7 @@ namespace WireMock.Serialization break; default: + // Empty break; } } diff --git a/src/WireMock.Net/Server/WireMockServer.Admin.cs b/src/WireMock.Net/Server/WireMockServer.Admin.cs index 3ef204c3..a271a592 100644 --- a/src/WireMock.Net/Server/WireMockServer.Admin.cs +++ b/src/WireMock.Net/Server/WireMockServer.Admin.cs @@ -9,6 +9,7 @@ using System.Threading.Tasks; using JetBrains.Annotations; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Stef.Validation; using WireMock.Admin.Mappings; using WireMock.Admin.Scenarios; using WireMock.Admin.Settings; @@ -24,7 +25,6 @@ using WireMock.Serialization; using WireMock.Settings; using WireMock.Types; using WireMock.Util; -using Stef.Validation; namespace WireMock.Server { @@ -785,7 +785,15 @@ namespace WireMock.Server { transformerType = TransformerType.Handlebars; } - responseBuilder = responseBuilder.WithTransformer(transformerType, responseModel.UseTransformerForBodyAsFile == true); + + if (!Enum.TryParse(responseModel.TransformerReplaceNodeOptions, out var option)) + { + option = ReplaceNodeOptions.None; + } + responseBuilder = responseBuilder.WithTransformer( + transformerType, + responseModel.UseTransformerForBodyAsFile == true, + option); } if (!string.IsNullOrEmpty(responseModel.ProxyUrl)) diff --git a/src/WireMock.Net/Transformers/Handlebars/HandlebarsContext.cs b/src/WireMock.Net/Transformers/Handlebars/HandlebarsContext.cs index a4e553be..10af57ac 100644 --- a/src/WireMock.Net/Transformers/Handlebars/HandlebarsContext.cs +++ b/src/WireMock.Net/Transformers/Handlebars/HandlebarsContext.cs @@ -1,18 +1,18 @@ -using HandlebarsDotNet; -using WireMock.Handlers; +using HandlebarsDotNet; +using WireMock.Handlers; -namespace WireMock.Transformers.Handlebars -{ - internal class HandlebarsContext : IHandlebarsContext - { - public IHandlebars Handlebars { get; set; } - - public IFileSystemHandler FileSystemHandler { get; set; } - - public string ParseAndRender(string text, object model) - { - var template = Handlebars.Compile(text); - return template(model); - } - } +namespace WireMock.Transformers.Handlebars +{ + internal class HandlebarsContext : IHandlebarsContext + { + public IHandlebars Handlebars { get; set; } + + public IFileSystemHandler FileSystemHandler { get; set; } + + public string ParseAndRender(string text, object model) + { + var template = Handlebars.Compile(text); + return template(model); + } + } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/ITransformer.cs b/src/WireMock.Net/Transformers/ITransformer.cs index ebcd3336..f1af1fc9 100644 --- a/src/WireMock.Net/Transformers/ITransformer.cs +++ b/src/WireMock.Net/Transformers/ITransformer.cs @@ -1,13 +1,14 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using WireMock.Types; using WireMock.Util; -namespace WireMock.Transformers -{ - interface ITransformer - { - ResponseMessage Transform(RequestMessage requestMessage, ResponseMessage original, bool useTransformerForBodyAsFile); - - (IBodyData BodyData, IDictionary> Headers) Transform(RequestMessage originalRequestMessage, ResponseMessage originalResponseMessage, IBodyData bodyData, IDictionary> headers); - } +namespace WireMock.Transformers +{ + interface ITransformer + { + ResponseMessage Transform(RequestMessage requestMessage, ResponseMessage original, bool useTransformerForBodyAsFile, ReplaceNodeOptions options); + + (IBodyData BodyData, IDictionary> Headers) Transform(RequestMessage originalRequestMessage, ResponseMessage originalResponseMessage, IBodyData bodyData, IDictionary> headers, ReplaceNodeOptions options); + } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/Transformer.cs b/src/WireMock.Net/Transformers/Transformer.cs index 99857779..afb90b99 100644 --- a/src/WireMock.Net/Transformers/Transformer.cs +++ b/src/WireMock.Net/Transformers/Transformer.cs @@ -18,7 +18,7 @@ namespace WireMock.Transformers _factory = factory ?? throw new ArgumentNullException(nameof(factory)); } - public (IBodyData BodyData, IDictionary> Headers) Transform(RequestMessage originalRequestMessage, ResponseMessage originalResponseMessage, IBodyData bodyData, IDictionary> headers) + public (IBodyData BodyData, IDictionary> Headers) Transform(RequestMessage originalRequestMessage, ResponseMessage originalResponseMessage, IBodyData bodyData, IDictionary> headers, ReplaceNodeOptions options) { var transformerContext = _factory.Create(); @@ -31,13 +31,13 @@ namespace WireMock.Transformers IBodyData newBodyData = null; if (bodyData?.DetectedBodyType != null) { - newBodyData = TransformBodyData(transformerContext, model, bodyData, false); + newBodyData = TransformBodyData(transformerContext, options, model, bodyData, false); } return (newBodyData, TransformHeaders(transformerContext, model, headers)); } - public ResponseMessage Transform(RequestMessage requestMessage, ResponseMessage original, bool useTransformerForBodyAsFile) + public ResponseMessage Transform(RequestMessage requestMessage, ResponseMessage original, bool useTransformerForBodyAsFile, ReplaceNodeOptions options) { var transformerContext = _factory.Create(); @@ -50,7 +50,7 @@ namespace WireMock.Transformers if (original.BodyData?.DetectedBodyType != null) { - responseMessage.BodyData = TransformBodyData(transformerContext, model, original.BodyData, useTransformerForBodyAsFile); + responseMessage.BodyData = TransformBodyData(transformerContext, options, model, original.BodyData, useTransformerForBodyAsFile); if (original.BodyData.DetectedBodyType == BodyType.String) { @@ -77,12 +77,12 @@ namespace WireMock.Transformers return responseMessage; } - private static IBodyData TransformBodyData(ITransformerContext transformerContext, object model, IBodyData original, bool useTransformerForBodyAsFile) + private static IBodyData TransformBodyData(ITransformerContext transformerContext, ReplaceNodeOptions options, object model, IBodyData original, bool useTransformerForBodyAsFile) { switch (original?.DetectedBodyType) { case BodyType.Json: - return TransformBodyAsJson(transformerContext, model, original); + return TransformBodyAsJson(transformerContext, options, model, original); case BodyType.File: return TransformBodyAsFile(transformerContext, model, original, useTransformerForBodyAsFile); @@ -114,28 +114,28 @@ namespace WireMock.Transformers return newHeaders; } - private static IBodyData TransformBodyAsJson(ITransformerContext handlebarsContext, object model, IBodyData original) + private static IBodyData TransformBodyAsJson(ITransformerContext handlebarsContext, ReplaceNodeOptions options, object model, IBodyData original) { JToken jToken; switch (original.BodyAsJson) { case JObject bodyAsJObject: jToken = bodyAsJObject.DeepClone(); - WalkNode(handlebarsContext, jToken, model); + WalkNode(handlebarsContext, options, jToken, model); break; case Array bodyAsArray: jToken = JArray.FromObject(bodyAsArray); - WalkNode(handlebarsContext, jToken, model); + WalkNode(handlebarsContext, options, jToken, model); break; case string bodyAsString: - jToken = ReplaceSingleNode(handlebarsContext, bodyAsString, model); + jToken = ReplaceSingleNode(handlebarsContext, options, bodyAsString, model); break; default: jToken = JObject.FromObject(original.BodyAsJson); - WalkNode(handlebarsContext, jToken, model); + WalkNode(handlebarsContext, options, jToken, model); break; } @@ -148,7 +148,7 @@ namespace WireMock.Transformers }; } - private static JToken ReplaceSingleNode(ITransformerContext handlebarsContext, string stringValue, object model) + private static JToken ReplaceSingleNode(ITransformerContext handlebarsContext, ReplaceNodeOptions options, string stringValue, object model) { string transformedString = handlebarsContext.ParseAndRender(stringValue, model); @@ -158,7 +158,7 @@ namespace WireMock.Transformers JObject dummy = JObject.Parse($"{{ \"{property}\": null }}"); JToken node = dummy[property]; - ReplaceNodeValue(node, transformedString); + ReplaceNodeValue(options, node, transformedString); return dummy[property]; } @@ -166,44 +166,47 @@ namespace WireMock.Transformers return stringValue; } - private static void WalkNode(ITransformerContext handlebarsContext, JToken node, object model) + private static void WalkNode(ITransformerContext handlebarsContext, ReplaceNodeOptions options, JToken node, object model) { - if (node.Type == JTokenType.Object) + switch (node.Type) { - // In case of Object, loop all children. Do a ToArray() to avoid `Collection was modified` exceptions. - foreach (JProperty child in node.Children().ToArray()) - { - WalkNode(handlebarsContext, child.Value, model); - } - } - else if (node.Type == JTokenType.Array) - { - // In case of Array, loop all items. Do a ToArray() to avoid `Collection was modified` exceptions. - foreach (JToken child in node.Children().ToArray()) - { - WalkNode(handlebarsContext, child, model); - } - } - else if (node.Type == JTokenType.String) - { - // In case of string, try to transform the value. - string stringValue = node.Value(); - if (string.IsNullOrEmpty(stringValue)) - { - return; - } + case JTokenType.Object: + // In case of Object, loop all children. Do a ToArray() to avoid `Collection was modified` exceptions. + foreach (var child in node.Children().ToArray()) + { + WalkNode(handlebarsContext, options, child.Value, model); + } + break; - string transformedString = handlebarsContext.ParseAndRender(stringValue, model); - if (!string.Equals(stringValue, transformedString)) - { - ReplaceNodeValue(node, transformedString); - } + case JTokenType.Array: + // In case of Array, loop all items. Do a ToArray() to avoid `Collection was modified` exceptions. + foreach (var child in node.Children().ToArray()) + { + WalkNode(handlebarsContext, options, child, model); + } + break; + + case JTokenType.String: + // In case of string, try to transform the value. + string stringValue = node.Value(); + if (string.IsNullOrEmpty(stringValue)) + { + return; + } + + string transformed = handlebarsContext.ParseAndRender(stringValue, model); + if (!string.Equals(stringValue, transformed)) + { + ReplaceNodeValue(options, node, transformed); + } + break; } } - private static void ReplaceNodeValue(JToken node, string stringValue) + private static void ReplaceNodeValue(ReplaceNodeOptions options, JToken node, string transformedString) { - if (bool.TryParse(stringValue, out bool valueAsBoolean)) + StringUtils.TryParseQuotedString(transformedString, out var result, out _); + if (bool.TryParse(result, out var valueAsBoolean) || bool.TryParse(transformedString, out valueAsBoolean)) { node.Replace(valueAsBoolean); return; @@ -213,12 +216,12 @@ namespace WireMock.Transformers try { // Try to convert this string into a JsonObject - value = JToken.Parse(stringValue); + value = JToken.Parse(transformedString); } catch (JsonException) { // Ignore JsonException and just keep string value and convert to JToken - value = stringValue; + value = transformedString; } node.Replace(value); @@ -248,18 +251,15 @@ namespace WireMock.Transformers BodyAsFile = transformedBodyAsFilename }; } - else - { - string text = handlebarsContext.FileSystemHandler.ReadResponseBodyAsString(transformedBodyAsFilename); - return new BodyData - { - DetectedBodyType = BodyType.String, - DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, - BodyAsString = handlebarsContext.ParseAndRender(text, model), - BodyAsFile = transformedBodyAsFilename - }; - } + string text = handlebarsContext.FileSystemHandler.ReadResponseBodyAsString(transformedBodyAsFilename); + return new BodyData + { + DetectedBodyType = BodyType.String, + DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, + BodyAsString = handlebarsContext.ParseAndRender(text, model), + BodyAsFile = transformedBodyAsFilename + }; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Util/JsonUtils.cs b/src/WireMock.Net/Util/JsonUtils.cs index 19626932..ec754147 100644 --- a/src/WireMock.Net/Util/JsonUtils.cs +++ b/src/WireMock.Net/Util/JsonUtils.cs @@ -10,6 +10,33 @@ namespace WireMock.Util { internal static class JsonUtils { + public static bool TryParseAsComplexObject(string strInput, out JToken token) + { + token = null; + + if (string.IsNullOrWhiteSpace(strInput)) + { + return false; + } + + strInput = strInput.Trim(); + if ((!strInput.StartsWith("{") || !strInput.EndsWith("}")) && (!strInput.StartsWith("[") || !strInput.EndsWith("]"))) + { + return false; + } + + try + { + // Try to convert this string into a JToken + token = JToken.Parse(strInput); + return true; + } + catch + { + return false; + } + } + public static string Serialize(T value) { return JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsIncludeNullValues); diff --git a/src/WireMock.Net/Util/StringUtils.cs b/src/WireMock.Net/Util/StringUtils.cs new file mode 100644 index 00000000..18c59853 --- /dev/null +++ b/src/WireMock.Net/Util/StringUtils.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Text.RegularExpressions; + +namespace WireMock.Util +{ + internal static class StringUtils + { + public static bool TryParseQuotedString(string value, out string result, out char quote) + { + result = null; + quote = '\0'; + + if (value == null || value.Length < 2) + { + return false; + } + + quote = value[0]; // This can be single or a double quote + if (quote != '"' && quote != '\'') + { + return false; + } + + if (value.Last() != quote) + { + return false; + } + + try + { + result = Regex.Unescape(value.Substring(1, value.Length - 2)); + return true; + } + catch + { + // Ignore Exception, just continue and return false. + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index c4504e66..4331ecf3 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -83,7 +83,7 @@ - + diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithBodyFromFileTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithBodyFromFileTests.cs index b1a91bf3..af990c5a 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithBodyFromFileTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithBodyFromFileTests.cs @@ -1,10 +1,8 @@ -using FluentAssertions; -using System; -using System.Collections.Generic; using System.IO; using System.Net; using System.Net.Http; using System.Threading.Tasks; +using FluentAssertions; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs index 6afc2ca4..728ab790 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs @@ -7,6 +7,7 @@ using WireMock.Handlers; using WireMock.Models; using WireMock.ResponseBuilders; using WireMock.Settings; +using WireMock.Types; using Xunit; namespace WireMock.Net.Tests.ResponseBuilders @@ -15,15 +16,14 @@ namespace WireMock.Net.Tests.ResponseBuilders { private const string ClientIp = "::1"; - private readonly Mock _filesystemHandlerMock; private readonly WireMockServerSettings _settings = new WireMockServerSettings(); public ResponseWithHandlebarsRandomTests() { - _filesystemHandlerMock = new Mock(MockBehavior.Strict); - _filesystemHandlerMock.Setup(fs => fs.ReadResponseBodyAsString(It.IsAny())).Returns("abc"); + var filesystemHandlerMock = new Mock(MockBehavior.Strict); + filesystemHandlerMock.Setup(fs => fs.ReadResponseBodyAsString(It.IsAny())).Returns("abc"); - _settings.FileSystemHandler = _filesystemHandlerMock.Object; + _settings.FileSystemHandler = filesystemHandlerMock.Object; } [Fact] @@ -73,6 +73,31 @@ namespace WireMock.Net.Tests.ResponseBuilders Check.That(j["Value"].Type).IsEqualTo(JTokenType.Boolean); } + [Theory] + [InlineData(ReplaceNodeOptions.None, JTokenType.Integer)] + //[InlineData(ReplaceNodeOptions.Bool, JTokenType.String)] + //[InlineData(ReplaceNodeOptions.Integer, JTokenType.Integer)] + //[InlineData(ReplaceNodeOptions.Bool | ReplaceNodeOptions.Integer, JTokenType.Integer)] + public async Task Response_ProvideResponseAsync_Handlebars_Random1_Integer(ReplaceNodeOptions options, JTokenType expected) + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost:1234"), "GET", ClientIp); + + var responseBuilder = Response.Create() + .WithBodyAsJson(new + { + Value = "{{Random Type=\"Integer\"}}" + }) + .WithTransformer(options); + + // Act + var response = await responseBuilder.ProvideResponseAsync(request, _settings).ConfigureAwait(false); + + // Assert + JObject j = JObject.FromObject(response.Message.BodyData.BodyAsJson); + Check.That(j["Value"].Type).IsEqualTo(expected); + } + [Fact] public async Task Response_ProvideResponseAsync_Handlebars_Random1_Guid() { diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs index 230143f3..e8f36677 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs @@ -23,17 +23,16 @@ namespace WireMock.Net.Tests.ResponseBuilders { public class ResponseWithTransformerTests { - private readonly Mock _filesystemHandlerMock; private readonly WireMockServerSettings _settings = new WireMockServerSettings(); private const string ClientIp = "::1"; public ResponseWithTransformerTests() { - _filesystemHandlerMock = new Mock(MockBehavior.Strict); - _filesystemHandlerMock.Setup(fs => fs.ReadResponseBodyAsString(It.IsAny())).Returns("abc"); + var filesystemHandlerMock = new Mock(MockBehavior.Strict); + filesystemHandlerMock.Setup(fs => fs.ReadResponseBodyAsString(It.IsAny())).Returns("abc"); - _settings.FileSystemHandler = _filesystemHandlerMock.Object; + _settings.FileSystemHandler = filesystemHandlerMock.Object; } [Theory] @@ -366,7 +365,7 @@ namespace WireMock.Net.Tests.ResponseBuilders public async Task Response_ProvideResponse_Transformer_WithBodyAsJson_ResultAsObject(TransformerType transformerType) { // Assign - string jsonString = "{ \"things\": [ { \"name\": \"RequiredThing\" }, { \"name\": \"Wiremock\" } ] }"; + string jsonString = "{ \"things\": [ { \"name\": \"RequiredThing\" }, { \"name\": \"WireMock\" } ] }"; var bodyData = new BodyData { BodyAsJson = JsonConvert.DeserializeObject(jsonString), @@ -386,6 +385,108 @@ namespace WireMock.Net.Tests.ResponseBuilders Check.That(JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson)).Equals("{\"x\":\"test /foo_object\"}"); } + //[Theory] + //[InlineData(TransformerType.Handlebars, "a")] + //[InlineData(TransformerType.Handlebars, "42")] + //[InlineData(TransformerType.Handlebars, "{")] + //[InlineData(TransformerType.Handlebars, "]")] + //[InlineData(TransformerType.Handlebars, " ")] + //public async Task Response_ProvideResponse_Transformer_WithBodyAsJsonWithExtraQuotes_AndSpecialOption_MakesAString_ResultAsObject(TransformerType transformerType, string text) + //{ + // string jsonString = $"{{ \"x\": \"{text}\" }}"; + // var bodyData = new BodyData + // { + // BodyAsJson = JsonConvert.DeserializeObject(jsonString), + // DetectedBodyType = BodyType.Json, + // Encoding = Encoding.UTF8 + // }; + // var request = new RequestMessage(new UrlDetails("http://localhost/foo_object"), "POST", ClientIp, bodyData); + + // var responseBuilder = Response.Create() + // .WithBodyAsJson(new { text = "\"{{request.bodyAsJson.x}}\"" }) + // .WithTransformer(transformerType, false, ReplaceNodeOptions.Default); + + // // Act + // var response = await responseBuilder.ProvideResponseAsync(request, _settings).ConfigureAwait(false); + + // // Assert + // JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson).Should().Be($"{{\"text\":\"{text}\"}}"); + //} + + [Theory] + [InlineData(TransformerType.Handlebars, "\"\"", "\"\"")] + [InlineData(TransformerType.Handlebars, "\"a\"", "\"a\"")] + [InlineData(TransformerType.Handlebars, "\" \"", "\" \"")] + [InlineData(TransformerType.Handlebars, "\"'\"", "\"'\"")] + [InlineData(TransformerType.Handlebars, "\"false\"", "false")] // bool is special + [InlineData(TransformerType.Handlebars, "false", "false")] + [InlineData(TransformerType.Handlebars, "\"true\"", "true")] // bool is special + [InlineData(TransformerType.Handlebars, "true", "true")] + [InlineData(TransformerType.Handlebars, "\"-42\"", "-42")] // todo + [InlineData(TransformerType.Handlebars, "-42", "-42")] + [InlineData(TransformerType.Handlebars, "\"2147483647\"", "2147483647")] // todo + [InlineData(TransformerType.Handlebars, "2147483647", "2147483647")] + [InlineData(TransformerType.Handlebars, "\"9223372036854775807\"", "9223372036854775807")] // todo + [InlineData(TransformerType.Handlebars, "9223372036854775807", "9223372036854775807")] + public async Task Response_ProvideResponse_Transformer_WithBodyAsJson_And_ReplaceNodeOptionsKeep(TransformerType transformerType, string value, string expected) + { + string jsonString = $"{{ \"x\": {value} }}"; + var bodyData = new BodyData + { + BodyAsJson = JsonConvert.DeserializeObject(jsonString), + DetectedBodyType = BodyType.Json, + Encoding = Encoding.UTF8 + }; + var request = new RequestMessage(new UrlDetails("http://localhost/foo_object"), "POST", ClientIp, bodyData); + + var responseBuilder = Response.Create() + .WithBodyAsJson(new { text = "{{request.bodyAsJson.x}}" }) + .WithTransformer(transformerType, false, ReplaceNodeOptions.None); + + // Act + var response = await responseBuilder.ProvideResponseAsync(request, _settings).ConfigureAwait(false); + + // Assert + JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson).Should().Be($"{{\"text\":{expected}}}"); + } + + [Theory] + [InlineData(TransformerType.Handlebars, "\"\"", "\"\"")] + [InlineData(TransformerType.Handlebars, "\"a\"", "\"a\"")] + [InlineData(TransformerType.Handlebars, "\" \"", "\" \"")] + [InlineData(TransformerType.Handlebars, "\"'\"", "\"'\"")] + [InlineData(TransformerType.Handlebars, "\"false\"", "false")] // bool is special + [InlineData(TransformerType.Handlebars, "false", "false")] + [InlineData(TransformerType.Handlebars, "\"true\"", "true")] // bool is special + [InlineData(TransformerType.Handlebars, "true", "true")] + [InlineData(TransformerType.Handlebars, "\"-42\"", "\"-42\"")] + [InlineData(TransformerType.Handlebars, "-42", "\"-42\"")] + [InlineData(TransformerType.Handlebars, "\"2147483647\"", "\"2147483647\"")] + [InlineData(TransformerType.Handlebars, "2147483647", "\"2147483647\"")] + [InlineData(TransformerType.Handlebars, "\"9223372036854775807\"", "\"9223372036854775807\"")] + [InlineData(TransformerType.Handlebars, "9223372036854775807", "\"9223372036854775807\"")] + public async Task Response_ProvideResponse_Transformer_WithBodyAsJsonWithExtraQuotes_AlwaysMakesString(TransformerType transformerType, string value, string expected) + { + string jsonString = $"{{ \"x\": {value} }}"; + var bodyData = new BodyData + { + BodyAsJson = JsonConvert.DeserializeObject(jsonString), + DetectedBodyType = BodyType.Json, + Encoding = Encoding.UTF8 + }; + var request = new RequestMessage(new UrlDetails("http://localhost/foo_object"), "POST", ClientIp, bodyData); + + var responseBuilder = Response.Create() + .WithBodyAsJson(new { text = "\"{{request.bodyAsJson.x}}\"" }) + .WithTransformer(transformerType); + + // Act + var response = await responseBuilder.ProvideResponseAsync(request, _settings).ConfigureAwait(false); + + // Assert + JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson).Should().Be($"{{\"text\":{expected}}}"); + } + [Theory] [InlineData(TransformerType.Handlebars)] //[InlineData(TransformerType.Scriban)] Scriban cannot access dynamic Json Objects diff --git a/test/WireMock.Net.Tests/Util/StringUtilsTests.cs b/test/WireMock.Net.Tests/Util/StringUtilsTests.cs new file mode 100644 index 00000000..0afd2e90 --- /dev/null +++ b/test/WireMock.Net.Tests/Util/StringUtilsTests.cs @@ -0,0 +1,104 @@ +using FluentAssertions; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.Util +{ + public class StringUtilsTests + { + [Theory] + [InlineData("'s")] + [InlineData("\"s")] + public void StringUtils_TryParseQuotedString_With_UnexpectedUnclosedString_Returns_False(string input) + { + // Act + bool valid = StringUtils.TryParseQuotedString(input, out var result, out var quote); + + // Assert + valid.Should().BeFalse(); + } + + [Theory] + [InlineData("")] + [InlineData(null)] + [InlineData("x")] + public void StringUtils_TryParseQuotedString_With_InvalidStringLength_Returns_False(string input) + { + // Act + bool valid = StringUtils.TryParseQuotedString(input, out var result, out var quote); + + // Assert + valid.Should().BeFalse(); + } + + [Theory] + [InlineData("xx")] + [InlineData(" ")] + public void StringUtils_TryParseQuotedString_With_InvalidStringQuoteCharacter_Returns_False(string input) + { + // Act + bool valid = StringUtils.TryParseQuotedString(input, out var result, out var quote); + + // Assert + valid.Should().BeFalse(); + } + + [Fact] + public void StringUtils_TryParseQuotedString_With_UnexpectedUnrecognizedEscapeSequence_Returns_False() + { + // Arrange + string input = new string(new[] { '"', '\\', 'u', '?', '"' }); + + // Act + bool valid = StringUtils.TryParseQuotedString(input, out var result, out var quote); + + // Assert + valid.Should().BeFalse(); + } + + [Theory] + [InlineData("''", "")] + [InlineData("'s'", "s")] + [InlineData("'\\\\'", "\\")] + [InlineData("'\\n'", "\n")] + public void StringUtils_TryParseQuotedString_SingleQuotedString(string input, string expectedResult) + { + // Act + bool valid = StringUtils.TryParseQuotedString(input, out var result, out var quote); + + // Assert + valid.Should().BeTrue(); + result.Should().Be(expectedResult); + quote.Should().Be('\''); + } + + [Theory] + [InlineData("\"\"", "")] + [InlineData("\"\\\\\"", "\\")] + [InlineData("\"\\n\"", "\n")] + [InlineData("\"\\\\n\"", "\\n")] + [InlineData("\"\\\\new\"", "\\new")] + [InlineData("\"[]\"", "[]")] + [InlineData("\"()\"", "()")] + [InlineData("\"(\\\"\\\")\"", "(\"\")")] + [InlineData("\"/\"", "/")] + [InlineData("\"a\"", "a")] + [InlineData("\"This \\\"is\\\" a test.\"", "This \"is\" a test.")] + [InlineData(@"""This \""is\"" b test.""", @"This ""is"" b test.")] + [InlineData("\"ab\\\"cd\"", "ab\"cd")] + [InlineData("\"\\\"\"", "\"")] + [InlineData("\"\\\"\\\"\"", "\"\"")] + [InlineData("\"AB YZ 19 \uD800\udc05 \u00e4\"", "AB YZ 19 \uD800\udc05 \u00e4")] + [InlineData("\"\\\\\\\\192.168.1.1\\\\audio\\\\new\"", "\\\\192.168.1.1\\audio\\new")] + public void StringUtils_TryParseQuotedString_DoubleQuotedString(string input, string expectedResult) + { + // Act + bool valid = StringUtils.TryParseQuotedString(input, out var result, out var quote); + + // Assert + valid.Should().BeTrue(); + result.Should().Be(expectedResult); + quote.Should().Be('"'); + } + } +} \ No newline at end of file