diff --git a/WireMock.Net Solution.sln.DotSettings b/WireMock.Net Solution.sln.DotSettings index 99ef6d87..92eaabab 100644 --- a/WireMock.Net Solution.sln.DotSettings +++ b/WireMock.Net Solution.sln.DotSettings @@ -33,6 +33,7 @@ True True True + True True True True diff --git a/src/WireMock.Net.Abstractions/Types/ReplaceNodeOptions.cs b/src/WireMock.Net.Abstractions/Types/ReplaceNodeOptions.cs index cc70e57f..3f8cb318 100644 --- a/src/WireMock.Net.Abstractions/Types/ReplaceNodeOptions.cs +++ b/src/WireMock.Net.Abstractions/Types/ReplaceNodeOptions.cs @@ -1,36 +1,25 @@ -using System; +namespace WireMock.Types; -namespace WireMock.Types +/// +/// Logic to use when replace a JSON node using the Transformer. +/// +public enum ReplaceNodeOptions { /// - /// Flags to use when replace a JSON node using the Transformer. + /// Try to evaluate a templated value. + /// In case this is valid, return the value and if the value can be converted to a primitive type, use that value. /// - [Flags] - public enum ReplaceNodeOptions - { - /// - /// Default - /// - None = 0 + EvaluateAndTryToConvert = 0, - ///// - ///// Replace boolean string value to a real boolean value. (This is used by default to maintain backward compatibility.) - ///// - //Bool = 0b00000001, + /// + /// Try to evaluate a templated value. + /// In case this is valid, return the value, else fallback to the parse behavior. + /// + Evaluate = 1, - ///// - ///// 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 - } + /// + /// Parse templated string to a templated string. + /// (keep a templated string value as string value). + /// + Parse = 2 } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Types/TransformerType.cs b/src/WireMock.Net.Abstractions/Types/TransformerType.cs index 5d7015e1..9ae26df9 100644 --- a/src/WireMock.Net.Abstractions/Types/TransformerType.cs +++ b/src/WireMock.Net.Abstractions/Types/TransformerType.cs @@ -1,23 +1,22 @@ -namespace WireMock.Types +namespace WireMock.Types; + +/// +/// The ResponseMessage Transformers +/// +public enum TransformerType { /// - /// The ResponseMessage Transformers + /// https://github.com/Handlebars-Net/Handlebars.Net /// - public enum TransformerType - { - /// - /// https://github.com/Handlebars-Net/Handlebars.Net - /// - Handlebars, + Handlebars, - /// - /// https://github.com/scriban/scriban : default - /// - Scriban, + /// + /// https://github.com/scriban/scriban : default + /// + Scriban, - /// - /// https://github.com/scriban/scriban : DotLiquid - /// - ScribanDotLiquid - } + /// + /// https://github.com/scriban/scriban : DotLiquid + /// + ScribanDotLiquid } \ No newline at end of file diff --git a/src/WireMock.Net/Http/WebhookSender.cs b/src/WireMock.Net/Http/WebhookSender.cs index 63b4fd6d..25aaebe5 100644 --- a/src/WireMock.Net/Http/WebhookSender.cs +++ b/src/WireMock.Net/Http/WebhookSender.cs @@ -51,14 +51,14 @@ internal class WebhookSender switch (webhookRequest.TransformerType) { case TransformerType.Handlebars: - var factoryHandlebars = new HandlebarsContextFactory(_settings.FileSystemHandler, _settings.HandlebarsRegistrationCallback); - transformer = new Transformer(factoryHandlebars); + var factoryHandlebars = new HandlebarsContextFactory(_settings); + transformer = new Transformer(_settings, factoryHandlebars); break; case TransformerType.Scriban: case TransformerType.ScribanDotLiquid: var factoryDotLiquid = new ScribanContextFactory(_settings.FileSystemHandler, webhookRequest.TransformerType); - transformer = new Transformer(factoryDotLiquid); + transformer = new Transformer(_settings, factoryDotLiquid); break; default: diff --git a/src/WireMock.Net/ResponseBuilders/ITransformResponseBuilder.cs b/src/WireMock.Net/ResponseBuilders/ITransformResponseBuilder.cs index a9da2312..729f8894 100644 --- a/src/WireMock.Net/ResponseBuilders/ITransformResponseBuilder.cs +++ b/src/WireMock.Net/ResponseBuilders/ITransformResponseBuilder.cs @@ -29,5 +29,5 @@ public interface ITransformResponseBuilder : IDelayResponseBuilder /// /// The . /// - IResponseBuilder WithTransformer(TransformerType transformerType = TransformerType.Handlebars, bool transformContentFromBodyAsFile = false, ReplaceNodeOptions options = ReplaceNodeOptions.None); + IResponseBuilder WithTransformer(TransformerType transformerType = TransformerType.Handlebars, bool transformContentFromBodyAsFile = false, ReplaceNodeOptions options = ReplaceNodeOptions.Evaluate); } \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 23221f8a..13c482b4 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -207,7 +207,7 @@ public partial class Response : IResponseBuilder } /// - public IResponseBuilder WithTransformer(TransformerType transformerType, bool transformContentFromBodyAsFile = false, ReplaceNodeOptions options = ReplaceNodeOptions.None) + public IResponseBuilder WithTransformer(TransformerType transformerType, bool transformContentFromBodyAsFile = false, ReplaceNodeOptions options = ReplaceNodeOptions.Evaluate) { UseTransformer = true; TransformerType = transformerType; @@ -314,14 +314,14 @@ public partial class Response : IResponseBuilder switch (TransformerType) { case TransformerType.Handlebars: - var factoryHandlebars = new HandlebarsContextFactory(settings.FileSystemHandler, settings.HandlebarsRegistrationCallback); - responseMessageTransformer = new Transformer(factoryHandlebars); + var factoryHandlebars = new HandlebarsContextFactory(settings); + responseMessageTransformer = new Transformer(settings, factoryHandlebars); break; case TransformerType.Scriban: case TransformerType.ScribanDotLiquid: var factoryDotLiquid = new ScribanContextFactory(settings.FileSystemHandler, TransformerType); - responseMessageTransformer = new Transformer(factoryDotLiquid); + responseMessageTransformer = new Transformer(settings, factoryDotLiquid); break; default: diff --git a/src/WireMock.Net/Serialization/WebhookMapper.cs b/src/WireMock.Net/Serialization/WebhookMapper.cs index ba72e9b1..5550190f 100644 --- a/src/WireMock.Net/Serialization/WebhookMapper.cs +++ b/src/WireMock.Net/Serialization/WebhookMapper.cs @@ -39,7 +39,7 @@ internal static class WebhookMapper if (!Enum.TryParse(model.Request.TransformerReplaceNodeOptions, out var option)) { - option = ReplaceNodeOptions.None; + option = ReplaceNodeOptions.Evaluate; } webhook.Request.TransformerReplaceNodeOptions = option; } diff --git a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs index 1368b06d..1e3bbcb5 100644 --- a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs +++ b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs @@ -250,7 +250,7 @@ public partial class WireMockServer if (!Enum.TryParse(responseModel.TransformerReplaceNodeOptions, out var option)) { - option = ReplaceNodeOptions.None; + option = ReplaceNodeOptions.Evaluate; } responseBuilder = responseBuilder.WithTransformer( transformerType, diff --git a/src/WireMock.Net/Settings/WireMockServerSettings.cs b/src/WireMock.Net/Settings/WireMockServerSettings.cs index 7422046a..a03aa4a9 100644 --- a/src/WireMock.Net/Settings/WireMockServerSettings.cs +++ b/src/WireMock.Net/Settings/WireMockServerSettings.cs @@ -10,272 +10,279 @@ using WireMock.Logging; using WireMock.Matchers; using WireMock.RegularExpressions; using WireMock.Types; +using System.Globalization; #if USE_ASPNETCORE using Microsoft.Extensions.DependencyInjection; #endif -namespace WireMock.Settings +namespace WireMock.Settings; + +/// +/// WireMockServerSettings +/// +public class WireMockServerSettings { /// - /// WireMockServerSettings + /// Gets or sets the http port. /// - public class WireMockServerSettings - { - /// - /// Gets or sets the http port. - /// - [PublicAPI] - public int? Port { get; set; } + [PublicAPI] + public int? Port { get; set; } - /// - /// Gets or sets the use SSL. - /// - // ReSharper disable once InconsistentNaming - [PublicAPI] - public bool? UseSSL { get; set; } + /// + /// Gets or sets the use SSL. + /// + // ReSharper disable once InconsistentNaming + [PublicAPI] + public bool? UseSSL { get; set; } - /// - /// Defines on which scheme (http/https) to host. (This overrides the UseSSL value). - /// - [PublicAPI] - public HostingScheme? HostingScheme { get; set; } + /// + /// Defines on which scheme (http/https) to host. (This overrides the UseSSL value). + /// + [PublicAPI] + public HostingScheme? HostingScheme { get; set; } - /// - /// Gets or sets whether to start admin interface. - /// - [PublicAPI] - public bool? StartAdminInterface { get; set; } + /// + /// Gets or sets whether to start admin interface. + /// + [PublicAPI] + public bool? StartAdminInterface { get; set; } - /// - /// Gets or sets if the static mappings should be read at startup. - /// - [PublicAPI] - public bool? ReadStaticMappings { get; set; } + /// + /// Gets or sets if the static mappings should be read at startup. + /// + [PublicAPI] + public bool? ReadStaticMappings { get; set; } - /// - /// Watch the static mapping files + folder for changes when running. - /// - [PublicAPI] - public bool? WatchStaticMappings { get; set; } + /// + /// Watch the static mapping files + folder for changes when running. + /// + [PublicAPI] + public bool? WatchStaticMappings { get; set; } - /// - /// A value indicating whether subdirectories within the static mappings path should be monitored. - /// - [PublicAPI] - public bool? WatchStaticMappingsInSubdirectories { get; set; } + /// + /// A value indicating whether subdirectories within the static mappings path should be monitored. + /// + [PublicAPI] + public bool? WatchStaticMappingsInSubdirectories { get; set; } - /// - /// Gets or sets if the proxy and record settings. - /// - [PublicAPI] - public ProxyAndRecordSettings? ProxyAndRecordSettings { get; set; } + /// + /// Gets or sets if the proxy and record settings. + /// + [PublicAPI] + public ProxyAndRecordSettings? ProxyAndRecordSettings { get; set; } - /// - /// Gets or sets the urls. - /// - [PublicAPI] - public string[]? Urls { get; set; } + /// + /// Gets or sets the urls. + /// + [PublicAPI] + public string[]? Urls { get; set; } - /// - /// StartTimeout - /// - [PublicAPI] - public int StartTimeout { get; set; } = 10000; + /// + /// StartTimeout + /// + [PublicAPI] + public int StartTimeout { get; set; } = 10000; - /// - /// Allow Partial Mapping (default set to false). - /// - [PublicAPI] - public bool? AllowPartialMapping { get; set; } + /// + /// Allow Partial Mapping (default set to false). + /// + [PublicAPI] + public bool? AllowPartialMapping { get; set; } - /// - /// The username needed for __admin access. - /// - [PublicAPI] - public string? AdminUsername { get; set; } + /// + /// The username needed for __admin access. + /// + [PublicAPI] + public string? AdminUsername { get; set; } - /// - /// The password needed for __admin access. - /// - [PublicAPI] - public string? AdminPassword { get; set; } + /// + /// The password needed for __admin access. + /// + [PublicAPI] + public string? AdminPassword { get; set; } - /// - /// The AzureAD Tenant needed for __admin access. - /// - [PublicAPI] - public string? AdminAzureADTenant { get; set; } + /// + /// The AzureAD Tenant needed for __admin access. + /// + [PublicAPI] + public string? AdminAzureADTenant { get; set; } - /// - /// The AzureAD Audience / Resource for __admin access. - /// - [PublicAPI] - public string? AdminAzureADAudience { get; set; } + /// + /// The AzureAD Audience / Resource for __admin access. + /// + [PublicAPI] + public string? AdminAzureADAudience { get; set; } - /// - /// The RequestLog expiration in hours (optional). - /// - [PublicAPI] - public int? RequestLogExpirationDuration { get; set; } + /// + /// The RequestLog expiration in hours (optional). + /// + [PublicAPI] + public int? RequestLogExpirationDuration { get; set; } - /// - /// The MaxRequestLog count (optional). - /// - [PublicAPI] - public int? MaxRequestLogCount { get; set; } + /// + /// The MaxRequestLog count (optional). + /// + [PublicAPI] + public int? MaxRequestLogCount { get; set; } - /// - /// Action which is called (with the IAppBuilder or IApplicationBuilder) before the internal WireMockMiddleware is initialized. [Optional] - /// - [PublicAPI] - [JsonIgnore] - public Action? PreWireMockMiddlewareInit { get; set; } + /// + /// Action which is called (with the IAppBuilder or IApplicationBuilder) before the internal WireMockMiddleware is initialized. [Optional] + /// + [PublicAPI] + [JsonIgnore] + public Action? PreWireMockMiddlewareInit { get; set; } - /// - /// Action which is called (with the IAppBuilder or IApplicationBuilder) after the internal WireMockMiddleware is initialized. [Optional] - /// - [PublicAPI] - [JsonIgnore] - public Action? PostWireMockMiddlewareInit { get; set; } + /// + /// Action which is called (with the IAppBuilder or IApplicationBuilder) after the internal WireMockMiddleware is initialized. [Optional] + /// + [PublicAPI] + [JsonIgnore] + public Action? PostWireMockMiddlewareInit { get; set; } #if USE_ASPNETCORE - /// - /// Action which is called with IServiceCollection when ASP.NET Core DI is being configured. [Optional] - /// - [PublicAPI] - [JsonIgnore] - public Action? AdditionalServiceRegistration { get; set; } + /// + /// Action which is called with IServiceCollection when ASP.NET Core DI is being configured. [Optional] + /// + [PublicAPI] + [JsonIgnore] + public Action? AdditionalServiceRegistration { get; set; } - /// - /// Policies to use when using CORS. By default CORS is disabled. [Optional] - /// - [PublicAPI] - public CorsPolicyOptions? CorsPolicyOptions { get; set; } + /// + /// Policies to use when using CORS. By default CORS is disabled. [Optional] + /// + [PublicAPI] + public CorsPolicyOptions? CorsPolicyOptions { get; set; } #endif - /// - /// The IWireMockLogger which logs Debug, Info, Warning or Error - /// - [PublicAPI] - [JsonIgnore] - public IWireMockLogger Logger { get; set; } = null!; + /// + /// The IWireMockLogger which logs Debug, Info, Warning or Error + /// + [PublicAPI] + [JsonIgnore] + public IWireMockLogger Logger { get; set; } = null!; - /// - /// Handler to interact with the file system to read and write static mapping files. - /// - [PublicAPI] - [JsonIgnore] - public IFileSystemHandler FileSystemHandler { get; set; } = null!; + /// + /// Handler to interact with the file system to read and write static mapping files. + /// + [PublicAPI] + [JsonIgnore] + public IFileSystemHandler FileSystemHandler { get; set; } = null!; - /// - /// Action which can be used to add additional Handlebars registrations. [Optional] - /// - [PublicAPI] - [JsonIgnore] - public Action? HandlebarsRegistrationCallback { get; set; } + /// + /// Action which can be used to add additional Handlebars registrations. [Optional] + /// + [PublicAPI] + [JsonIgnore] + public Action? HandlebarsRegistrationCallback { get; set; } - /// - /// Allow the usage of CSharpCodeMatcher (default is not allowed). - /// - [PublicAPI] - public bool? AllowCSharpCodeMatcher { get; set; } + /// + /// Allow the usage of CSharpCodeMatcher (default is not allowed). + /// + [PublicAPI] + public bool? AllowCSharpCodeMatcher { get; set; } - /// - /// Allow a Body for all HTTP Methods. (default set to false). - /// - [PublicAPI] - public bool? AllowBodyForAllHttpMethods { get; set; } + /// + /// Allow a Body for all HTTP Methods. (default set to false). + /// + [PublicAPI] + public bool? AllowBodyForAllHttpMethods { get; set; } - /// - /// Allow only a HttpStatus Code in the response which is defined. (default set to false). - /// - false : also null, 0, empty or invalid HttpStatus codes are allowed. - /// - true : only codes defined in are allowed. - /// - [PublicAPI] - public bool? AllowOnlyDefinedHttpStatusCodeInResponse { get; set; } + /// + /// Allow only a HttpStatus Code in the response which is defined. (default set to false). + /// - false : also null, 0, empty or invalid HttpStatus codes are allowed. + /// - true : only codes defined in are allowed. + /// + [PublicAPI] + public bool? AllowOnlyDefinedHttpStatusCodeInResponse { get; set; } - /// - /// Set to true to disable Json deserialization when processing requests. (default set to false). - /// - [PublicAPI] - public bool? DisableJsonBodyParsing { get; set; } + /// + /// Set to true to disable Json deserialization when processing requests. (default set to false). + /// + [PublicAPI] + public bool? DisableJsonBodyParsing { get; set; } - /// - /// Disable support for GZip and Deflate request body decompression. (default set to false). - /// - [PublicAPI] - public bool? DisableRequestBodyDecompressing { get; set; } + /// + /// Disable support for GZip and Deflate request body decompression. (default set to false). + /// + [PublicAPI] + public bool? DisableRequestBodyDecompressing { get; set; } - /// - /// Handle all requests synchronously. (default set to false). - /// - [PublicAPI] - public bool? HandleRequestsSynchronously { get; set; } + /// + /// Handle all requests synchronously. (default set to false). + /// + [PublicAPI] + public bool? HandleRequestsSynchronously { get; set; } - /// - /// Throw an exception when the fails because of invalid input. (default set to false). - /// - [PublicAPI] - public bool? ThrowExceptionWhenMatcherFails { get; set; } + /// + /// Throw an exception when the fails because of invalid input. (default set to false). + /// + [PublicAPI] + public bool? ThrowExceptionWhenMatcherFails { get; set; } - /// - /// If https is used, these settings can be used to configure the CertificateSettings in case a custom certificate instead the default .NET certificate should be used. - /// - /// X509StoreName and X509StoreLocation should be defined - /// OR - /// X509CertificateFilePath and X509CertificatePassword should be defined - /// - [PublicAPI] - public WireMockCertificateSettings? CertificateSettings { get; set; } + /// + /// If https is used, these settings can be used to configure the CertificateSettings in case a custom certificate instead the default .NET certificate should be used. + /// + /// X509StoreName and X509StoreLocation should be defined + /// OR + /// X509CertificateFilePath and X509CertificatePassword should be defined + /// + [PublicAPI] + public WireMockCertificateSettings? CertificateSettings { get; set; } - /// - /// Defines if custom CertificateSettings are defined - /// - [PublicAPI] - public bool CustomCertificateDefined => CertificateSettings?.IsDefined == true; + /// + /// Defines if custom CertificateSettings are defined + /// + [PublicAPI] + public bool CustomCertificateDefined => CertificateSettings?.IsDefined == true; - /// - /// Defines the global IWebhookSettings to use. - /// - [PublicAPI] - public WebhookSettings? WebhookSettings { get; set; } + /// + /// Defines the global IWebhookSettings to use. + /// + [PublicAPI] + public WebhookSettings? WebhookSettings { get; set; } - /// - /// Use the instead of the default (default set to true). - /// - [PublicAPI] - public bool? UseRegexExtended { get; set; } = true; + /// + /// Use the instead of the default (default set to true). + /// + [PublicAPI] + public bool? UseRegexExtended { get; set; } = true; - /// - /// Save unmatched requests to a file using the (default set to false). - /// - [PublicAPI] - public bool? SaveUnmatchedRequests { get; set; } + /// + /// Save unmatched requests to a file using the (default set to false). + /// + [PublicAPI] + public bool? SaveUnmatchedRequests { get; set; } - /// - /// Don't save the response-string in the LogEntry when WithBody(Func{IRequestMessage, string}) or WithBody(Func{IRequestMessage, Task{string}}) is used. (default set to false). - /// - [PublicAPI] - public bool? DoNotSaveDynamicResponseInLogEntry { get; set; } + /// + /// Don't save the response-string in the LogEntry when WithBody(Func{IRequestMessage, string}) or WithBody(Func{IRequestMessage, Task{string}}) is used. (default set to false). + /// + [PublicAPI] + public bool? DoNotSaveDynamicResponseInLogEntry { get; set; } - /// - /// See . - /// - /// Default value = "All". - /// - [PublicAPI] - public QueryParameterMultipleValueSupport? QueryParameterMultipleValueSupport { get; set; } + /// + /// See . + /// + /// Default value = "All". + /// + [PublicAPI] + public QueryParameterMultipleValueSupport? QueryParameterMultipleValueSupport { get; set; } - /// - /// Custom matcher mappings for static mappings - /// - [PublicAPI, JsonIgnore] - public IDictionary>? CustomMatcherMappings { get; set; } + /// + /// Custom matcher mappings for static mappings + /// + [PublicAPI, JsonIgnore] + public IDictionary>? CustomMatcherMappings { get; set; } - /// - /// The used when the a JSON response is generated. - /// - [PublicAPI, JsonIgnore] - public JsonSerializerSettings? JsonSerializerSettings { get; set; } - } + /// + /// The used when the a JSON response is generated. + /// + [PublicAPI, JsonIgnore] + public JsonSerializerSettings? JsonSerializerSettings { get; set; } + + /// + /// The Culture to use. + /// Currently used for: + /// - Handlebars Transformer + /// + public CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture; } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs b/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs index 7b917d77..dbbb2716 100644 --- a/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs +++ b/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs @@ -1,8 +1,11 @@ using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; using JetBrains.Annotations; using Stef.Validation; using WireMock.Logging; using WireMock.Types; +using WireMock.Util; namespace WireMock.Settings; @@ -55,7 +58,8 @@ public static class WireMockServerSettingsParser WatchStaticMappingsInSubdirectories = parser.GetBoolValue("WatchStaticMappingsInSubdirectories"), HostingScheme = parser.GetEnumValue(nameof(WireMockServerSettings.HostingScheme)), DoNotSaveDynamicResponseInLogEntry = parser.GetBoolValue(nameof(WireMockServerSettings.DoNotSaveDynamicResponseInLogEntry)), - QueryParameterMultipleValueSupport = parser.GetEnumValue(nameof(WireMockServerSettings.QueryParameterMultipleValueSupport)) + QueryParameterMultipleValueSupport = parser.GetEnumValue(nameof(WireMockServerSettings.QueryParameterMultipleValueSupport)), + Culture = parser.GetValue(nameof(WireMockServerSettings.Culture), strings => CultureInfoUtils.Parse(strings.FirstOrDefault()), CultureInfo.CurrentCulture) }; #if USE_ASPNETCORE diff --git a/src/WireMock.Net/Transformers/Handlebars/FileHelpers.cs b/src/WireMock.Net/Transformers/Handlebars/FileHelpers.cs index 65bfd990..f8ac2e56 100644 --- a/src/WireMock.Net/Transformers/Handlebars/FileHelpers.cs +++ b/src/WireMock.Net/Transformers/Handlebars/FileHelpers.cs @@ -1,27 +1,26 @@ -using System; using HandlebarsDotNet; using HandlebarsDotNet.Helpers.Attributes; using HandlebarsDotNet.Helpers.Enums; using HandlebarsDotNet.Helpers.Helpers; +using Stef.Validation; using WireMock.Handlers; -namespace WireMock.Transformers.Handlebars +namespace WireMock.Transformers.Handlebars; + +internal class FileHelpers : BaseHelpers, IHelpers { - internal class FileHelpers : BaseHelpers, IHelpers + private readonly IFileSystemHandler _fileSystemHandler; + + public FileHelpers(IHandlebars context, IFileSystemHandler fileSystemHandler) : base(context) { - private readonly IFileSystemHandler _fileSystemHandler; + _fileSystemHandler = Guard.NotNull(fileSystemHandler); + } - public FileHelpers(IHandlebars context, IFileSystemHandler fileSystemHandler) : base(context) - { - _fileSystemHandler = fileSystemHandler ?? throw new ArgumentNullException(nameof(fileSystemHandler)); - } - - [HandlebarsWriter(WriterType.String, usage: HelperUsage.Both, passContext: true, name: "File")] - public string Read(Context context, string path) - { - var templateFunc = Context.Compile(path); - string transformed = templateFunc(context.Value); - return _fileSystemHandler.ReadResponseBodyAsString(transformed); - } + [HandlebarsWriter(WriterType.String, usage: HelperUsage.Both, passContext: true, name: "File")] + public string Read(Context context, string path) + { + var templateFunc = Context.Compile(path); + string transformed = templateFunc(context.Value); + return _fileSystemHandler.ReadResponseBodyAsString(transformed); } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/Handlebars/HandlebarsContext.cs b/src/WireMock.Net/Transformers/Handlebars/HandlebarsContext.cs index 10af57ac..d84a40a0 100644 --- a/src/WireMock.Net/Transformers/Handlebars/HandlebarsContext.cs +++ b/src/WireMock.Net/Transformers/Handlebars/HandlebarsContext.cs @@ -1,18 +1,35 @@ using HandlebarsDotNet; +using HandlebarsDotNet.Helpers.Extensions; +using Stef.Validation; using WireMock.Handlers; -namespace WireMock.Transformers.Handlebars +namespace WireMock.Transformers.Handlebars; + +internal class HandlebarsContext : IHandlebarsContext { - internal class HandlebarsContext : IHandlebarsContext + public IHandlebars Handlebars { get; } + + public IFileSystemHandler FileSystemHandler { get; } + + public HandlebarsContext(IHandlebars handlebars, IFileSystemHandler fileSystemHandler) { - public IHandlebars Handlebars { get; set; } + Handlebars = Guard.NotNull(handlebars); + FileSystemHandler = Guard.NotNull(fileSystemHandler); + } - public IFileSystemHandler FileSystemHandler { get; set; } + public string ParseAndRender(string text, object model) + { + var template = Handlebars.Compile(text); + return template(model); + } - public string ParseAndRender(string text, object model) + public object? ParseAndEvaluate(string text, object model) + { + if (Handlebars.TryEvaluate(text, model, out var result) && result is not UndefinedBindingResult) { - var template = Handlebars.Compile(text); - return template(model); + return result; } + + return ParseAndRender(text, model); } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/Handlebars/HandlebarsContextFactory.cs b/src/WireMock.Net/Transformers/Handlebars/HandlebarsContextFactory.cs index d81910aa..631e53fc 100644 --- a/src/WireMock.Net/Transformers/Handlebars/HandlebarsContextFactory.cs +++ b/src/WireMock.Net/Transformers/Handlebars/HandlebarsContextFactory.cs @@ -1,33 +1,30 @@ -using System; using HandlebarsDotNet; using Stef.Validation; -using WireMock.Handlers; +using WireMock.Settings; namespace WireMock.Transformers.Handlebars; internal class HandlebarsContextFactory : ITransformerContextFactory { - private readonly IFileSystemHandler _fileSystemHandler; - private readonly Action? _action; + private readonly WireMockServerSettings _settings; - public HandlebarsContextFactory(IFileSystemHandler fileSystemHandler, Action? action) + public HandlebarsContextFactory(WireMockServerSettings settings) { - _fileSystemHandler = Guard.NotNull(fileSystemHandler); - _action = action; + _settings = Guard.NotNull(settings); } public ITransformerContext Create() { - var handlebars = HandlebarsDotNet.Handlebars.Create(); - - WireMockHandlebarsHelpers.Register(handlebars, _fileSystemHandler); - - _action?.Invoke(handlebars, _fileSystemHandler); - - return new HandlebarsContext + var config = new HandlebarsConfiguration { - Handlebars = handlebars, - FileSystemHandler = _fileSystemHandler + FormatProvider = _settings.Culture }; + var handlebars = HandlebarsDotNet.Handlebars.Create(config); + + WireMockHandlebarsHelpers.Register(handlebars, _settings.FileSystemHandler); + + _settings.HandlebarsRegistrationCallback?.Invoke(handlebars, _settings.FileSystemHandler); + + return new HandlebarsContext(handlebars, _settings.FileSystemHandler); } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/Handlebars/IHandlebarsContext.cs b/src/WireMock.Net/Transformers/Handlebars/IHandlebarsContext.cs index e96b48bd..964290f8 100644 --- a/src/WireMock.Net/Transformers/Handlebars/IHandlebarsContext.cs +++ b/src/WireMock.Net/Transformers/Handlebars/IHandlebarsContext.cs @@ -1,9 +1,8 @@ -using HandlebarsDotNet; +using HandlebarsDotNet; -namespace WireMock.Transformers.Handlebars +namespace WireMock.Transformers.Handlebars; + +interface IHandlebarsContext : ITransformerContext { - interface IHandlebarsContext : ITransformerContext - { - IHandlebars Handlebars { get; set; } - } + IHandlebars Handlebars { get; } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/ITransformerContext.cs b/src/WireMock.Net/Transformers/ITransformerContext.cs index 1ddf0816..67bfd74f 100644 --- a/src/WireMock.Net/Transformers/ITransformerContext.cs +++ b/src/WireMock.Net/Transformers/ITransformerContext.cs @@ -1,11 +1,12 @@ -using WireMock.Handlers; +using WireMock.Handlers; -namespace WireMock.Transformers +namespace WireMock.Transformers; + +internal interface ITransformerContext { - interface ITransformerContext - { - IFileSystemHandler FileSystemHandler { get; set; } + IFileSystemHandler FileSystemHandler { get; } - string ParseAndRender(string text, object model); - } + string ParseAndRender(string text, object model); + + object? ParseAndEvaluate(string text, object model); } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/Scriban/ScribanContext.cs b/src/WireMock.Net/Transformers/Scriban/ScribanContext.cs index 4c33b783..aee169de 100644 --- a/src/WireMock.Net/Transformers/Scriban/ScribanContext.cs +++ b/src/WireMock.Net/Transformers/Scriban/ScribanContext.cs @@ -3,25 +3,30 @@ using Stef.Validation; using WireMock.Handlers; using WireMock.Types; -namespace WireMock.Transformers.Scriban +namespace WireMock.Transformers.Scriban; + +internal class ScribanContext : ITransformerContext { - internal class ScribanContext : ITransformerContext + private readonly TransformerType _transformerType; + + public IFileSystemHandler FileSystemHandler { get; } + + public ScribanContext(IFileSystemHandler fileSystemHandler, TransformerType transformerType) { - private readonly TransformerType _transformerType; + FileSystemHandler = Guard.NotNull(fileSystemHandler); + _transformerType = transformerType; + } - public IFileSystemHandler FileSystemHandler { get; set; } + public string ParseAndRender(string text, object model) + { + var template = _transformerType == TransformerType.ScribanDotLiquid ? Template.ParseLiquid(text) : Template.Parse(text); - public ScribanContext(IFileSystemHandler fileSystemHandler, TransformerType transformerType) - { - FileSystemHandler = Guard.NotNull(fileSystemHandler); - _transformerType = transformerType; - } + return template.Render(model, member => member.Name); + } - public string ParseAndRender(string text, object model) - { - var template = _transformerType == TransformerType.ScribanDotLiquid ? Template.ParseLiquid(text) : Template.Parse(text); - - return template.Render(model, member => member.Name); - } + public object? ParseAndEvaluate(string text, object model) + { + // In case of Scriban, call ParseAndRender. + return ParseAndRender(text, model); } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/Scriban/ScribanContextFactory.cs b/src/WireMock.Net/Transformers/Scriban/ScribanContextFactory.cs index 6649deb7..15c93e04 100644 --- a/src/WireMock.Net/Transformers/Scriban/ScribanContextFactory.cs +++ b/src/WireMock.Net/Transformers/Scriban/ScribanContextFactory.cs @@ -2,22 +2,21 @@ using WireMock.Handlers; using WireMock.Types; using Stef.Validation; -namespace WireMock.Transformers.Scriban +namespace WireMock.Transformers.Scriban; + +internal class ScribanContextFactory : ITransformerContextFactory { - internal class ScribanContextFactory : ITransformerContextFactory + private readonly IFileSystemHandler _fileSystemHandler; + private readonly TransformerType _transformerType; + + public ScribanContextFactory(IFileSystemHandler fileSystemHandler, TransformerType transformerType) { - private readonly IFileSystemHandler _fileSystemHandler; - private readonly TransformerType _transformerType; + _fileSystemHandler = Guard.NotNull(fileSystemHandler); + _transformerType = Guard.Condition(transformerType, t => t is TransformerType.Scriban or TransformerType.ScribanDotLiquid); + } - public ScribanContextFactory(IFileSystemHandler fileSystemHandler, TransformerType transformerType) - { - _fileSystemHandler = Guard.NotNull(fileSystemHandler); - _transformerType = Guard.Condition(transformerType, t => t == TransformerType.Scriban || t == TransformerType.ScribanDotLiquid); - } - - public ITransformerContext Create() - { - return new ScribanContext(_fileSystemHandler, _transformerType); - } + public ITransformerContext Create() + { + return new ScribanContext(_fileSystemHandler, _transformerType); } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/Transformer.cs b/src/WireMock.Net/Transformers/Transformer.cs index cddf0b3f..4c47ad1b 100644 --- a/src/WireMock.Net/Transformers/Transformer.cs +++ b/src/WireMock.Net/Transformers/Transformer.cs @@ -1,9 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Linq; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Stef.Validation; -using System; -using System.Collections.Generic; -using System.Linq; +using WireMock.Settings; using WireMock.Types; using WireMock.Util; @@ -11,11 +13,19 @@ namespace WireMock.Transformers; internal class Transformer : ITransformer { + private static readonly Type[] SupportedTypes = { typeof(bool), typeof(long), typeof(int), typeof(double), typeof(Guid), typeof(DateTime), typeof(TimeSpan), typeof(Uri) }; + + private readonly JsonSerializer _jsonSerializer; private readonly ITransformerContextFactory _factory; - public Transformer(ITransformerContextFactory factory) + public Transformer(WireMockServerSettings settings, ITransformerContextFactory factory) { _factory = Guard.NotNull(factory); + + _jsonSerializer = new JsonSerializer + { + Culture = Guard.NotNull(settings).Culture + }; } public IBodyData? TransformBody( @@ -109,7 +119,7 @@ internal class Transformer : ITransformer }); } - private static IBodyData? TransformBodyData(ITransformerContext transformerContext, ReplaceNodeOptions options, TransformModel model, IBodyData original, bool useTransformerForBodyAsFile) + private IBodyData? TransformBodyData(ITransformerContext transformerContext, ReplaceNodeOptions options, TransformModel model, IBodyData original, bool useTransformerForBodyAsFile) { return original.DetectedBodyType switch { @@ -139,33 +149,33 @@ internal class Transformer : ITransformer return newHeaders; } - private static IBodyData TransformBodyAsJson(ITransformerContext handlebarsContext, ReplaceNodeOptions options, object model, IBodyData original) + private IBodyData TransformBodyAsJson(ITransformerContext transformerContext, ReplaceNodeOptions options, object model, IBodyData original) { JToken? jToken = null; switch (original.BodyAsJson) { case JObject bodyAsJObject: jToken = bodyAsJObject.DeepClone(); - WalkNode(handlebarsContext, options, jToken, model); + WalkNode(transformerContext, options, jToken, model); break; case JArray bodyAsJArray: jToken = bodyAsJArray.DeepClone(); - WalkNode(handlebarsContext, options, jToken, model); + WalkNode(transformerContext, options, jToken, model); break; case Array bodyAsArray: - jToken = JArray.FromObject(bodyAsArray); - WalkNode(handlebarsContext, options, jToken, model); + jToken = JArray.FromObject(bodyAsArray, _jsonSerializer); + WalkNode(transformerContext, options, jToken, model); break; case string bodyAsString: - jToken = ReplaceSingleNode(handlebarsContext, options, bodyAsString, model); + jToken = ReplaceSingleNode(transformerContext, options, bodyAsString, model); break; case not null: - jToken = JObject.FromObject(original.BodyAsJson); - WalkNode(handlebarsContext, options, jToken, model); + jToken = JObject.FromObject(original.BodyAsJson, _jsonSerializer); + WalkNode(transformerContext, options, jToken, model); break; } @@ -178,9 +188,9 @@ internal class Transformer : ITransformer }; } - private static JToken ReplaceSingleNode(ITransformerContext handlebarsContext, ReplaceNodeOptions options, string stringValue, object model) + private JToken ReplaceSingleNode(ITransformerContext transformerContext, ReplaceNodeOptions options, string stringValue, object model) { - string transformedString = handlebarsContext.ParseAndRender(stringValue, model); + string transformedString = transformerContext.ParseAndRender(stringValue, model); if (!string.Equals(stringValue, transformedString)) { @@ -202,7 +212,7 @@ internal class Transformer : ITransformer return stringValue; } - private static void WalkNode(ITransformerContext handlebarsContext, ReplaceNodeOptions options, JToken node, object model) + private void WalkNode(ITransformerContext transformerContext, ReplaceNodeOptions options, JToken node, object model) { switch (node.Type) { @@ -210,7 +220,7 @@ internal class Transformer : ITransformer // 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); + WalkNode(transformerContext, options, child.Value, model); } break; @@ -218,7 +228,7 @@ internal class Transformer : ITransformer // 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); + WalkNode(transformerContext, options, child, model); } break; @@ -230,8 +240,8 @@ internal class Transformer : ITransformer return; } - string transformed = handlebarsContext.ParseAndRender(stringValue!, model); - if (!string.Equals(stringValue, transformed)) + var transformed = transformerContext.ParseAndEvaluate(stringValue, model); + if (!Equals(stringValue, transformed)) { ReplaceNodeValue(options, node, transformed); } @@ -240,44 +250,88 @@ internal class Transformer : ITransformer } // ReSharper disable once UnusedParameter.Local - private static void ReplaceNodeValue(ReplaceNodeOptions options, JToken node, string transformedString) + private void ReplaceNodeValue(ReplaceNodeOptions options, JToken node, object? transformedValue) { - StringUtils.TryParseQuotedString(transformedString, out var result, out _); - if (bool.TryParse(result, out var valueAsBoolean) || bool.TryParse(transformedString, out valueAsBoolean)) + switch (transformedValue) { - node.Replace(valueAsBoolean); - return; - } + case JValue jValue: + node.Replace(jValue); + return; - JToken value; - try - { - // Try to convert this string into a JsonObject - value = JToken.Parse(transformedString); - } - catch (JsonException) - { - // Ignore JsonException and just keep string value and convert to JToken - value = transformedString; - } + case string transformedString: + if (TryConvert(transformedString, out var convertedFromStringValue)) + { + node.Replace(JToken.FromObject(convertedFromStringValue, _jsonSerializer)); + } + else + { + node.Replace(ParseAsJObject(transformedString)); + } + break; - node.Replace(value); + case WireMockList strings: + switch (strings.Count) + { + case 1: + node.Replace(ParseAsJObject(strings[0])); + return; + + case > 1: + node.Replace(JToken.FromObject(strings.ToArray(), _jsonSerializer)); + return; + } + break; + + case { }: + if (TryConvert(transformedValue, out var convertedValue)) + { + node.Replace(JToken.FromObject(convertedValue, _jsonSerializer)); + } + return; + + default: // It's null, skip it. Maybe remove it ? + return; + } } - private static IBodyData TransformBodyAsString(ITransformerContext handlebarsContext, object model, IBodyData original) + private static JToken ParseAsJObject(string stringValue) + { + return JsonUtils.TryParseAsJObject(stringValue, out var parsedAsjObject) ? parsedAsjObject : stringValue; + } + + private static bool TryConvert(object? transformedValue, [NotNullWhen(true)] out object? convertedValue) + { + foreach (var supportedType in SupportedTypes) + { + try + { + convertedValue = Convert.ChangeType(transformedValue, supportedType); + return convertedValue is not null; + } + catch + { + // Ignore + } + } + + convertedValue = null; + return false; + } + + private static IBodyData TransformBodyAsString(ITransformerContext transformerContext, object model, IBodyData original) { return new BodyData { Encoding = original.Encoding, DetectedBodyType = original.DetectedBodyType, DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, - BodyAsString = handlebarsContext.ParseAndRender(original.BodyAsString!, model) + BodyAsString = transformerContext.ParseAndRender(original.BodyAsString!, model) }; } - private static IBodyData TransformBodyAsFile(ITransformerContext handlebarsContext, object model, IBodyData original, bool useTransformerForBodyAsFile) + private static IBodyData TransformBodyAsFile(ITransformerContext transformerContext, object model, IBodyData original, bool useTransformerForBodyAsFile) { - string transformedBodyAsFilename = handlebarsContext.ParseAndRender(original.BodyAsFile!, model); + string transformedBodyAsFilename = transformerContext.ParseAndRender(original.BodyAsFile!, model); if (!useTransformerForBodyAsFile) { @@ -289,12 +343,12 @@ internal class Transformer : ITransformer }; } - string text = handlebarsContext.FileSystemHandler.ReadResponseBodyAsString(transformedBodyAsFilename); + string text = transformerContext.FileSystemHandler.ReadResponseBodyAsString(transformedBodyAsFilename); return new BodyData { DetectedBodyType = BodyType.String, DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, - BodyAsString = handlebarsContext.ParseAndRender(text, model), + BodyAsString = transformerContext.ParseAndRender(text, model), BodyAsFile = transformedBodyAsFilename }; } diff --git a/src/WireMock.Net/Util/CultureInfoExtensions.cs b/src/WireMock.Net/Util/CultureInfoExtensions.cs new file mode 100644 index 00000000..086c153e --- /dev/null +++ b/src/WireMock.Net/Util/CultureInfoExtensions.cs @@ -0,0 +1,40 @@ +using System; +using System.Globalization; + +namespace WireMock.Util; + +internal static class CultureInfoUtils +{ + public static CultureInfo Parse(string? value) + { + if (value is null) + { + return CultureInfo.CurrentCulture; + } + + try + { +#if !NETSTANDARD1_3 + if (int.TryParse(value, out var culture)) + { + return new CultureInfo(culture); + } +#endif + if (string.Equals(value, nameof(CultureInfo.CurrentCulture), StringComparison.OrdinalIgnoreCase)) + { + return CultureInfo.CurrentCulture; + } + + if (string.Equals(value, nameof(CultureInfo.InvariantCulture), StringComparison.OrdinalIgnoreCase)) + { + return CultureInfo.InvariantCulture; + } + + return new CultureInfo(value); + } + catch + { + return CultureInfo.CurrentCulture; + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index 50b43e3c..c21c3bd1 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -87,8 +87,8 @@ - all - runtime; build; native; contentfiles; analyzers; buildtransitive + all + runtime; build; native; contentfiles; analyzers; buildtransitive @@ -163,13 +163,13 @@ - - - - - - - + + + + + + + diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsJsonPathTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsJsonPathTests.cs index 2409df88..04ee3d3d 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsJsonPathTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsJsonPathTests.cs @@ -108,7 +108,7 @@ public class ResponseWithHandlebarsJsonPathTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - JObject j = JObject.FromObject(response.Message.BodyData.BodyAsJson); + JObject j = JObject.FromObject(response.Message.BodyData.BodyAsJson!); Check.That(j["x"].Value()).Equals(99); } diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs index df434d0c..a90b2c83 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithHandlebarsRandomTests.cs @@ -77,7 +77,7 @@ public class ResponseWithHandlebarsRandomTests } [Theory] - [InlineData(ReplaceNodeOptions.None, JTokenType.Integer)] + [InlineData(ReplaceNodeOptions.Evaluate, JTokenType.Integer)] //[InlineData(ReplaceNodeOptions.Bool, JTokenType.String)] //[InlineData(ReplaceNodeOptions.Integer, JTokenType.Integer)] //[InlineData(ReplaceNodeOptions.Bool | ReplaceNodeOptions.Integer, JTokenType.Integer)] diff --git a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs index 17f28353..b7276682 100644 --- a/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs +++ b/test/WireMock.Net.Tests/ResponseBuilders/ResponseWithTransformerTests.cs @@ -14,6 +14,8 @@ using WireMock.Settings; using WireMock.Types; using WireMock.Util; using Xunit; +using System.Globalization; +using CultureAwareTesting.xUnit; #if NET452 using Microsoft.Owin; #else @@ -24,6 +26,7 @@ namespace WireMock.Net.Tests.ResponseBuilders; public class ResponseWithTransformerTests { + private readonly Mock _filesystemHandlerMock; private readonly WireMockServerSettings _settings = new(); private const string ClientIp = "::1"; @@ -33,10 +36,10 @@ public class ResponseWithTransformerTests { _mappingMock = new Mock(); - var filesystemHandlerMock = new Mock(MockBehavior.Strict); - filesystemHandlerMock.Setup(fs => fs.ReadResponseBodyAsString(It.IsAny())).Returns("abc"); + _filesystemHandlerMock = new Mock(MockBehavior.Strict); + _filesystemHandlerMock.Setup(fs => fs.ReadResponseBodyAsString(It.IsAny())).Returns("abc"); - _settings.FileSystemHandler = filesystemHandlerMock.Object; + _settings.FileSystemHandler = _filesystemHandlerMock.Object; } [Theory] @@ -333,7 +336,7 @@ public class ResponseWithTransformerTests // Assert Check.That(response.Message.BodyData!.BodyAsString).Equals("test"); Check.That(response.Message.Headers).ContainsKey("x"); - Check.That(response.Message.Headers["x"]).Contains("text/plain"); + Check.That(response.Message.Headers!["x"]).Contains("text/plain"); Check.That(response.Message.Headers["x"]).Contains("http://localhost/foo"); } @@ -358,7 +361,7 @@ public class ResponseWithTransformerTests // Assert Check.That(response.Message.BodyData!.BodyAsString).Equals("test"); Check.That(response.Message.Headers).ContainsKey("x"); - Check.That(response.Message.Headers["x"]).Contains("text/plain"); + Check.That(response.Message.Headers!["x"]).Contains("text/plain"); Check.That(response.Message.Headers["x"]).Contains("http://localhost/foo"); } @@ -394,7 +397,7 @@ public class ResponseWithTransformerTests public async Task Response_ProvideResponse_Transformer_WithBodyAsJson_ResultAsObject(TransformerType transformerType) { // Assign - string jsonString = "{ \"things\": [ { \"name\": \"RequiredThing\" }, { \"name\": \"WireMock\" } ] }"; + string jsonString = "{ \"id\": 42, \"things\": [ { \"name\": \"RequiredThing\" }, { \"name\": \"WireMock\" } ] }"; var bodyData = new BodyData { BodyAsJson = JsonConvert.DeserializeObject(jsonString), @@ -411,7 +414,49 @@ public class ResponseWithTransformerTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - Check.That(JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson)).Equals("{\"x\":\"test /foo_object\"}"); + Check.That(JsonConvert.SerializeObject(response.Message.BodyData!.BodyAsJson)).Equals("{\"x\":\"test /foo_object\"}"); + } + + [CulturedTheory("en-US")] + [InlineData(TransformerType.Handlebars, "{ \"id\": 42 }", "{\"x\":\"test 42\",\"y\":42}")] + [InlineData(TransformerType.Scriban, "{ \"id\": 42 }", "{\"x\":\"test 42\",\"y\":42}")] + [InlineData(TransformerType.ScribanDotLiquid, "{ \"id\": 42 }", "{\"x\":\"test 42\",\"y\":42}")] + [InlineData(TransformerType.Handlebars, "{ \"id\": true }", "{\"x\":\"test True\",\"y\":true}")] + [InlineData(TransformerType.Scriban, "{ \"id\": true }", "{\"x\":\"test True\",\"y\":true}")] + [InlineData(TransformerType.ScribanDotLiquid, "{ \"id\": true }", "{\"x\":\"test True\",\"y\":true}")] + [InlineData(TransformerType.Handlebars, "{ \"id\": 0.005 }", "{\"x\":\"test 0.005\",\"y\":0.005}")] + [InlineData(TransformerType.Scriban, "{ \"id\": 0.005 }", "{\"x\":\"test 0.005\",\"y\":0.005}")] + [InlineData(TransformerType.ScribanDotLiquid, "{ \"id\": 0.005 }", "{\"x\":\"test 0.005\",\"y\":0.005}")] + public async Task Response_ProvideResponse_Transformer_WithBodyAsJson_KeepType(TransformerType transformerType, string jsonString, string expected) + { + // Assign + var culture = CultureInfo.CreateSpecificCulture("en-US"); + var settings = new WireMockServerSettings + { + FileSystemHandler = _filesystemHandlerMock.Object, + Culture = culture + }; + var jsonSettings = new JsonSerializerSettings + { + Culture = culture + }; + var bodyData = new BodyData + { + BodyAsJson = JsonConvert.DeserializeObject(jsonString, jsonSettings), + 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 { x = "test {{request.BodyAsJson.id}}", y = "{{request.BodyAsJson.id}}" }) + .WithTransformer(transformerType); + + // Act + var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, settings).ConfigureAwait(false); + + // Assert + JsonConvert.SerializeObject(response.Message.BodyData!.BodyAsJson).Should().Be(expected); } [Theory] @@ -431,7 +476,7 @@ public class ResponseWithTransformerTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson).Should().Be("[{\"x\":\"test\"}]"); + JsonConvert.SerializeObject(response.Message.BodyData!.BodyAsJson).Should().Be("[{\"x\":\"test\"}]"); } [Theory] @@ -452,7 +497,7 @@ public class ResponseWithTransformerTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson).Should().Be("[{\"x\":\"test\"}]"); + JsonConvert.SerializeObject(response.Message.BodyData!.BodyAsJson).Should().Be("[{\"x\":\"test\"}]"); } //[Theory] @@ -488,15 +533,15 @@ public class ResponseWithTransformerTests [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, "false", "false")] - [InlineData(TransformerType.Handlebars, "\"true\"", "true")] // bool is special + [InlineData(TransformerType.Handlebars, "\"true\"", "\"true\"")] [InlineData(TransformerType.Handlebars, "true", "true")] - [InlineData(TransformerType.Handlebars, "\"-42\"", "-42")] // todo + [InlineData(TransformerType.Handlebars, "\"-42\"", "\"-42\"")] [InlineData(TransformerType.Handlebars, "-42", "-42")] - [InlineData(TransformerType.Handlebars, "\"2147483647\"", "2147483647")] // todo + [InlineData(TransformerType.Handlebars, "\"2147483647\"", "\"2147483647\"")] [InlineData(TransformerType.Handlebars, "2147483647", "2147483647")] - [InlineData(TransformerType.Handlebars, "\"9223372036854775807\"", "9223372036854775807")] // todo + [InlineData(TransformerType.Handlebars, "\"9223372036854775807\"", "\"9223372036854775807\"")] [InlineData(TransformerType.Handlebars, "9223372036854775807", "9223372036854775807")] public async Task Response_ProvideResponse_Transformer_WithBodyAsJson_And_ReplaceNodeOptionsKeep(TransformerType transformerType, string value, string expected) { @@ -511,50 +556,13 @@ public class ResponseWithTransformerTests var responseBuilder = Response.Create() .WithBodyAsJson(new { text = "{{request.bodyAsJson.x}}" }) - .WithTransformer(transformerType, false, ReplaceNodeOptions.None); + .WithTransformer(transformerType, false, ReplaceNodeOptions.Evaluate); // Act var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, 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(_mappingMock.Object, request, _settings).ConfigureAwait(false); - - // Assert - JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson).Should().Be($"{{\"text\":{expected}}}"); + JsonConvert.SerializeObject(response.Message.BodyData!.BodyAsJson).Should().Be($"{{\"text\":{expected}}}"); } [Theory] @@ -581,7 +589,7 @@ public class ResponseWithTransformerTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - Check.That(JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson)).Equals("[\"first\",\"/foo_array\",\"test 1\",\"test 2\",\"last\"]"); + Check.That(JsonConvert.SerializeObject(response.Message.BodyData!.BodyAsJson)).Equals("[\"first\",\"/foo_array\",\"test 1\",\"test 2\",\"last\"]"); } [Fact] @@ -598,7 +606,7 @@ public class ResponseWithTransformerTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - Check.That(response.Message.BodyData.BodyAsFile).Equals(@"c:\1\test.xml"); + Check.That(response.Message.BodyData!.BodyAsFile).Equals(@"c:\1\test.xml"); } [Theory(Skip = @"Does not work in Scriban --> c:\\[""1""]\\test.xml")] @@ -617,7 +625,7 @@ public class ResponseWithTransformerTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - Check.That(response.Message.BodyData.BodyAsFile).Equals(@"c:\1\test.xml"); + Check.That(response.Message.BodyData!.BodyAsFile).Equals(@"c:\1\test.xml"); } [Theory] @@ -642,7 +650,7 @@ public class ResponseWithTransformerTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - Check.That(response.Message.BodyData.BodyAsFile).Equals(@"c:\1\test.xml"); + Check.That(response.Message.BodyData!.BodyAsFile).Equals(@"c:\1\test.xml"); Check.That(response.Message.BodyData.DetectedBodyType).Equals(BodyType.String); Check.That(response.Message.BodyData!.BodyAsString).Equals(""); } @@ -671,14 +679,15 @@ public class ResponseWithTransformerTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - Check.That(JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson)).Equals("\"test\""); + Check.That(JsonConvert.SerializeObject(response.Message.BodyData!.BodyAsJson)).Equals("\"test\""); } [Fact(Skip = "todo...")] + //[Fact] public async Task Response_ProvideResponse_Handlebars_WithBodyAsJson_ResultAsTemplatedString() { // Assign - string jsonString = "{ \"name\": \"WireMock\" }"; + string jsonString = "{ \"name\": \"WireMock\", \"id\": 12345 }"; var bodyData = new BodyData { BodyAsJson = JsonConvert.DeserializeObject(jsonString), @@ -688,14 +697,14 @@ public class ResponseWithTransformerTests var request = new RequestMessage(new UrlDetails("http://localhost/foo_object"), "POST", ClientIp, bodyData); var responseBuilder = Response.Create() - .WithBodyAsJson("{{{request.BodyAsJson}}}") + .WithBodyAsJson("{{{request.BodyAsJson.name}}}") .WithTransformer(); // Act var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - Check.That(JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson)).Equals("{\"name\":\"WireMock\"}"); + Check.That(JsonConvert.SerializeObject(response.Message.BodyData!.BodyAsJson)).Equals("{\"name\":\"WireMock\"}"); } [Theory(Skip = "{{{ }}} Does not work in Scriban")] @@ -721,7 +730,7 @@ public class ResponseWithTransformerTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - Check.That(JsonConvert.SerializeObject(response.Message.BodyData.BodyAsJson)).Equals("{\"name\":\"WireMock\"}"); + Check.That(JsonConvert.SerializeObject(response.Message.BodyData!.BodyAsJson)).Equals("{\"name\":\"WireMock\"}"); } [Theory] @@ -749,7 +758,7 @@ public class ResponseWithTransformerTests var response = await responseBuilder.ProvideResponseAsync(_mappingMock.Object, request, _settings).ConfigureAwait(false); // Assert - response.Message.BodyData.BodyAsString.Should().Be(text); + response.Message.BodyData!.BodyAsString.Should().Be(text); response.Message.BodyData.Encoding.Should().Be(enc); } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Transformers/Handlebars/HandlebarsContextFactoryTests.cs b/test/WireMock.Net.Tests/Transformers/Handlebars/HandlebarsContextFactoryTests.cs index 8de3952c..05b180c0 100644 --- a/test/WireMock.Net.Tests/Transformers/Handlebars/HandlebarsContextFactoryTests.cs +++ b/test/WireMock.Net.Tests/Transformers/Handlebars/HandlebarsContextFactoryTests.cs @@ -1,52 +1,59 @@ -using System; using FluentAssertions; -using HandlebarsDotNet; using Moq; using WireMock.Handlers; +using WireMock.Settings; using WireMock.Transformers.Handlebars; using Xunit; -namespace WireMock.Net.Tests.Transformers.Handlebars +namespace WireMock.Net.Tests.Transformers.Handlebars; + +public class HandlebarsContextFactoryTests { - public class HandlebarsContextFactoryTests + private readonly Mock _fileSystemHandlerMock = new(); + + [Fact] + public void Create_WithNullAction_DoesNotInvokeAction() { - private readonly Mock _fileSystemHandlerMock = new Mock(); - - [Fact] - public void Create_WithNullAction_DoesNotInvokeAction() + // Arrange + var settings = new WireMockServerSettings { - // Arrange - var sut = new HandlebarsContextFactory(_fileSystemHandlerMock.Object, null); + FileSystemHandler = _fileSystemHandlerMock.Object + }; + var sut = new HandlebarsContextFactory(settings); - // Act - var result = sut.Create(); + // Act + var result = sut.Create(); - // Assert - result.Should().NotBeNull(); - } + // Assert + result.Should().NotBeNull(); + } - [Fact] - public void Create_WithAction_InvokesAction() + [Fact] + public void Create_WithAction_InvokesAction() + { + // Arrange + int num = 0; + var settings = new WireMockServerSettings { - // Arrange - int num = 0; - Action action = (ctx, fs) => + FileSystemHandler = _fileSystemHandlerMock.Object, + HandlebarsRegistrationCallback = (ctx, fs) => { ctx.Should().NotBeNull(); fs.Should().NotBeNull(); num++; - }; - var sut = new HandlebarsContextFactory(_fileSystemHandlerMock.Object, action); + } + }; - // Act - var result = sut.Create(); + var sut = new HandlebarsContextFactory(settings); - // Assert - result.Should().NotBeNull(); + // Act + var result = sut.Create(); - // Verify - num.Should().Be(1); - } + // Assert + result.Should().NotBeNull(); + + // Verify + num.Should().Be(1); } -} +} \ 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 22a5738c..baabaa95 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -20,6 +20,10 @@ true + + NETFRAMEWORK + + @@ -35,8 +39,10 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive