diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 000000000..15dfda8c1 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,5 @@ +# Copilot Instructions + +## Project Guidelines +- When running tests in this workspace, do not run tests for the net48 target framework. +- When changing System.Text.Json code in this workspace, verify API availability for netstandard2.0 and netstandard2.1 instead of assuming newer APIs exist. \ No newline at end of file diff --git a/README.md b/README.md index d3e86b6ac..b64b40b15 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,7 @@ A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) w | | | | |   **WireMock.Net.Extensions.Routing** | [![NuGet Badge WireMock.Net.Extensions.Routing](https://img.shields.io/nuget/v/WireMock.Net.Extensions.Routing)](https://www.nuget.org/packages/WireMock.Net.Extensions.Routing) | [![MyGet Badge WireMock.Net.Extensions.Routing](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Extensions.Routing?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Extensions.Routing) |   **WireMock.Net.Matchers.CSharpCode** | [![NuGet Badge WireMock.Net.Matchers.CSharpCode](https://img.shields.io/nuget/v/WireMock.Net.Matchers.CSharpCode)](https://www.nuget.org/packages/WireMock.Net.Matchers.CSharpCode) | [![MyGet Badge WireMock.Net.Matchers.CSharpCode](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Matchers.CSharpCode?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Matchers.CSharpCode) +|   **WireMock.Net.Matchers.SystemTextJsonPath** | [![NuGet Badge WireMock.Net.Matchers.SystemTextJsonPath](https://img.shields.io/nuget/v/WireMock.Net.Matchers.SystemTextJsonPath)](https://www.nuget.org/packages/WireMock.Net.Matchers.SystemTextJsonPath) | [![MyGet Badge WireMock.Net.Matchers.SystemTextJsonPath](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.Matchers.SystemTextJsonPath?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.Matchers.SystemTextJsonPath) |   **WireMock.Net.OpenApiParser** | [![NuGet Badge WireMock.Net.OpenApiParser](https://img.shields.io/nuget/v/WireMock.Net.OpenApiParser)](https://www.nuget.org/packages/WireMock.Net.OpenApiParser) | [![MyGet Badge WireMock.Net.OpenApiParser](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.OpenApiParser?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.OpenApiParser) |   **WireMock.Net.MimePart** | [![NuGet Badge WireMock.Net.MimePart](https://img.shields.io/nuget/v/WireMock.Net.MimePart)](https://www.nuget.org/packages/WireMock.Net.MimePart) | [![MyGet Badge WireMock.Net.MimePart](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.MimePart?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.MimePart) |   **WireMock.Net.GraphQL** | [![NuGet Badge WireMock.Net.GraphQL](https://img.shields.io/nuget/v/WireMock.Net.GraphQL)](https://www.nuget.org/packages/WireMock.Net.GraphQL) | [![MyGet Badge WireMock.Net.GraphQL](https://img.shields.io/myget/wiremock-net/vpre/WireMock.Net.GraphQL?includePreReleases=true&label=MyGet)](https://www.myget.org/feed/wiremock-net/package/nuget/WireMock.Net.GraphQL) @@ -76,7 +77,7 @@ A C# .NET version based on [mock4net](https://github.com/alexvictoor/mock4net) w
-🔺 **WireMock.Net.Minimal** does not include *WireMock.Net.MimePart*, *WireMock.Net.GraphQL*, *WireMock.Net.ProtoBuf* and *WireMock.Net.OpenTelemetry*. +🔺 **WireMock.Net.Minimal** does not include *WireMock.Net.MimePart*, *WireMock.Net.GraphQL*, *WireMock.Net.ProtoBuf*, *WireMock.Net.OpenTelemetry* and *WireMock.Net.Matchers.SystemTextJsonPath*. --- diff --git a/WireMock.Net Solution.sln b/WireMock.Net Solution.sln index 6968b4034..3058ff0c9 100644 --- a/WireMock.Net Solution.sln +++ b/WireMock.Net Solution.sln @@ -1,4 +1,3 @@ - Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 18 VisualStudioVersion = 18.0.11205.157 @@ -150,6 +149,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.TestWebApplica EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.RestClient.AwesomeAssertions", "src\WireMock.Net.RestClient.AwesomeAssertions\WireMock.Net.RestClient.AwesomeAssertions.csproj", "{F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Matchers.SystemTextJsonPath", "src\WireMock.Net.Matchers.SystemTextJsonPath\WireMock.Net.Matchers.SystemTextJsonPath.csproj", "{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}" +EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WireMock.Net.Service", "examples\WireMock.Net.Service\WireMock.Net.Service.csproj", "{7F0B2446-0363-4720-AF46-F47F83B557DC}" EndProject Global @@ -822,6 +823,18 @@ Global {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x64.Build.0 = Release|Any CPU {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x86.ActiveCfg = Release|Any CPU {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A}.Release|x86.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|Any CPU.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x64.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.ActiveCfg = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Debug|x86.Build.0 = Debug|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|Any CPU.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x64.Build.0 = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.ActiveCfg = Release|Any CPU + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890}.Release|x86.Build.0 = Release|Any CPU {7F0B2446-0363-4720-AF46-F47F83B557DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7F0B2446-0363-4720-AF46-F47F83B557DC}.Debug|Any CPU.Build.0 = Debug|Any CPU {7F0B2446-0363-4720-AF46-F47F83B557DC}.Debug|x64.ActiveCfg = Debug|Any CPU @@ -896,6 +909,7 @@ Global {2CE8E3A6-59CC-FE9C-9399-AD54E1FA862B} = {985E0ADB-D4B4-473A-AA40-567E279B7946} {3B05CC76-C3CB-8667-6B65-3129DFB25681} = {0BB8B634-407A-4610-A91F-11586990767A} {F4B2B967-98D7-4D93-9A5C-5EF7B84B941A} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} = {8F890C6F-9ACC-438D-928A-AD61CDA862F2} {7F0B2446-0363-4720-AF46-F47F83B557DC} = {985E0ADB-D4B4-473A-AA40-567E279B7946} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs index e4b7aa065..70d12a646 100644 --- a/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs +++ b/src/WireMock.Net.AwesomeAssertions/Assertions/WireMockAssertions.WithBody.cs @@ -31,19 +31,19 @@ public AndConstraint WithBody(IStringMatcher matcher, string } [CustomAssertion] - public AndConstraint WithBodyAsJson(object body, string because = "", params object[] becauseArgs) + public AndConstraint WithBodyAsJson(object body, IJsonMatcher? jsonMatcher = null, string because = "", params object[] becauseArgs) { - return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs); + return WithBodyAsJson(jsonMatcher ?? new JsonMatcher(body), because, becauseArgs); } [CustomAssertion] - public AndConstraint WithBodyAsJson(string body, string because = "", params object[] becauseArgs) + public AndConstraint WithBodyAsJson(string body, IJsonMatcher? jsonMatcher = null, string because = "", params object[] becauseArgs) { - return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs); + return WithBodyAsJson(jsonMatcher ?? new JsonMatcher(body), because, becauseArgs); } [CustomAssertion] - public AndConstraint WithBodyAsJson(IObjectMatcher matcher, string because = "", params object[] becauseArgs) + public AndConstraint WithBodyAsJson(IJsonMatcher matcher, string because = "", params object[] becauseArgs) { var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsJson, matcher); @@ -126,15 +126,44 @@ private AndConstraint ExecuteAssertionWithBodyAsIObjectMatch private static string? FormatBody(object? body) { - return body switch + if (body == null) { - null => null, - string str => str, - AnyOf[] stringPatterns => FormatBodies(stringPatterns.Select(p => p.GetPattern())), - byte[] bytes => $"byte[{bytes.Length}] {{...}}", - JToken jToken => jToken.ToString(Formatting.None), - _ => JToken.FromObject(body).ToString(Formatting.None) - }; + return null; + } + + if (body is string str) + { + return str; + } + + if (body is AnyOf[] stringPatterns) + { + return FormatBodies(stringPatterns.Select(p => p.GetPattern())); + } + + if (body is byte[] bytes) + { + return $"byte[{bytes.Length}] {{...}}"; + } + + if (body is JToken jToken) + { + return jToken.ToString(Formatting.None); + } + + // System.IO.FileNotFoundException : Could not load file or assembly 'System.Text.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. The system cannot find the file specified. + var typeName = body.GetType().FullName; + if (typeName == "System.Text.Json.JsonElement") + { + return ((dynamic)body).GetRawText(); + } + + if (typeName == "System.Text.Json.JsonDocument") + { + return ((dynamic)body).RootElement.GetRawText(); + } + + return JToken.FromObject(body).ToString(Formatting.None); } private static string? FormatBodies(IEnumerable bodies) diff --git a/src/WireMock.Net.Extensions.Routing/WireMock.Net.Extensions.Routing.csproj b/src/WireMock.Net.Extensions.Routing/WireMock.Net.Extensions.Routing.csproj index 11578493e..8bdd01c6a 100644 --- a/src/WireMock.Net.Extensions.Routing/WireMock.Net.Extensions.Routing.csproj +++ b/src/WireMock.Net.Extensions.Routing/WireMock.Net.Extensions.Routing.csproj @@ -1,4 +1,4 @@ - + WireMock.Net.Routing extends WireMock.Net with modern, minimal-API-style routing for .NET Gennadii Saltyshchak @@ -25,7 +25,7 @@ - + diff --git a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs index a7b06ff8c..19e482835 100644 --- a/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs +++ b/src/WireMock.Net.FluentAssertions/Assertions/WireMockAssertions.WithBody.cs @@ -31,19 +31,19 @@ public AndConstraint WithBody(IStringMatcher matcher, string } [CustomAssertion] - public AndConstraint WithBodyAsJson(object body, string because = "", params object[] becauseArgs) + public AndConstraint WithBodyAsJson(object body, IJsonMatcher? jsonMatcher = null, string because = "", params object[] becauseArgs) { - return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs); + return WithBodyAsJson(jsonMatcher ?? new JsonMatcher(body), because, becauseArgs); } [CustomAssertion] - public AndConstraint WithBodyAsJson(string body, string because = "", params object[] becauseArgs) + public AndConstraint WithBodyAsJson(string body, IJsonMatcher? jsonMatcher = null, string because = "", params object[] becauseArgs) { - return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs); + return WithBodyAsJson(jsonMatcher ?? new JsonMatcher(body), because, becauseArgs); } [CustomAssertion] - public AndConstraint WithBodyAsJson(IObjectMatcher matcher, string because = "", params object[] becauseArgs) + public AndConstraint WithBodyAsJson(IJsonMatcher matcher, string because = "", params object[] becauseArgs) { var (filter, condition) = BuildFilterAndCondition(r => r.BodyAsJson, matcher); @@ -126,15 +126,44 @@ private AndConstraint ExecuteAssertionWithBodyAsIObjectMatch private static string? FormatBody(object? body) { - return body switch + if (body == null) { - null => null, - string str => str, - AnyOf[] stringPatterns => FormatBodies(stringPatterns.Select(p => p.GetPattern())), - byte[] bytes => $"byte[{bytes.Length}] {{...}}", - JToken jToken => jToken.ToString(Formatting.None), - _ => JToken.FromObject(body).ToString(Formatting.None) - }; + return null; + } + + if (body is string str) + { + return str; + } + + if (body is AnyOf[] stringPatterns) + { + return FormatBodies(stringPatterns.Select(p => p.GetPattern())); + } + + if (body is byte[] bytes) + { + return $"byte[{bytes.Length}] {{...}}"; + } + + if (body is JToken jToken) + { + return jToken.ToString(Formatting.None); + } + + // System.IO.FileNotFoundException : Could not load file or assembly 'System.Text.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. The system cannot find the file specified. + var typeName = body.GetType().FullName; + if (typeName == "System.Text.Json.JsonElement") + { + return ((dynamic)body).GetRawText(); + } + + if (typeName == "System.Text.Json.JsonDocument") + { + return ((dynamic)body).RootElement.GetRawText(); + } + + return JToken.FromObject(body).ToString(Formatting.None); } private static string? FormatBodies(IEnumerable bodies) diff --git a/src/WireMock.Net.Matchers.SystemTextJsonPath/Matchers/SystemTextJsonPathMatcher.cs b/src/WireMock.Net.Matchers.SystemTextJsonPath/Matchers/SystemTextJsonPathMatcher.cs new file mode 100644 index 000000000..606f3574d --- /dev/null +++ b/src/WireMock.Net.Matchers.SystemTextJsonPath/Matchers/SystemTextJsonPathMatcher.cs @@ -0,0 +1,184 @@ +// Copyright © WireMock.Net + +using System.Text.Json; +using System.Text.Json.Nodes; +using AnyOfTypes; +using Json.Path; +using Stef.Validation; +using WireMock.Extensions; +using WireMock.Models; +using WireMock.Util; + +namespace WireMock.Matchers; + +/// +/// SystemTextJsonPathMatcher - behaves the same as JsonPathMatcher but uses System.Text.Json and Json.Path instead of Newtonsoft.Json. +/// +/// +public class SystemTextJsonPathMatcher : ISystemTextJsonPathMatcher +{ + private readonly AnyOf[] _patterns; + + /// + public MatchBehaviour MatchBehaviour { get; } + + /// + public object Value { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The patterns. + public SystemTextJsonPathMatcher(params string[] patterns) + : this(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, patterns.ToAnyOfPatterns()) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The patterns. + public SystemTextJsonPathMatcher(params AnyOf[] patterns) + : this(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, patterns) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The to use. (default = "Or") + /// The patterns. + public SystemTextJsonPathMatcher( + MatchBehaviour matchBehaviour, + MatchOperator matchOperator = MatchOperator.Or, + params AnyOf[] patterns) + { + _patterns = Guard.NotNull(patterns); + MatchBehaviour = matchBehaviour; + MatchOperator = matchOperator; + Value = patterns; + } + + /// + public MatchResult IsMatch(string? input) + { + var score = MatchScores.Mismatch; + Exception? exception = null; + + if (!string.IsNullOrWhiteSpace(input)) + { + try + { + var node = JsonNode.Parse(input!); + score = IsMatchInternal(node); + } + catch (Exception ex) + { + exception = ex; + } + } + + return MatchResult.From(Name, MatchBehaviourHelper.Convert(MatchBehaviour, score), exception); + } + + /// + public MatchResult IsMatch(object? input) + { + var score = MatchScores.Mismatch; + Exception? exception = null; + + // When input is null or byte[], return Mismatch. + if (input != null && input is not byte[]) + { + try + { + JsonNode? node = input switch + { + JsonNode jsonNode => jsonNode, + string str => JsonNode.Parse(str), + _ => JsonNode.Parse(JsonSerializer.Serialize(input)) + }; + + score = IsMatchInternal(node); + } + catch (Exception ex) + { + exception = ex; + } + } + + return MatchResult.From(Name, MatchBehaviourHelper.Convert(MatchBehaviour, score), exception); + } + + /// + public AnyOf[] GetPatterns() + { + return _patterns; + } + + /// + public MatchOperator MatchOperator { get; } + + /// + public string Name => nameof(SystemTextJsonPathMatcher); + + /// + public string GetCSharpCodeArguments() + { + return $"new {Name}" + + $"(" + + $"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " + + $"{MatchOperator.GetFullyQualifiedEnumValue()}, " + + $"{MappingConverterUtils.ToCSharpCodeArguments(_patterns)}" + + $")"; + } + + private double IsMatchInternal(JsonNode? node) + { + // JsonPath.Net requires the node to be inside an object or array for filter expressions. + // Similar to JsonPathMatcher's ConvertJTokenToJArrayIfNeeded, wrap a plain object in an array + // when it's an object with a single non-array child property. + var evaluationNode = WrapIfNeeded(node); + + var values = _patterns + .Select(pattern => + { + var path = JsonPath.Parse(pattern.GetPattern()); + var result = path.Evaluate(evaluationNode); + return result.Matches is { Count: > 0 }; + }) + .ToArray(); + + return MatchScores.ToScore(values, MatchOperator); + } + + // Mirrors JsonPathMatcher.ConvertJTokenToJArrayIfNeeded: + // If the node is an object with exactly one property whose value is not already an array, + // wrap that value in an array so that filter expressions (e.g. [?(@.x == y)]) can match. + private static JsonNode? WrapIfNeeded(JsonNode? node) + { + if (node is not JsonObject obj) + { + return node; + } + + var properties = obj.ToList(); + if (properties.Count != 1) + { + return node; + } + + var single = properties[0]; + if (single.Value is JsonArray) + { + return node; + } + + var clonedValue = JsonNode.Parse(single.Value?.ToJsonString() ?? "null"); + return new JsonObject + { + [single.Key] = new JsonArray(clonedValue) + }; + } +} diff --git a/src/WireMock.Net.Matchers.SystemTextJsonPath/Properties/AssemblyInfo.cs b/src/WireMock.Net.Matchers.SystemTextJsonPath/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..da86a356e --- /dev/null +++ b/src/WireMock.Net.Matchers.SystemTextJsonPath/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +// Copyright © WireMock.Net + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] diff --git a/src/WireMock.Net.Matchers.SystemTextJsonPath/WireMock.Net.Matchers.SystemTextJsonPath.csproj b/src/WireMock.Net.Matchers.SystemTextJsonPath/WireMock.Net.Matchers.SystemTextJsonPath.csproj new file mode 100644 index 000000000..9a26d34a5 --- /dev/null +++ b/src/WireMock.Net.Matchers.SystemTextJsonPath/WireMock.Net.Matchers.SystemTextJsonPath.csproj @@ -0,0 +1,41 @@ + + + + A SystemTextJsonPathMatcher which can be used to match WireMock.Net Requests using JsonPath.Net. + WireMock.Net.Matchers.SystemTextJsonPath + Stef Heyenrath + netstandard2.0;net8.0 + true + wiremock;matchers;matcher;jsonpath;systemtextjson + WireMock + WireMock.Net.Matchers.SystemTextJsonPath + {A1B2C3D4-E5F6-7890-ABCD-EF1234567890} + true + $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb + true + true + true + true + ../WireMock.Net/WireMock.Net.snk + + true + MIT + + + + ../WireMock.Net/WireMock.Net.ruleset + + + + true + + + + + + + + + + + diff --git a/src/WireMock.Net.MimePart/Matchers/MimePartMatcher.cs b/src/WireMock.Net.MimePart/Matchers/MimePartMatcher.cs index 32b2df37a..8e9b00333 100644 --- a/src/WireMock.Net.MimePart/Matchers/MimePartMatcher.cs +++ b/src/WireMock.Net.MimePart/Matchers/MimePartMatcher.cs @@ -1,7 +1,8 @@ // Copyright © WireMock.Net -using System; -using System.Collections.Generic; +using JsonConverter.Abstractions; +using JsonConverter.Newtonsoft.Json; +using Newtonsoft.Json; using WireMock.Matchers.Helpers; using WireMock.Models.Mime; using WireMock.Util; @@ -15,6 +16,8 @@ public class MimePartMatcher : IMimePartMatcher { private readonly IList<(string Name, Func func)> _matcherFunctions; + private readonly IJsonConverter _jsonConverter; + /// public string Name => nameof(MimePartMatcher); @@ -41,7 +44,8 @@ public MimePartMatcher( IStringMatcher? contentTypeMatcher, IStringMatcher? contentDispositionMatcher, IStringMatcher? contentTransferEncodingMatcher, - IMatcher? contentMatcher + IMatcher? contentMatcher, + IJsonConverter? jsonConverter = null ) { MatchBehaviour = matchBehaviour; @@ -49,6 +53,7 @@ public MimePartMatcher( ContentDispositionMatcher = contentDispositionMatcher; ContentTransferEncodingMatcher = contentTransferEncodingMatcher; ContentMatcher = contentMatcher; + _jsonConverter = jsonConverter ?? new NewtonsoftJsonConverter(); _matcherFunctions = []; if (ContentTypeMatcher != null) @@ -107,7 +112,8 @@ private MatchResult MatchOnContent(IMimePartData mimePart) ContentType = GetContentTypeAsString(mimePart.ContentType), DeserializeJson = true, ContentEncoding = null, // mimePart.ContentType?.CharsetEncoding.ToString(), - DecompressGZipAndDeflate = true + DecompressGZipAndDeflate = true, + DefaultJsonConverter = _jsonConverter }; var bodyData = BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false).GetAwaiter().GetResult(); diff --git a/src/WireMock.Net.Minimal/Http/HttpResponseMessageHelper.cs b/src/WireMock.Net.Minimal/Http/HttpResponseMessageHelper.cs index 3279bc3ba..817faf7dd 100644 --- a/src/WireMock.Net.Minimal/Http/HttpResponseMessageHelper.cs +++ b/src/WireMock.Net.Minimal/Http/HttpResponseMessageHelper.cs @@ -1,8 +1,7 @@ // Copyright © WireMock.Net -using System.Linq; using System.Net; -using System.Net.Http; +using JsonConverter.Abstractions; using WireMock.Util; namespace WireMock.Http; @@ -15,7 +14,8 @@ public static async Task CreateAsync( Uri originalUri, bool deserializeJson, bool decompressGzipAndDeflate, - bool deserializeFormUrlEncoded) + bool deserializeFormUrlEncoded, + IJsonConverter jsonConverter) { var responseMessage = new ResponseMessage { StatusCode = (int)httpResponseMessage.StatusCode }; @@ -45,7 +45,8 @@ public static async Task CreateAsync( DeserializeJson = deserializeJson, ContentEncoding = contentEncodingHeader?.FirstOrDefault(), DecompressGZipAndDeflate = decompressGzipAndDeflate, - DeserializeFormUrlEncoded = deserializeFormUrlEncoded + DeserializeFormUrlEncoded = deserializeFormUrlEncoded, + DefaultJsonConverter = jsonConverter }; responseMessage.BodyData = await BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false); } diff --git a/src/WireMock.Net.Minimal/Matchers/AbstractJsonPartialMatcher.cs b/src/WireMock.Net.Minimal/Matchers/AbstractJsonPartialMatcher.cs index f2f853cbf..f1e25ccc6 100644 --- a/src/WireMock.Net.Minimal/Matchers/AbstractJsonPartialMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/AbstractJsonPartialMatcher.cs @@ -1,6 +1,5 @@ // Copyright © WireMock.Net -using System.Linq; using Newtonsoft.Json.Linq; using WireMock.Util; diff --git a/src/WireMock.Net.Minimal/Matchers/AbstractSystemTextJsonPartialMatcher.cs b/src/WireMock.Net.Minimal/Matchers/AbstractSystemTextJsonPartialMatcher.cs new file mode 100644 index 000000000..76a70cd57 --- /dev/null +++ b/src/WireMock.Net.Minimal/Matchers/AbstractSystemTextJsonPartialMatcher.cs @@ -0,0 +1,173 @@ +// Copyright © WireMock.Net + +using System.Text.Json; +using WireMock.Util; + +namespace WireMock.Matchers; + +/// +/// Generic AbstractSystemTextJsonPartialMatcher - uses System.Text.Json instead of Newtonsoft.Json. +/// +public abstract class AbstractSystemTextJsonPartialMatcher : SystemTextJsonMatcher +{ + /// + /// Initializes a new instance of the class. + /// + protected AbstractSystemTextJsonPartialMatcher(string value, bool ignoreCase = false, bool regex = false) + : base(value, ignoreCase, regex) + { + } + + /// + /// Initializes a new instance of the class. + /// + protected AbstractSystemTextJsonPartialMatcher(object value, bool ignoreCase = false, bool regex = false) + : base(value, ignoreCase, regex) + { + } + + /// + /// Initializes a new instance of the class. + /// + protected AbstractSystemTextJsonPartialMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) + : base(matchBehaviour, value, ignoreCase, regex) + { + } + + /// + protected override bool IsMatch(JsonElement value, JsonElement? input) + { + if (input == null) + { + return false; + } + + var inputElement = input.Value; + + // Regex on a string value + if (Regex && value.ValueKind == JsonValueKind.String) + { + var valueAsString = value.GetString()!; + var inputAsString = GetStringValue(inputElement); + + var (valid, result) = RegexUtils.MatchRegex(valueAsString, inputAsString); + if (valid) + { + return result; + } + } + + // Guid comparison: both strings, both parseable as Guid + if (value.ValueKind == JsonValueKind.String && inputElement.ValueKind == JsonValueKind.String) + { + var valueStr = value.GetString()!; + var inputStr = inputElement.GetString()!; + if (Guid.TryParse(valueStr, out var vg) && Guid.TryParse(inputStr, out var ig)) + { + return IsMatch(vg.ToString(), ig.ToString()); + } + } + + // Type mismatch (after regex/guid checks) + if (value.ValueKind != inputElement.ValueKind) + { + return false; + } + + switch (value.ValueKind) + { + case JsonValueKind.Object: + { + var nestedValues = value.EnumerateObject().ToArray(); + if (nestedValues.Length == 0) + { + return true; + } + + return nestedValues.All(pair => + { + var selected = SelectElement(inputElement, pair.Name); + return selected != null && IsMatch(pair.Value, selected.Value); + }); + } + + case JsonValueKind.Array: + { + var valuesArray = value.EnumerateArray().ToArray(); + if (valuesArray.Length == 0) + { + return true; + } + + var tokenArray = inputElement.EnumerateArray().ToArray(); + if (tokenArray.Length == 0) + { + return false; + } + + return valuesArray.All(subFilter => tokenArray.Any(subToken => IsMatch(subFilter, subToken))); + } + + default: + return IsMatch(GetStringValue(value), GetStringValue(inputElement)); + } + } + + /// + /// Check if two strings are a match (matching can be done exact or wildcard). + /// + protected abstract bool IsMatch(string value, string input); + + /// + /// Selects a from an object using a key that may be a plain property name, + /// a dotted path (e.g. "a.b.c") or bracket notation (e.g. "['name.with.dot']"), + /// mirroring Newtonsoft's SelectToken + direct indexer fallback. + /// + private static JsonElement? SelectElement(JsonElement input, string key) + { + if (input.ValueKind != JsonValueKind.Object) + { + return null; + } + + // Direct property access (also handles keys containing colons or dots that are literal property names) + if (input.TryGetProperty(key, out var direct)) + { + return direct; + } + + // Bracket notation: ['property.name.with.dots'] + if (key.StartsWith("['") && key.EndsWith("']")) + { + var propertyName = key.Substring(2, key.Length - 4); + return input.TryGetProperty(propertyName, out var bracketValue) ? bracketValue : null; + } + + // Dotted path: a.b.c + if (key.Contains('.')) + { + var parts = key.Split('.'); + var current = input; + foreach (var part in parts) + { + if (current.ValueKind != JsonValueKind.Object || !current.TryGetProperty(part, out var next)) + { + return null; + } + + current = next; + } + + return current; + } + + return null; + } + + private static string GetStringValue(JsonElement element) + { + return element.ValueKind == JsonValueKind.String + ? element.GetString()! + : element.GetRawText(); + } +} diff --git a/src/WireMock.Net.Minimal/Matchers/JSONPathMatcher.cs b/src/WireMock.Net.Minimal/Matchers/JSONPathMatcher.cs index b6e2488ed..53da43a47 100644 --- a/src/WireMock.Net.Minimal/Matchers/JSONPathMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/JSONPathMatcher.cs @@ -1,6 +1,5 @@ // Copyright © WireMock.Net -using System.Linq; using AnyOfTypes; using Newtonsoft.Json.Linq; using Stef.Validation; @@ -13,9 +12,8 @@ namespace WireMock.Matchers; /// /// JsonPathMatcher /// -/// -/// -public class JsonPathMatcher : IStringMatcher, IObjectMatcher +/// +public class JsonPathMatcher : IJsonPathMatcher { private readonly AnyOf[] _patterns; diff --git a/src/WireMock.Net.Minimal/Matchers/JsonMatcher.cs b/src/WireMock.Net.Minimal/Matchers/JsonMatcher.cs index 64a047334..1957e4975 100644 --- a/src/WireMock.Net.Minimal/Matchers/JsonMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/JsonMatcher.cs @@ -1,11 +1,12 @@ // Copyright © WireMock.Net -using System.Linq; +using System.Collections; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Stef.Validation; using WireMock.Extensions; +using WireMock.Serialization; using WireMock.Util; -using JsonUtils = WireMock.Util.JsonUtils; namespace WireMock.Matchers; @@ -69,7 +70,7 @@ public JsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase Regex = regex; Value = value; - _valueAsJToken = JsonUtils.ConvertValueToJToken(value); + _valueAsJToken = ConvertValueToJToken(value); } /// @@ -83,7 +84,7 @@ public MatchResult IsMatch(object? input) { try { - var inputAsJToken = JsonUtils.ConvertValueToJToken(input); + var inputAsJToken = ConvertValueToJToken(input); var match = IsMatch(RenameJToken(_valueAsJToken), RenameJToken(inputAsJToken)); score = MatchScores.ToScore(match); @@ -103,7 +104,7 @@ public virtual string GetCSharpCodeArguments() return $"new {Name}" + $"(" + $"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " + - $"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " + + $"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " + $"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " + $"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}" + $")"; @@ -241,6 +242,18 @@ private JObject RenameJObject(JObject obj) return new JObject(renamedProperties); } + private static JToken ConvertValueToJToken(object value) + { + // Check if JToken, string, IEnumerable or object + return value switch + { + JToken tokenValue => tokenValue, + string stringValue => JsonConvert.DeserializeObject(stringValue, JsonSerializationConstants.JsonDeserializerSettingsWithDateParsingNone)!, + IEnumerable enumerableValue => JArray.FromObject(enumerableValue), + _ => JObject.FromObject(value), + }; + } + private static string? ToUpper(string? input) { return input?.ToUpperInvariant(); diff --git a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs index 55728137e..71c01f8ef 100644 --- a/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/Request/RequestMessageBodyMatcher.cs @@ -1,6 +1,5 @@ // Copyright © WireMock.Net -using System.Linq; using Stef.Validation; using WireMock.Matchers.Helpers; using WireMock.Util; diff --git a/src/WireMock.Net.Minimal/Matchers/SystemTextJsonMatcher.cs b/src/WireMock.Net.Minimal/Matchers/SystemTextJsonMatcher.cs new file mode 100644 index 000000000..1f4723430 --- /dev/null +++ b/src/WireMock.Net.Minimal/Matchers/SystemTextJsonMatcher.cs @@ -0,0 +1,282 @@ +// Copyright © WireMock.Net + +using System.Collections; +using System.Text.Json; +using Stef.Validation; +using WireMock.Extensions; +using WireMock.Util; + +namespace WireMock.Matchers; + +/// +/// SystemTextJsonMatcher - behaves the same as but uses System.Text.Json instead of Newtonsoft.Json. +/// +public class SystemTextJsonMatcher : IJsonMatcher +{ + private static readonly JsonSerializerOptions DefaultSerializerOptions = new() + { + PropertyNameCaseInsensitive = false + }; + + /// + public virtual string Name => nameof(SystemTextJsonMatcher); + + /// + public object Value { get; } + + /// + public MatchBehaviour MatchBehaviour { get; } + + /// + public bool IgnoreCase { get; } + + /// + /// Support Regex + /// + public bool Regex { get; } + + private readonly JsonElement _valueAsJsonElement; + + /// + /// Initializes a new instance of the class. + /// + /// The string value to check for equality. + /// Ignore the case from the PropertyName and PropertyValue (string only). + /// Support Regex. + public SystemTextJsonMatcher(string value, bool ignoreCase = false, bool regex = false) + : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The object value to check for equality. + /// Ignore the case from the PropertyName and PropertyValue (string only). + /// Support Regex. + public SystemTextJsonMatcher(object value, bool ignoreCase = false, bool regex = false) + : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The match behaviour. + /// The value to check for equality. + /// Ignore the case from the PropertyName and PropertyValue (string only). + /// Support Regex. + public SystemTextJsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) + { + Guard.NotNull(value); + + MatchBehaviour = matchBehaviour; + IgnoreCase = ignoreCase; + Regex = regex; + + Value = value; + _valueAsJsonElement = ConvertToJsonElement(value); + } + + /// + public MatchResult IsMatch(object? input) + { + var score = MatchScores.Mismatch; + Exception? error = null; + + // When input is null or byte[], return Mismatch. + if (input != null && input is not byte[]) + { + try + { + var inputAsJsonElement = ConvertToJsonElement(input); + + var match = IsMatch(NormalizeElement(_valueAsJsonElement), NormalizeElement(inputAsJsonElement)); + score = MatchScores.ToScore(match); + } + catch (Exception ex) + { + error = ex; + } + } + + return MatchResult.From(Name, MatchBehaviourHelper.Convert(MatchBehaviour, score), error); + } + + /// + public virtual string GetCSharpCodeArguments() + { + return $"new {Name}" + + $"(" + + $"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " + + $"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " + + $"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " + + $"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}" + + $")"; + } + + /// + /// Compares the input against the matcher value + /// + protected virtual bool IsMatch(JsonElement value, JsonElement? input) + { + if (input == null) + { + return false; + } + + var inputElement = input.Value; + + // If using Regex and the value is a string, use the MatchRegex method. + if (Regex && value.ValueKind == JsonValueKind.String) + { + var valueAsString = value.GetString()!; + var inputAsString = inputElement.ValueKind == JsonValueKind.String + ? inputElement.GetString()! + : inputElement.GetRawText(); + + var (valid, result) = RegexUtils.MatchRegex(valueAsString, inputAsString); + if (valid) + { + return result; + } + } + + // If the value is a Guid (string) and input is a string, or vice versa, compare as strings. + if (value.ValueKind == JsonValueKind.String && inputElement.ValueKind == JsonValueKind.String) + { + var valueStr = value.GetString()!; + var inputStr = inputElement.GetString()!; + + if (Guid.TryParse(valueStr, out var valueGuid) && Guid.TryParse(inputStr, out var inputGuid)) + { + return valueGuid == inputGuid; + } + } + + switch (value.ValueKind) + { + case JsonValueKind.Object: + { + if (inputElement.ValueKind != JsonValueKind.Object) + { + return false; + } + + var valueProperties = value.EnumerateObject().ToDictionary(p => p.Name, p => p.Value); + var inputProperties = inputElement.EnumerateObject().ToDictionary(p => p.Name, p => p.Value); + + if (valueProperties.Count != inputProperties.Count) + { + return false; + } + + foreach (var pair in valueProperties) + { + if (!inputProperties.TryGetValue(pair.Key, out var inputPropValue)) + { + return false; + } + + if (!IsMatch(pair.Value, inputPropValue)) + { + return false; + } + } + + return true; + } + + case JsonValueKind.Array: + { + if (inputElement.ValueKind != JsonValueKind.Array) + { + return false; + } + + var valueArray = value.EnumerateArray().ToArray(); + var inputArray = inputElement.EnumerateArray().ToArray(); + + if (valueArray.Length != inputArray.Length) + { + return false; + } + + return !valueArray.Where((valueToken, index) => !IsMatch(valueToken, inputArray[index])).Any(); + } + + default: + return value.GetRawText() == inputElement.GetRawText(); + } + } + + private JsonElement NormalizeElement(JsonElement element) + { + if (!IgnoreCase) + { + return element; + } + + var normalized = NormalizeValue(element); + return ConvertToJsonElement(normalized); + } + + private object NormalizeValue(JsonElement element) + { + switch (element.ValueKind) + { + case JsonValueKind.Object: + { + var dict = new Dictionary(); + foreach (var prop in element.EnumerateObject()) + { + var normalizedKey = prop.Name.ToUpperInvariant(); + dict[normalizedKey] = NormalizeValue(prop.Value); + } + + return dict; + } + + case JsonValueKind.Array: + { + if (Regex) + { + return element.EnumerateArray().Select(e => (object)e.GetRawText()).ToArray(); + } + + return element.EnumerateArray().Select(NormalizeValue).ToArray(); + } + + case JsonValueKind.String: + { + var str = element.GetString()!; + return Regex ? str : str.ToUpperInvariant(); + } + + default: + return element.GetRawText(); + } + } + + private static JsonElement ConvertToJsonElement(object value) + { + switch (value) + { + case JsonElement jsonElement: + return jsonElement; + + case JsonDocument jsonDocument: + return jsonDocument.RootElement; + + case string stringValue: + return JsonDocument.Parse(stringValue).RootElement; + + case IEnumerable enumerableValue when value is not string: + return JsonSerializer.SerializeToElement(enumerableValue, DefaultSerializerOptions); + + default: + var json = JsonSerializer.Serialize(value, DefaultSerializerOptions); + return JsonDocument.Parse(json).RootElement; + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Matchers/SystemTextJsonPartialMatcher.cs b/src/WireMock.Net.Minimal/Matchers/SystemTextJsonPartialMatcher.cs new file mode 100644 index 000000000..e431f89c3 --- /dev/null +++ b/src/WireMock.Net.Minimal/Matchers/SystemTextJsonPartialMatcher.cs @@ -0,0 +1,52 @@ +// Copyright © WireMock.Net + +using WireMock.Extensions; +using WireMock.Util; + +namespace WireMock.Matchers; + +/// +/// SystemTextJsonPartialMatcher - uses System.Text.Json instead of Newtonsoft.Json. +/// +public class SystemTextJsonPartialMatcher : AbstractSystemTextJsonPartialMatcher +{ + /// + public override string Name => nameof(SystemTextJsonPartialMatcher); + + /// + public SystemTextJsonPartialMatcher(string value, bool ignoreCase = false, bool regex = false) + : base(value, ignoreCase, regex) + { + } + + /// + public SystemTextJsonPartialMatcher(object value, bool ignoreCase = false, bool regex = false) + : base(value, ignoreCase, regex) + { + } + + /// + public SystemTextJsonPartialMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) + : base(matchBehaviour, value, ignoreCase, regex) + { + } + + /// + protected override bool IsMatch(string value, string input) + { + var exactStringMatcher = new ExactMatcher(MatchBehaviour.AcceptOnMatch, IgnoreCase, MatchOperator.Or, value); + return exactStringMatcher.IsMatch(input).IsPerfect(); + } + + /// + public override string GetCSharpCodeArguments() + { + return $"new {Name}" + + $"(" + + $"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " + + $"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " + + $"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " + + $"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}" + + $")"; + } +} diff --git a/src/WireMock.Net.Minimal/Matchers/SystemTextJsonPartialWildcardMatcher.cs b/src/WireMock.Net.Minimal/Matchers/SystemTextJsonPartialWildcardMatcher.cs new file mode 100644 index 000000000..924c9434a --- /dev/null +++ b/src/WireMock.Net.Minimal/Matchers/SystemTextJsonPartialWildcardMatcher.cs @@ -0,0 +1,52 @@ +// Copyright © WireMock.Net + +using WireMock.Extensions; +using WireMock.Util; + +namespace WireMock.Matchers; + +/// +/// SystemTextJsonPartialWildcardMatcher - uses System.Text.Json instead of Newtonsoft.Json. +/// +public class SystemTextJsonPartialWildcardMatcher : AbstractSystemTextJsonPartialMatcher +{ + /// + public override string Name => nameof(SystemTextJsonPartialWildcardMatcher); + + /// + public SystemTextJsonPartialWildcardMatcher(string value, bool ignoreCase = false, bool regex = false) + : base(value, ignoreCase, regex) + { + } + + /// + public SystemTextJsonPartialWildcardMatcher(object value, bool ignoreCase = false, bool regex = false) + : base(value, ignoreCase, regex) + { + } + + /// + public SystemTextJsonPartialWildcardMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) + : base(matchBehaviour, value, ignoreCase, regex) + { + } + + /// + protected override bool IsMatch(string value, string input) + { + var wildcardStringMatcher = new WildcardMatcher(MatchBehaviour.AcceptOnMatch, value, IgnoreCase); + return wildcardStringMatcher.IsMatch(input).IsPerfect(); + } + + /// + public override string GetCSharpCodeArguments() + { + return $"new {Name}" + + $"(" + + $"{MatchBehaviour.GetFullyQualifiedEnumValue()}, " + + $"{CSharpFormatter.ConvertToAnonymousObjectDefinition(Value, 3)}, " + + $"{CSharpFormatter.ToCSharpBooleanLiteral(IgnoreCase)}, " + + $"{CSharpFormatter.ToCSharpBooleanLiteral(Regex)}" + + $")"; + } +} diff --git a/src/WireMock.Net.Minimal/Owin/Mappers/OwinRequestMapper.cs b/src/WireMock.Net.Minimal/Owin/Mappers/OwinRequestMapper.cs index ace4844cf..0db51eb47 100644 --- a/src/WireMock.Net.Minimal/Owin/Mappers/OwinRequestMapper.cs +++ b/src/WireMock.Net.Minimal/Owin/Mappers/OwinRequestMapper.cs @@ -53,7 +53,8 @@ public async Task MapAsync(HttpContext context, IWireMockMiddlew ContentType = request.ContentType, DeserializeJson = !options.DisableJsonBodyParsing.GetValueOrDefault(false), ContentEncoding = contentEncodingHeader?.FirstOrDefault(), - DecompressGZipAndDeflate = !options.DisableRequestBodyDecompressing.GetValueOrDefault(false) + DecompressGZipAndDeflate = !options.DisableRequestBodyDecompressing.GetValueOrDefault(false), + DefaultJsonConverter = options.DefaultJsonSerializer }; body = await BodyParser.ParseAsync(bodyParserSettings).ConfigureAwait(false); diff --git a/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareLogger.cs b/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareLogger.cs index d034286b9..3a0d0a395 100644 --- a/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareLogger.cs +++ b/src/WireMock.Net.Minimal/Owin/WireMockMiddlewareLogger.cs @@ -1,7 +1,6 @@ // Copyright © WireMock.Net using System.Diagnostics; -using System.Linq; using WireMock.Logging; using WireMock.Owin.ActivityTracing; using WireMock.Serialization; @@ -42,7 +41,7 @@ public void LogRequestAndResponse(bool logRequest, RequestMessage request, IResp if (_options.SaveUnmatchedRequests == true && match?.RequestMatchResult is not { IsPerfectMatch: true }) { var filename = $"{logEntry.Guid}.LogEntry.json"; - _options.FileSystemHandler?.WriteUnmatchedRequest(filename, JsonUtils.Serialize(logEntry)); + _options.FileSystemHandler?.WriteUnmatchedRequest(filename, _options.DefaultJsonSerializer.Serialize(logEntry)); } } catch diff --git a/src/WireMock.Net.Minimal/Properties/AssemblyInfo.cs b/src/WireMock.Net.Minimal/Properties/AssemblyInfo.cs index ab3957c29..0e83fce12 100644 --- a/src/WireMock.Net.Minimal/Properties/AssemblyInfo.cs +++ b/src/WireMock.Net.Minimal/Properties/AssemblyInfo.cs @@ -7,4 +7,4 @@ [assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] // Needed for Moq in the UnitTest project -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] \ No newline at end of file +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Proxy/ProxyHelper.cs b/src/WireMock.Net.Minimal/Proxy/ProxyHelper.cs index 3e7991826..821f4ee26 100644 --- a/src/WireMock.Net.Minimal/Proxy/ProxyHelper.cs +++ b/src/WireMock.Net.Minimal/Proxy/ProxyHelper.cs @@ -48,7 +48,8 @@ internal class ProxyHelper(WireMockServerSettings settings) originalUri, deserializeJson, decompressGzipAndDeflate, - deserializeFormUrlEncoded + deserializeFormUrlEncoded, + _settings.DefaultJsonSerializer ).ConfigureAwait(false); IMapping? newMapping = null; diff --git a/src/WireMock.Net.Minimal/ResponseBuilders/Response.WithBody.cs b/src/WireMock.Net.Minimal/ResponseBuilders/Response.WithBody.cs index 751b0b399..118dc70c0 100644 --- a/src/WireMock.Net.Minimal/ResponseBuilders/Response.WithBody.cs +++ b/src/WireMock.Net.Minimal/ResponseBuilders/Response.WithBody.cs @@ -2,8 +2,10 @@ using System.Text; using JsonConverter.Abstractions; +using JsonConverter.Newtonsoft.Json; using Stef.Validation; using WireMock.Models; +using WireMock.Serialization; using WireMock.Types; using WireMock.Util; @@ -119,11 +121,13 @@ public IResponseBuilder WithBodyFromFile(string filename, bool cache = true) } /// - public IResponseBuilder WithBody(string body, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null) + public IResponseBuilder WithBody(string body, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null, IJsonConverter? jsonConverter = null, JsonConverterOptions? options = null) { Guard.NotNull(body); encoding ??= Encoding.UTF8; + jsonConverter ??= new NewtonsoftJsonConverter(); + options ??= JsonSerializationConstants.JsonConverterOptionsWithDateParsingNone; ResponseMessage.BodyDestination = destination; ResponseMessage.BodyData = new BodyData @@ -140,7 +144,7 @@ public IResponseBuilder WithBody(string body, string? destination = BodyDestinat case BodyDestinationFormat.Json: ResponseMessage.BodyData.DetectedBodyType = BodyType.Json; - ResponseMessage.BodyData.BodyAsJson = JsonUtils.DeserializeObject(body); + ResponseMessage.BodyData.BodyAsJson = jsonConverter.Deserialize(body, options); break; default: diff --git a/src/WireMock.Net.Minimal/ResponseBuilders/Response.cs b/src/WireMock.Net.Minimal/ResponseBuilders/Response.cs index 52343d8a2..fdd9b5751 100644 --- a/src/WireMock.Net.Minimal/ResponseBuilders/Response.cs +++ b/src/WireMock.Net.Minimal/ResponseBuilders/Response.cs @@ -197,7 +197,7 @@ public IResponseBuilder WithRandomDelay(int minimumMilliseconds = 0, int maximum if (ProxyAndRecordSettings != null && _httpClientForProxy != null) { - string RemoveFirstOccurrence(string source, string find) + static string RemoveFirstOccurrence(string source, string find) { int place = source.IndexOf(find, StringComparison.OrdinalIgnoreCase); return place >= 0 ? source.Remove(place, find.Length) : source; @@ -265,7 +265,7 @@ string RemoveFirstOccurrence(string source, string find) var decoded = await protoBufMatcher.DecodeAsync(requestMessage.BodyData?.BodyAsBytes).ConfigureAwait(false); if (decoded != null) { - requestMessageImplementation.BodyAsJson = JsonUtils.ConvertValueToJToken(decoded); + requestMessageImplementation.BodyAsJson = settings.DefaultJsonSerializer.ToJsonToken(decoded); } } } diff --git a/src/WireMock.Net.Minimal/Serialization/MappingSerializer.cs b/src/WireMock.Net.Minimal/Serialization/MappingSerializer.cs index 5de20e499..865325956 100644 --- a/src/WireMock.Net.Minimal/Serialization/MappingSerializer.cs +++ b/src/WireMock.Net.Minimal/Serialization/MappingSerializer.cs @@ -1,55 +1,31 @@ // Copyright © WireMock.Net using JsonConverter.Abstractions; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -#if NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER || NET6_0_OR_GREATER || NET461 -using System.Text.Json; -#endif +using JsonConverter.Abstractions.Models; namespace WireMock.Serialization; internal class MappingSerializer(IJsonConverter jsonConverter) { - private static readonly JsonConverterOptions JsonConverterOptions = new JsonConverterOptions - { - DateParseHandling = (int) DateParseHandling.None - }; - internal T[] DeserializeJsonToArray(string value) { - // DeserializeObject - return DeserializeObjectToArray(jsonConverter.Deserialize(value, JsonConverterOptions)!); - } - - internal static T[] DeserializeObjectToArray(object value) - { - if (value is JArray jArray) + switch (JsonTypeHelper.GetJsonType(value)) { - return jArray.ToObject()!; - } + case JsonType.Array: + return jsonConverter.Deserialize(value, JsonSerializationConstants.JsonConverterOptionsWithDateParsingNone)!; - if (value is JObject jObject) - { - var singleResult = jObject.ToObject(); - return [singleResult!]; - } - -#if NETSTANDARD2_0_OR_GREATER || NETCOREAPP3_1_OR_GREATER || NET6_0_OR_GREATER || NET461 - if (value is JsonElement jElement) - { - if (jElement.ValueKind == JsonValueKind.Array) - { - return jElement.Deserialize()!; - } - - if (jElement.ValueKind == JsonValueKind.Object) - { - var singleResult = jElement.Deserialize(); + case JsonType.Object: + var singleResult = jsonConverter.Deserialize(value, JsonSerializationConstants.JsonConverterOptionsWithDateParsingNone); return [singleResult!]; - } + + default: + throw new InvalidOperationException("Cannot deserialize the provided value to an array or object."); } -#endif - throw new InvalidOperationException("Cannot deserialize the provided value to an array or object."); + } + + internal T[] DeserializeObjectToArray(object value) + { + var json = jsonConverter.Serialize(value, JsonSerializationConstants.JsonConverterOptionsWithDateParsingNone); + return DeserializeJsonToArray(json); } } \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Serialization/MatcherMapper.cs b/src/WireMock.Net.Minimal/Serialization/MatcherMapper.cs index 76b3ee564..b6038d927 100644 --- a/src/WireMock.Net.Minimal/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net.Minimal/Serialization/MatcherMapper.cs @@ -106,9 +106,29 @@ public IMatcher[] Map(IEnumerable? matchers) var valueForJsonPartialWildcardMatcher = matcherModel.Pattern ?? matcherModel.Patterns; return new JsonPartialWildcardMatcher(matchBehaviour, valueForJsonPartialWildcardMatcher!, ignoreCase, useRegex); + case nameof(SystemTextJsonMatcher): + var valueForSystemTextJsonMatcher = matcherModel.Pattern ?? matcherModel.Patterns; + return new SystemTextJsonMatcher(matchBehaviour, valueForSystemTextJsonMatcher!, ignoreCase, useRegex); + + case nameof(SystemTextJsonPartialMatcher): + var valueForSystemTextJsonPartialMatcher = matcherModel.Pattern ?? matcherModel.Patterns; + return new SystemTextJsonPartialMatcher(matchBehaviour, valueForSystemTextJsonPartialMatcher!, ignoreCase, useRegex); + + case nameof(SystemTextJsonPartialWildcardMatcher): + var valueForSystemTextJsonPartialWildcardMatcher = matcherModel.Pattern ?? matcherModel.Patterns; + return new SystemTextJsonPartialWildcardMatcher(matchBehaviour, valueForSystemTextJsonPartialWildcardMatcher!, ignoreCase, useRegex); + case nameof(JsonPathMatcher): return new JsonPathMatcher(matchBehaviour, matchOperator, stringPatterns); + case "SystemTextJsonPathMatcher": + if (TypeLoader.TryLoadNewInstance(out var systemTextJsonPathMatcher, matchBehaviour, matchOperator, stringPatterns)) + { + return systemTextJsonPathMatcher; + } + + throw new InvalidOperationException("The 'SystemTextJsonPathMatcher' cannot be loaded. Please install the WireMock.Net.Matchers.SystemTextJsonPath package."); + case nameof(JmesPathMatcher): return new JmesPathMatcher(matchBehaviour, matchOperator, stringPatterns); @@ -171,6 +191,10 @@ public IMatcher[] Map(IEnumerable? matchers) model.Regex = jsonMatcher.Regex; break; + case SystemTextJsonMatcher stjMatcher: + model.Regex = stjMatcher.Regex; + break; + case XPathMatcher xpathMatcher: model.XmlNamespaceMap = xpathMatcher.XmlNamespaceMap; break; diff --git a/src/WireMock.Net.Minimal/Serialization/PactMapper.cs b/src/WireMock.Net.Minimal/Serialization/PactMapper.cs index eae419359..ec7266f56 100644 --- a/src/WireMock.Net.Minimal/Serialization/PactMapper.cs +++ b/src/WireMock.Net.Minimal/Serialization/PactMapper.cs @@ -1,6 +1,8 @@ // Copyright © WireMock.Net using System.Linq; +using System.Text; +using Newtonsoft.Json; using WireMock.Admin.Mappings; using WireMock.Extensions; using WireMock.Pact.Models.V2; @@ -49,7 +51,7 @@ public static (string FileName, byte[] Bytes) ToPact(WireMockServer server, stri pact.Interactions.Add(interaction); } - return (filename, JsonUtils.SerializeAsPactFile(pact)); + return (filename, SerializeAsPactFile(pact)); } private static PactRequest MapRequest(RequestModel request, string path) @@ -152,7 +154,7 @@ private static int MapStatusCode(object? statusCode) /// private static object? TryDeserializeJsonStringAsObject(string? value) { - return value != null ? JsonUtils.TryDeserializeObject(value) ?? value : null; + return value != null ? TryDeserializeObject(value) ?? value : null; } //private static string GetPatternAsStringFromMatchers(MatcherModel[]? matchers, string defaultValue) @@ -164,4 +166,22 @@ private static int MapStatusCode(object? statusCode) // return defaultValue; //} + + private static byte[] SerializeAsPactFile(object value) + { + var json = JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsPact); + return Encoding.UTF8.GetBytes(json); + } + + private static T? TryDeserializeObject(string json) + { + try + { + return JsonConvert.DeserializeObject(json); + } + catch + { + return default; + } + } } \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Serialization/SwaggerMapper.cs b/src/WireMock.Net.Minimal/Serialization/SwaggerMapper.cs index 7116171a3..58873c0e1 100644 --- a/src/WireMock.Net.Minimal/Serialization/SwaggerMapper.cs +++ b/src/WireMock.Net.Minimal/Serialization/SwaggerMapper.cs @@ -1,7 +1,8 @@ // Copyright © WireMock.Net -using System.Linq; +using System.Diagnostics.CodeAnalysis; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using NJsonSchema; using NJsonSchema.Extensions; using NSwag; @@ -281,7 +282,7 @@ private static JsonSchema GetJsonSchema(object instance) if (matcher is { Name: nameof(JsonMatcher) }) { var pattern = GetPatternAsStringFromMatcher(matcher); - if (JsonUtils.TryParseAsJObject(pattern, out var jObject)) + if (TryParseAsJObject(pattern, out var jObject)) { return jObject; } @@ -292,6 +293,39 @@ private static JsonSchema GetJsonSchema(object instance) return null; } + private static bool IsJson(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + { + return false; + } + + value = value!.Trim(); + + return (value.StartsWith("{") && value.EndsWith("}")) || (value.StartsWith("[") && value.EndsWith("]")); + } + + private static bool TryParseAsJObject(string? strInput, [NotNullWhen(true)] out JObject? value) + { + value = null; + + if (!IsJson(strInput)) + { + return false; + } + + try + { + // Try to convert this string into a JObject + value = JObject.Parse(strInput!); + return true; + } + catch + { + return false; + } + } + private static string GetContentType(RequestModel request) { var contentType = request.Headers?.FirstOrDefault(h => h.Name == "Content-Type"); diff --git a/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs b/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs index eb89a443b..2a2e4e7d2 100644 --- a/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs +++ b/src/WireMock.Net.Minimal/Server/WireMockServer.Admin.cs @@ -346,7 +346,7 @@ private IResponseMessage SettingsUpdate(HttpContext _, IRequestMessage requestMe } o.CorsPolicyOptions = corsPolicyOptions; - o.ClientCertificateMode = (Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode) _settings.ClientCertificateMode; + o.ClientCertificateMode = (Microsoft.AspNetCore.Server.Kestrel.Https.ClientCertificateMode)_settings.ClientCertificateMode; o.AcceptAnyClientCertificate = _settings.AcceptAnyClientCertificate; }); @@ -883,6 +883,18 @@ private ResponseMessage ToJson(T result, bool keepNullValues = false, object? }; } + private T[] DeserializeRequestMessageToArray(IRequestMessage requestMessage) + { + if (requestMessage.BodyData?.DetectedBodyType == BodyType.Json && requestMessage.BodyData.BodyAsJson != null) + { + var bodyAsJson = requestMessage.BodyData.BodyAsJson!; + + return _mappingSerializer.DeserializeObjectToArray(bodyAsJson); + } + + throw new NotSupportedException(); + } + private static Encoding? ToEncoding(EncodingModel? encodingModel) { return encodingModel != null ? Encoding.GetEncoding(encodingModel.CodePage) : null; @@ -902,28 +914,16 @@ private static ResponseMessage ToResponseMessage(string text) }; } - private static T[] DeserializeRequestMessageToArray(IRequestMessage requestMessage) - { - if (requestMessage.BodyData?.DetectedBodyType == BodyType.Json && requestMessage.BodyData.BodyAsJson != null) - { - var bodyAsJson = requestMessage.BodyData.BodyAsJson!; - - return MappingSerializer.DeserializeObjectToArray(bodyAsJson); - } - - throw new NotSupportedException(); - } - - private static T DeserializeObject(IRequestMessage requestMessage) + private T DeserializeObject(IRequestMessage requestMessage) { switch (requestMessage.BodyData?.DetectedBodyType) { - case BodyType.String: - case BodyType.FormUrlEncoded: - return JsonUtils.DeserializeObject(requestMessage.BodyData.BodyAsString!); + case BodyType.String when requestMessage.BodyData?.BodyAsString != null: + case BodyType.FormUrlEncoded when requestMessage.BodyData?.BodyAsString != null: + return _settings.DefaultJsonSerializer.Deserialize(requestMessage.BodyData.BodyAsString)!; case BodyType.Json when requestMessage.BodyData?.BodyAsJson != null: - return ((JObject)requestMessage.BodyData.BodyAsJson).ToObject()!; + return _settings.DefaultJsonSerializer.ParseJsonToken(requestMessage.BodyData.BodyAsJson)!; default: throw new NotSupportedException(); diff --git a/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs index 5df902def..02960a632 100644 --- a/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs +++ b/src/WireMock.Net.Minimal/Server/WireMockServer.ConvertMapping.cs @@ -1,6 +1,5 @@ // Copyright © WireMock.Net -using System.Linq; using Stef.Validation; using WireMock.Admin.Mappings; using WireMock.Matchers; @@ -153,7 +152,7 @@ private IRequestBuilder InitRequestBuilder(RequestModel requestModel, MappingMod } else { - var clientIPModel = JsonUtils.ParseJTokenToObject(requestModel.ClientIP); + var clientIPModel = _settings.DefaultJsonSerializer.ParseJsonToken(requestModel.ClientIP); if (clientIPModel.Matchers != null) { requestBuilder = requestBuilder.WithPath(clientIPModel.Matchers.Select(_matcherMapper.Map).OfType().ToArray()); @@ -169,7 +168,7 @@ private IRequestBuilder InitRequestBuilder(RequestModel requestModel, MappingMod } else { - var pathModel = JsonUtils.ParseJTokenToObject(requestModel.Path); + var pathModel = _settings.DefaultJsonSerializer.ParseJsonToken(requestModel.Path); if (pathModel.Matchers != null) { var matchOperator = StringUtils.ParseMatchOperator(pathModel.MatchOperator); @@ -185,7 +184,7 @@ private IRequestBuilder InitRequestBuilder(RequestModel requestModel, MappingMod } else { - var urlModel = JsonUtils.ParseJTokenToObject(requestModel.Url); + var urlModel = _settings.DefaultJsonSerializer.ParseJsonToken(requestModel.Url); if (urlModel.Matchers != null) { var matchOperator = StringUtils.ParseMatchOperator(urlModel.MatchOperator); @@ -273,7 +272,7 @@ private IRequestBuilder InitRequestBuilder(RequestModel requestModel, MappingMod return requestBuilder; } - private static IResponseBuilder InitResponseBuilder(ResponseModel responseModel) + private IResponseBuilder InitResponseBuilder(ResponseModel responseModel) { var responseBuilder = Response.Create(); @@ -338,7 +337,7 @@ private static IResponseBuilder InitResponseBuilder(ResponseModel responseModel) } else { - var headers = JsonUtils.ParseJTokenToObject(entry.Value); + var headers = _settings.DefaultJsonSerializer.ParseJsonToken(entry.Value); responseBuilder.WithHeader(entry.Key, headers); } } @@ -364,7 +363,7 @@ private static IResponseBuilder InitResponseBuilder(ResponseModel responseModel) } else { - var headers = JsonUtils.ParseJTokenToObject(entry.Value); + var headers = _settings.DefaultJsonSerializer.ParseJsonToken(entry.Value); responseBuilder.WithTrailingHeader(entry.Key, headers); } } diff --git a/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs b/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs index 7ecbd3a41..af3a8c00a 100644 --- a/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs +++ b/src/WireMock.Net.Minimal/Settings/WireMockServerSettingsParser.cs @@ -3,12 +3,14 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Linq; using JetBrains.Annotations; +using JsonConverter.Newtonsoft.Json; +using JsonConverter.System.Text.Json; using Stef.Validation; using WireMock.Constants; using WireMock.Logging; using WireMock.Models; +using WireMock.Transformers; using WireMock.Types; using WireMock.Util; @@ -57,11 +59,9 @@ public static bool TryParseArguments(string[] args, IDictionary? environment, [N DisableRequestBodyDecompressing = parser.GetBoolValue(nameof(WireMockServerSettings.DisableRequestBodyDecompressing)), DisableDeserializeFormUrlEncoded = parser.GetBoolValue(nameof(WireMockServerSettings.DisableDeserializeFormUrlEncoded)), DoNotSaveDynamicResponseInLogEntry = parser.GetBoolValue(nameof(WireMockServerSettings.DoNotSaveDynamicResponseInLogEntry)), - GraphQLSchemas = parser.GetObjectValueFromJson>(nameof(settings.GraphQLSchemas)), HandleRequestsSynchronously = parser.GetBoolValue(nameof(WireMockServerSettings.HandleRequestsSynchronously)), HostingScheme = parser.GetEnumValue(nameof(WireMockServerSettings.HostingScheme)), MaxRequestLogCount = parser.GetIntValue(nameof(WireMockServerSettings.MaxRequestLogCount)), - ProtoDefinitions = parser.GetObjectValueFromJson>(nameof(settings.ProtoDefinitions)), QueryParameterMultipleValueSupport = parser.GetEnumValue(nameof(WireMockServerSettings.QueryParameterMultipleValueSupport)), ReadStaticMappings = parser.GetBoolValue(nameof(WireMockServerSettings.ReadStaticMappings)), RequestLogExpirationDuration = parser.GetIntValue(nameof(WireMockServerSettings.RequestLogExpirationDuration)), @@ -80,6 +80,7 @@ public static bool TryParseArguments(string[] args, IDictionary? environment, [N settings.AcceptAnyClientCertificate = parser.GetBoolValue(nameof(WireMockServerSettings.AcceptAnyClientCertificate)); #endif + ParseJsonSerializerSettings(settings, parser); ParseLoggerSettings(settings, logger, parser); ParsePortSettings(settings, parser); ParseProxyAndRecordSettings(settings, parser); @@ -88,6 +89,9 @@ public static bool TryParseArguments(string[] args, IDictionary? environment, [N ParseActivityTracingSettings(settings, parser); ParseWebSocketSettings(settings, parser); + settings.GraphQLSchemas = parser.GetObjectValueFromJson>(nameof(settings.GraphQLSchemas), settings.DefaultJsonSerializer); + settings.ProtoDefinitions = parser.GetObjectValueFromJson>(nameof(settings.ProtoDefinitions), settings.DefaultJsonSerializer); + return true; } @@ -259,4 +263,21 @@ private static void ParseWebSocketSettings(WireMockServerSettings settings, Simp }; } } + + private static void ParseJsonSerializerSettings(WireMockServerSettings settings, SimpleSettingsParser parser) + { + var defaultJsonSerializer = parser.GetStringValue(nameof(WireMockServerSettings.DefaultJsonSerializer)); + settings.DefaultJsonSerializer = defaultJsonSerializer switch + { + nameof(SystemTextJsonConverter) => new SystemTextJsonConverter(), + _ => new NewtonsoftJsonConverter(), + }; + + var defaultJsonBodyTransformer = parser.GetStringValue(nameof(WireMockServerSettings.DefaultJsonBodyTransformer)); + settings.DefaultJsonBodyTransformer = defaultJsonBodyTransformer switch + { + nameof(SystemTextJsonBodyTransformer) => new SystemTextJsonBodyTransformer(settings), + _ => new NewtonsoftJsonBodyTransformer(settings), + }; + } } \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Transformers/Handlebars/IHandlebarsContext.cs b/src/WireMock.Net.Minimal/Transformers/Handlebars/IHandlebarsContext.cs index 619e11e43..6bb00cdbd 100644 --- a/src/WireMock.Net.Minimal/Transformers/Handlebars/IHandlebarsContext.cs +++ b/src/WireMock.Net.Minimal/Transformers/Handlebars/IHandlebarsContext.cs @@ -1,6 +1,7 @@ // Copyright © WireMock.Net using HandlebarsDotNet; +using WireMock.Transformers; namespace WireMock.Transformers.Handlebars; diff --git a/src/WireMock.Net.Minimal/Transformers/ITransformerContext.cs b/src/WireMock.Net.Minimal/Transformers/ITransformerContext.cs deleted file mode 100644 index 8f7eeef69..000000000 --- a/src/WireMock.Net.Minimal/Transformers/ITransformerContext.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Copyright © WireMock.Net - -using WireMock.Handlers; - -namespace WireMock.Transformers; - -internal interface ITransformerContext -{ - IFileSystemHandler FileSystemHandler { get; } - - string ParseAndRender(string text, object model); - - object? ParseAndEvaluate(string text, object model); -} \ No newline at end of file diff --git a/src/WireMock.Net.Minimal/Transformers/Transformer.cs b/src/WireMock.Net.Minimal/Transformers/Transformer.cs index 6fa6b1219..276bac1d4 100644 --- a/src/WireMock.Net.Minimal/Transformers/Transformer.cs +++ b/src/WireMock.Net.Minimal/Transformers/Transformer.cs @@ -1,10 +1,5 @@ // Copyright © WireMock.Net -using System.Collections; -using System.Linq; -using HandlebarsDotNet.Helpers.Models; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using Stef.Validation; using WireMock.Settings; using WireMock.Types; @@ -14,17 +9,13 @@ namespace WireMock.Transformers; internal class Transformer : ITransformer { - private readonly JsonSerializer _jsonSerializer; + private readonly IJsonBodyTransformer _jsonBodyTransformer; private readonly ITransformerContextFactory _factory; public Transformer(WireMockServerSettings settings, ITransformerContextFactory factory) { _factory = Guard.NotNull(factory); - - _jsonSerializer = new JsonSerializer - { - Culture = Guard.NotNull(settings).Culture - }; + _jsonBodyTransformer = Guard.NotNull(settings).DefaultJsonBodyTransformer; } public IBodyData? TransformBody( @@ -121,13 +112,17 @@ public ResponseMessage Transform(IMapping mapping, IRequestMessage requestMessag }); } - private IBodyData? TransformBodyData(ITransformerContext transformerContext, ReplaceNodeOptions options, TransformModel model, IBodyData original, bool useTransformerForBodyAsFile) + private BodyData? TransformBodyData(ITransformerContext transformerContext, ReplaceNodeOptions options, TransformModel model, IBodyData original, bool useTransformerForBodyAsFile) { switch (original.DetectedBodyType) { case BodyType.Json: case BodyType.ProtoBuf: - return TransformBodyAsJson(transformerContext, options, model, original); + return _jsonBodyTransformer.TransformBodyAsJson( + transformerContext, + options, + model, + original); case BodyType.File: return TransformBodyAsFile(transformerContext, model, original, useTransformerForBodyAsFile); @@ -159,185 +154,7 @@ private static IDictionary> TransformHeaders(ITrans return newHeaders; } - private IBodyData TransformBodyAsJson(ITransformerContext transformerContext, ReplaceNodeOptions options, object model, IBodyData original) - { - JToken? jToken = null; - switch (original.BodyAsJson) - { - case JObject bodyAsJObject: - jToken = bodyAsJObject.DeepClone(); - WalkNode(transformerContext, options, jToken, model); - break; - - case JArray bodyAsJArray: - jToken = bodyAsJArray.DeepClone(); - WalkNode(transformerContext, options, jToken, model); - break; - - case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string: - jToken = JArray.FromObject(bodyAsEnumerable, _jsonSerializer); - WalkNode(transformerContext, options, jToken, model); - break; - - case string bodyAsString: - jToken = ReplaceSingleNode(transformerContext, options, bodyAsString, model); - break; - - case not null: - jToken = JObject.FromObject(original.BodyAsJson, _jsonSerializer); - WalkNode(transformerContext, options, jToken, model); - break; - } - - return new BodyData - { - Encoding = original.Encoding, - DetectedBodyType = original.DetectedBodyType, - DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, - ProtoDefinition = original.ProtoDefinition, - ProtoBufMessageType = original.ProtoBufMessageType, - BodyAsJson = jToken - }; - } - - private JToken ReplaceSingleNode(ITransformerContext transformerContext, ReplaceNodeOptions options, string stringValue, object model) - { - var transformedString = transformerContext.ParseAndRender(stringValue, model); - - if (!string.Equals(stringValue, transformedString)) - { - const string property = "_"; - JObject dummy = JObject.Parse($"{{ \"{property}\": null }}"); - if (dummy[property] == null) - { - // TODO: check if just returning null is fine - return string.Empty; - } - - JToken node = dummy[property]!; - - ReplaceNodeValue(options, node, transformedString); - - return dummy[property]!; - } - - return stringValue; - } - - private void WalkNode(ITransformerContext transformerContext, ReplaceNodeOptions options, JToken node, object model) - { - switch (node.Type) - { - 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(transformerContext, options, child.Value, model); - } - break; - - 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(transformerContext, options, child, model); - } - break; - - case JTokenType.String: - // In case of string, try to transform the value. - var stringValue = node.Value(); - if (string.IsNullOrEmpty(stringValue)) - { - return; - } - - var transformed = transformerContext.ParseAndEvaluate(stringValue!, model); - if (!Equals(stringValue, transformed)) - { - ReplaceNodeValue(options, node, transformed); - } - break; - } - } - - // ReSharper disable once UnusedParameter.Local - private void ReplaceNodeValue(ReplaceNodeOptions options, JToken node, object? transformedValue) - { - switch (transformedValue) - { - case JValue jValue: - node.Replace(jValue); - return; - - case string transformedString: - var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString); - if (isConvertedFromString) - { - node.Replace(JToken.FromObject(convertedValueFromString, _jsonSerializer)); - } - else - { - node.Replace(ParseAsJObject(transformedString)); - } - break; - - 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 { }: - var (isConverted, convertedValue) = TryConvert(options, transformedValue); - if (isConverted) - { - node.Replace(JToken.FromObject(convertedValue, _jsonSerializer)); - } - return; - - default: // It's null, skip it. Maybe remove it ? - return; - } - } - - private static JToken ParseAsJObject(string stringValue) - { - return JsonUtils.TryParseAsJObject(stringValue, out var parsedAsjObject) ? parsedAsjObject : stringValue; - } - - private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value) - { - var valueAsString = value as string; - - if (options == ReplaceNodeOptions.Evaluate) - { - if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded)) - { - return (true, decoded); - } - - return (false, value); - } - - if (valueAsString != null) - { - return WrappedString.TryDecode(valueAsString, out var decoded) ? - (true, decoded) : - StringUtils.TryConvertToKnownType(valueAsString); - } - - return (false, value); - } - - private static IBodyData TransformBodyAsString(ITransformerContext transformerContext, object model, IBodyData original) + private static BodyData TransformBodyAsString(ITransformerContext transformerContext, object model, IBodyData original) { return new BodyData { @@ -348,7 +165,7 @@ private static IBodyData TransformBodyAsString(ITransformerContext transformerCo }; } - private static IBodyData TransformBodyAsFile(ITransformerContext transformerContext, object model, IBodyData original, bool useTransformerForBodyAsFile) + private static BodyData TransformBodyAsFile(ITransformerContext transformerContext, object model, IBodyData original, bool useTransformerForBodyAsFile) { var transformedBodyAsFilename = transformerContext.ParseAndRender(original.BodyAsFile!, model); diff --git a/src/WireMock.Net.Minimal/WireMock.Net.Minimal.csproj b/src/WireMock.Net.Minimal/WireMock.Net.Minimal.csproj index a5377fc85..cf12cb58a 100644 --- a/src/WireMock.Net.Minimal/WireMock.Net.Minimal.csproj +++ b/src/WireMock.Net.Minimal/WireMock.Net.Minimal.csproj @@ -1,4 +1,4 @@ - + Minimal version from the lightweight Http Mocking Server for .NET WireMock.Net.Minimal diff --git a/src/WireMock.Net.NUnit/WireMock.Net.NUnit.csproj b/src/WireMock.Net.NUnit/WireMock.Net.NUnit.csproj index de0ac225f..58f00fd02 100644 --- a/src/WireMock.Net.NUnit/WireMock.Net.NUnit.csproj +++ b/src/WireMock.Net.NUnit/WireMock.Net.NUnit.csproj @@ -1,4 +1,4 @@ - + Some extensions for NUnit WireMock.Net.NUnit @@ -25,7 +25,7 @@ - + diff --git a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithBody.cs b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithBody.cs index b01a305f9..35f6930d2 100644 --- a/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithBody.cs +++ b/src/WireMock.Net.RestClient.AwesomeAssertions/Assertions/WireMockAdminApiAssertions.WithBody.cs @@ -32,15 +32,15 @@ public AndConstraint WithBody(IStringMatcher matcher } [CustomAssertion] - public AndConstraint WithBodyAsJson(object body, string because = "", params object[] becauseArgs) + public AndConstraint WithBodyAsJson(object body, IJsonMatcher? jsonMatcher = null, string because = "", params object[] becauseArgs) { - return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs); + return WithBodyAsJson(jsonMatcher ?? new JsonMatcher(body), because, becauseArgs); } [CustomAssertion] - public AndConstraint WithBodyAsJson(string body, string because = "", params object[] becauseArgs) + public AndConstraint WithBodyAsJson(string body, IJsonMatcher? jsonMatcher = null, string because = "", params object[] becauseArgs) { - return WithBodyAsJson(new JsonMatcher(body), because, becauseArgs); + return WithBodyAsJson(jsonMatcher ?? new JsonMatcher(body), because, becauseArgs); } [CustomAssertion] @@ -127,15 +127,44 @@ private AndConstraint ExecuteAssertionWithBodyAsIObj private static string? FormatBody(object? body) { - return body switch + if (body == null) { - null => null, - string str => str, - AnyOf[] stringPatterns => FormatBodies(stringPatterns.Select(p => p.GetPattern())), - byte[] bytes => $"byte[{bytes.Length}] {{...}}", - JToken jToken => jToken.ToString(Formatting.None), - _ => JToken.FromObject(body).ToString(Formatting.None) - }; + return null; + } + + if (body is string str) + { + return str; + } + + if (body is AnyOf[] stringPatterns) + { + return FormatBodies(stringPatterns.Select(p => p.GetPattern())); + } + + if (body is byte[] bytes) + { + return $"byte[{bytes.Length}] {{...}}"; + } + + if (body is JToken jToken) + { + return jToken.ToString(Formatting.None); + } + + // System.IO.FileNotFoundException : Could not load file or assembly 'System.Text.Json, Version=10.0.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'. The system cannot find the file specified. + var typeName = body.GetType().FullName; + if (typeName == "System.Text.Json.JsonElement") + { + return ((dynamic)body).GetRawText(); + } + + if (typeName == "System.Text.Json.JsonDocument") + { + return ((dynamic)body).RootElement.GetRawText(); + } + + return JToken.FromObject(body).ToString(Formatting.None); } private static string? FormatBodies(IEnumerable bodies) diff --git a/src/WireMock.Net.RestClient/WireMock.Net.RestClient.csproj b/src/WireMock.Net.RestClient/WireMock.Net.RestClient.csproj index 688103bd2..5d023f6f4 100644 --- a/src/WireMock.Net.RestClient/WireMock.Net.RestClient.csproj +++ b/src/WireMock.Net.RestClient/WireMock.Net.RestClient.csproj @@ -33,7 +33,7 @@ - + diff --git a/src/WireMock.Net.Shared/Matchers/IJsonPathMatcher.cs b/src/WireMock.Net.Shared/Matchers/IJsonPathMatcher.cs new file mode 100644 index 000000000..b2cc178ce --- /dev/null +++ b/src/WireMock.Net.Shared/Matchers/IJsonPathMatcher.cs @@ -0,0 +1,11 @@ +// Copyright © WireMock.Net + +namespace WireMock.Matchers; + +/// +/// IJsonPathMatcher +/// and . +/// +public interface IJsonPathMatcher : IStringMatcher, IObjectMatcher +{ +} \ No newline at end of file diff --git a/src/WireMock.Net.Shared/Matchers/ISystemTextJsonPathMatcher.cs b/src/WireMock.Net.Shared/Matchers/ISystemTextJsonPathMatcher.cs new file mode 100644 index 000000000..93de7a229 --- /dev/null +++ b/src/WireMock.Net.Shared/Matchers/ISystemTextJsonPathMatcher.cs @@ -0,0 +1,11 @@ +// Copyright © WireMock.Net + +namespace WireMock.Matchers; + +/// +/// ISystemTextJsonPathMatcher +/// . +/// +public interface ISystemTextJsonPathMatcher : IJsonPathMatcher +{ +} \ No newline at end of file diff --git a/src/WireMock.Net.Shared/Properties/AssemblyInfo.cs b/src/WireMock.Net.Shared/Properties/AssemblyInfo.cs index 9bb112fd0..31959313a 100644 --- a/src/WireMock.Net.Shared/Properties/AssemblyInfo.cs +++ b/src/WireMock.Net.Shared/Properties/AssemblyInfo.cs @@ -7,9 +7,10 @@ [assembly: InternalsVisibleTo("WireMock.Net.GraphQL, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] [assembly: InternalsVisibleTo("WireMock.Net.ProtoBuf, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] [assembly: InternalsVisibleTo("WireMock.Net.Matchers.CSharpCode, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] +[assembly: InternalsVisibleTo("WireMock.Net.Matchers.SystemTextJsonPath, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] [assembly: InternalsVisibleTo("WireMock.Net.OpenTelemetry, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] // [assembly: InternalsVisibleTo("WireMock.Net.StandAlone, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] [assembly: InternalsVisibleTo("WireMock.Net.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100e138ec44d93acac565953052636eb8d5e7e9f27ddb030590055cd1a0ab2069a5623f1f77ca907d78e0b37066ca0f6d63da7eecc3fcb65b76aa8ebeccf7ebe1d11264b8404cd9b1cbbf2c83f566e033b3e54129f6ef28daffff776ba7aebbc53c0d635ebad8f45f78eb3f7e0459023c218f003416e080f96a1a3c5ffeb56bee9e")] // Needed for Moq in the UnitTest project -[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] \ No newline at end of file +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] \ No newline at end of file diff --git a/src/WireMock.Net.Shared/ResponseBuilders/IBodyResponseBuilder.cs b/src/WireMock.Net.Shared/ResponseBuilders/IBodyResponseBuilder.cs index a3ca1d0bf..b1ecfd10b 100644 --- a/src/WireMock.Net.Shared/ResponseBuilders/IBodyResponseBuilder.cs +++ b/src/WireMock.Net.Shared/ResponseBuilders/IBodyResponseBuilder.cs @@ -1,8 +1,6 @@ // Copyright © WireMock.Net -using System; using System.Text; -using System.Threading.Tasks; using JsonConverter.Abstractions; using WireMock.Models; @@ -19,8 +17,10 @@ public interface IBodyResponseBuilder : IFaultResponseBuilder /// The body. /// The Body Destination format (SameAsSource, String or Bytes). /// The body encoding. + /// The JSON converter. + /// The JSON converter options. /// A . - IResponseBuilder WithBody(string body, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null); + IResponseBuilder WithBody(string body, string? destination = BodyDestinationFormat.SameAsSource, Encoding? encoding = null, IJsonConverter? jsonConverter = null, JsonConverterOptions? options = null); /// /// WithBody : Create a ... response based on a callback function. diff --git a/src/WireMock.Net.Shared/Serialization/JsonSerializationConstants.cs b/src/WireMock.Net.Shared/Serialization/JsonSerializationConstants.cs index f1b31ef32..892a3ef91 100644 --- a/src/WireMock.Net.Shared/Serialization/JsonSerializationConstants.cs +++ b/src/WireMock.Net.Shared/Serialization/JsonSerializationConstants.cs @@ -14,16 +14,16 @@ internal static class JsonSerializationConstants IgnoreNullValues = true }; - //internal static readonly JsonSerializerSettings JsonSerializerSettingsDefault = new() - //{ - // Formatting = Formatting.Indented, - // NullValueHandling = NullValueHandling.Ignore - //}; + internal static readonly JsonConverterOptions JsonConverterOptionsIncludeNullValues = new() + { + WriteIndented = true, + IgnoreNullValues = false + }; - internal static readonly JsonSerializerSettings JsonSerializerSettingsIncludeNullValues = new() + internal static readonly JsonConverterOptions JsonConverterOptionsWithDateParsingNone = new() { - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Include + WriteIndented = true, + DateParseHandling = 0 }; internal static readonly JsonSerializerSettings JsonDeserializerSettingsWithDateParsingNone = new() diff --git a/src/WireMock.Net.Shared/Settings/SimpleSettingsParser.cs b/src/WireMock.Net.Shared/Settings/SimpleSettingsParser.cs index 5a0c149c1..013402667 100644 --- a/src/WireMock.Net.Shared/Settings/SimpleSettingsParser.cs +++ b/src/WireMock.Net.Shared/Settings/SimpleSettingsParser.cs @@ -1,6 +1,7 @@ // Copyright © WireMock.Net using System.Collections; +using JsonConverter.Abstractions; using WireMock.Extensions; using WireMock.Util; @@ -191,9 +192,9 @@ public string GetStringValue(string name, string defaultValue) return GetValue(name, values => values.FirstOrDefault()); } - public T? GetObjectValueFromJson(string name) + public T? GetObjectValueFromJson(string name, IJsonConverter jsonConverter) { var value = GetValue(name, values => values.FirstOrDefault()); - return string.IsNullOrWhiteSpace(value) ? default : JsonUtils.DeserializeObject(value!); + return string.IsNullOrWhiteSpace(value) ? default : jsonConverter.Deserialize(value!); } } diff --git a/src/WireMock.Net.Shared/Settings/WireMockServerSettings.cs b/src/WireMock.Net.Shared/Settings/WireMockServerSettings.cs index 1f1b343c6..4d91fc344 100644 --- a/src/WireMock.Net.Shared/Settings/WireMockServerSettings.cs +++ b/src/WireMock.Net.Shared/Settings/WireMockServerSettings.cs @@ -14,6 +14,7 @@ using WireMock.Matchers; using WireMock.Models; using WireMock.RegularExpressions; +using WireMock.Transformers; using WireMock.Types; namespace WireMock.Settings; @@ -362,11 +363,31 @@ public class WireMockServerSettings /// Default is . /// [PublicAPI] - public IJsonConverter DefaultJsonSerializer { get; set; } = new NewtonsoftJsonConverter(); + public IJsonConverter DefaultJsonSerializer { get; set; } + + /// + /// Gets or sets the default JSON body transformer used for template-based JSON body transformations. + /// + /// + /// Set this property to provide a custom implementation for transforming JSON and ProtoBuf body content. + /// Default is . + /// + [PublicAPI] + [JsonIgnore] + public IJsonBodyTransformer DefaultJsonBodyTransformer { get; set; } /// /// WebSocket settings. /// [PublicAPI] public WebSocketSettings? WebSocketSettings { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public WireMockServerSettings() + { + DefaultJsonSerializer = new NewtonsoftJsonConverter(); + DefaultJsonBodyTransformer = new NewtonsoftJsonBodyTransformer(this); + } } \ No newline at end of file diff --git a/src/WireMock.Net.Shared/Transformers/IJsonBodyTransformer.cs b/src/WireMock.Net.Shared/Transformers/IJsonBodyTransformer.cs new file mode 100644 index 000000000..64b4b673e --- /dev/null +++ b/src/WireMock.Net.Shared/Transformers/IJsonBodyTransformer.cs @@ -0,0 +1,28 @@ +// Copyright © WireMock.Net + +using JetBrains.Annotations; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Transformers; + +/// +/// Defines the contract for transforming JSON-like body data using a transformer context. +/// +[PublicAPI] +public interface IJsonBodyTransformer +{ + /// + /// Transforms the JSON body using the provided transformer context and model. + /// + /// The transformer context used to render and evaluate template values. + /// The JSON node replacement behavior to apply during transformation. + /// The model used when rendering or evaluating template values. + /// The original body data to transform. + /// The transformed JSON body data. + BodyData TransformBodyAsJson( + ITransformerContext transformerContext, + ReplaceNodeOptions options, + object model, + IBodyData original); +} diff --git a/src/WireMock.Net.Shared/Transformers/ITransformerContext.cs b/src/WireMock.Net.Shared/Transformers/ITransformerContext.cs new file mode 100644 index 000000000..69dfe92e5 --- /dev/null +++ b/src/WireMock.Net.Shared/Transformers/ITransformerContext.cs @@ -0,0 +1,34 @@ +// Copyright © WireMock.Net + +using JetBrains.Annotations; +using WireMock.Handlers; + +namespace WireMock.Transformers; + +/// +/// Defines the transformer context used to render and evaluate templates during response transformation. +/// +[PublicAPI] +public interface ITransformerContext +{ + /// + /// Gets the file system handler used by the current transformer context. + /// + IFileSystemHandler FileSystemHandler { get; } + + /// + /// Renders the specified template text using the supplied model. + /// + /// The template text to render. + /// The model used during rendering. + /// The rendered text. + string ParseAndRender(string text, object model); + + /// + /// Evaluates the specified template text using the supplied model. + /// + /// The template text to evaluate. + /// The model used during evaluation. + /// The evaluated value. + object? ParseAndEvaluate(string text, object model); +} diff --git a/src/WireMock.Net.Shared/Transformers/NewtonsoftJsonBodyTransformer.cs b/src/WireMock.Net.Shared/Transformers/NewtonsoftJsonBodyTransformer.cs new file mode 100644 index 000000000..920b05fa5 --- /dev/null +++ b/src/WireMock.Net.Shared/Transformers/NewtonsoftJsonBodyTransformer.cs @@ -0,0 +1,271 @@ +// Copyright © WireMock.Net + +using System.Collections; +using HandlebarsDotNet.Helpers.Models; +using JetBrains.Annotations; +using JsonConverter.Newtonsoft.Json; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WireMock.Settings; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Transformers; + +/// +/// Default JSON body transformer implementation based on Newtonsoft.Json. +/// +/// +/// Initializes a new instance of the class. +/// +/// The server settings used to configure JSON transformation behavior. +[PublicAPI] +public class NewtonsoftJsonBodyTransformer(WireMockServerSettings settings) : IJsonBodyTransformer +{ + private static readonly NewtonsoftJsonConverter _jsonConverter = new(); + + /// + public BodyData TransformBodyAsJson( + ITransformerContext transformerContext, + ReplaceNodeOptions options, + object model, + IBodyData original) + { + var jsonSerializer = new JsonSerializer + { + Culture = settings.Culture + }; + + JToken? jToken = null; + switch (original.BodyAsJson) + { + case JObject bodyAsJObject: + jToken = bodyAsJObject.DeepClone(); + WalkNode(transformerContext, jsonSerializer, options, jToken, model); + break; + + case JArray bodyAsJArray: + jToken = bodyAsJArray.DeepClone(); + WalkNode(transformerContext, jsonSerializer, options, jToken, model); + break; + + case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string: + jToken = JArray.FromObject(bodyAsEnumerable, jsonSerializer); + WalkNode(transformerContext, jsonSerializer, options, jToken, model); + break; + + case string bodyAsString: + jToken = ReplaceSingleNode(transformerContext, jsonSerializer, options, bodyAsString, model); + break; + + case not null: + jToken = JObject.FromObject(original.BodyAsJson, jsonSerializer); + WalkNode(transformerContext, jsonSerializer, options, jToken, model); + break; + } + + return new BodyData + { + Encoding = original.Encoding, + DetectedBodyType = original.DetectedBodyType, + DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, + ProtoDefinition = original.ProtoDefinition, + ProtoBufMessageType = original.ProtoBufMessageType, + BodyAsJson = jToken + }; + } + + private JToken ParseAsJObject(string stringValue) + { + if (_jsonConverter.IsValidJson(stringValue)) + { + try + { + // Try to convert this string into a JObject + return JObject.Parse(stringValue!); + } + catch + { + settings.Logger.Warn("Failed to parse string ''{0}'' as JSON. Returning the original string value.", stringValue); + } + } + + return stringValue; + } + + private JToken ReplaceSingleNode(ITransformerContext transformerContext, JsonSerializer jsonSerializer, ReplaceNodeOptions options, string stringValue, object model) + { + var transformedString = transformerContext.ParseAndRender(stringValue, model); + + if (!string.Equals(stringValue, transformedString)) + { + const string property = "_"; + JObject dummy = JObject.Parse($"{{ \"{property}\": null }}"); + if (dummy[property] == null) + { + return string.Empty; + } + + JToken node = dummy[property]!; + + ReplaceNodeValue(jsonSerializer, options, node, transformedString); + + return dummy[property]!; + } + + return stringValue; + } + + private void WalkNode(ITransformerContext transformerContext, JsonSerializer jsonSerializer, ReplaceNodeOptions options, JToken node, object model) + { + switch (node.Type) + { + case JTokenType.Object: + foreach (var child in node.Children().ToArray()) + { + WalkNode(transformerContext, jsonSerializer, options, child.Value, model); + } + break; + + case JTokenType.Array: + foreach (var child in node.Children().ToArray()) + { + WalkNode(transformerContext, jsonSerializer, options, child, model); + } + break; + + case JTokenType.String: + var stringValue = node.Value(); + if (string.IsNullOrEmpty(stringValue)) + { + return; + } + + var transformed = transformerContext.ParseAndEvaluate(stringValue!, model); + if (!Equals(stringValue, transformed)) + { + ReplaceNodeValue(jsonSerializer, options, node, transformed); + } + break; + } + } + + private void ReplaceNodeValue(JsonSerializer jsonSerializer, ReplaceNodeOptions options, JToken node, object? transformedValue) + { + switch (transformedValue) + { + case JValue jValue: + node.Replace(jValue); + return; + + case string transformedString: + var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString); + if (isConvertedFromString) + { + node.Replace(JToken.FromObject(convertedValueFromString, jsonSerializer)); + } + else + { + node.Replace(ParseAsJObject(transformedString)); + } + break; + + 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 { }: + var (isConverted, convertedValue) = TryConvert(options, transformedValue); + if (isConverted) + { + node.Replace(JToken.FromObject(convertedValue, jsonSerializer)); + } + return; + + default: + return; + } + } + + private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value) + { + var valueAsString = value as string; + + if (options == ReplaceNodeOptions.Evaluate) + { + if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded)) + { + return (true, decoded); + } + + return (false, value); + } + + if (valueAsString != null) + { + return WrappedString.TryDecode(valueAsString, out var decoded) + ? (true, decoded) + : TryConvertToKnownType(valueAsString); + } + + return (false, value); + } + + internal static (bool IsConverted, object ConvertedValue) TryConvertToKnownType(string value) + { + if (bool.TryParse(value, out var boolResult)) + { + return (true, boolResult); + } + + if (int.TryParse(value, out var intResult)) + { + return (true, intResult); + } + + if (long.TryParse(value, out var longResult)) + { + return (true, longResult); + } + + if (double.TryParse(value, out var doubleResult)) + { + return (true, doubleResult); + } + + if (Guid.TryParseExact(value, "D", out var guidResult)) + { + return (true, guidResult); + } + + if (TimeSpan.TryParse(value, out var timeSpanResult)) + { + return (true, timeSpanResult); + } + + if (DateTime.TryParse(value, out var dateTimeResult)) + { + return (true, dateTimeResult); + } + + if ((value.StartsWith("ftp://", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("ftps://", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("https://", StringComparison.OrdinalIgnoreCase)) && + Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var uriResult)) + { + return (true, uriResult); + } + + return (false, value); + } +} diff --git a/src/WireMock.Net.Shared/Transformers/SystemTextJsonBodyTransformer.cs b/src/WireMock.Net.Shared/Transformers/SystemTextJsonBodyTransformer.cs new file mode 100644 index 000000000..66ee02dfe --- /dev/null +++ b/src/WireMock.Net.Shared/Transformers/SystemTextJsonBodyTransformer.cs @@ -0,0 +1,208 @@ +// Copyright © WireMock.Net + +using System.Collections; +using System.Text.Json; +using System.Text.Json.Nodes; +using HandlebarsDotNet.Helpers.Models; +using JetBrains.Annotations; +using JsonConverter.System.Text.Json; +using WireMock.Settings; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Transformers; + +/// +/// JSON body transformer implementation based on System.Text.Json. +/// +[PublicAPI] +public class SystemTextJsonBodyTransformer(WireMockServerSettings settings) : IJsonBodyTransformer +{ + private static readonly SystemTextJsonConverter _jsonConverter = new(); + + /// + public BodyData TransformBodyAsJson( + ITransformerContext transformerContext, + ReplaceNodeOptions options, + object model, + IBodyData original) + { + JsonNode? jsonNode = null; + switch (original.BodyAsJson) + { + case JsonObject bodyAsJsonObject: + jsonNode = CloneNode(bodyAsJsonObject); + jsonNode = WalkNode(transformerContext, options, jsonNode, model); + break; + + case JsonArray bodyAsJsonArray: + jsonNode = CloneNode(bodyAsJsonArray); + jsonNode = WalkNode(transformerContext, options, jsonNode, model); + break; + + case var bodyAsEnumerable when bodyAsEnumerable is IEnumerable and not string: + jsonNode = JsonSerializer.SerializeToNode(bodyAsEnumerable); + if (jsonNode != null) + { + jsonNode = WalkNode(transformerContext, options, jsonNode, model); + } + break; + + case string bodyAsString: + jsonNode = ReplaceSingleNode(transformerContext, options, bodyAsString, model); + break; + + case not null: + jsonNode = JsonSerializer.SerializeToNode(original.BodyAsJson); + if (jsonNode != null) + { + jsonNode = WalkNode(transformerContext, options, jsonNode, model); + } + break; + } + + return new BodyData + { + Encoding = original.Encoding, + DetectedBodyType = original.DetectedBodyType, + DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, + ProtoDefinition = original.ProtoDefinition, + ProtoBufMessageType = original.ProtoBufMessageType, + BodyAsJson = jsonNode + }; + } + + private JsonNode ParseAsJsonObject(string stringValue) + { + if (_jsonConverter.IsValidJson(stringValue)) + { + try + { + var parsed = JsonNode.Parse(stringValue); + if (parsed is JsonObject) + { + return parsed; + } + } + catch + { + settings.Logger.Warn("Failed to parse string ''{0}'' as JSON. Returning the original string value.", stringValue); + } + } + + return JsonValue.Create(stringValue)!; + } + + private JsonNode? ReplaceSingleNode(ITransformerContext transformerContext, ReplaceNodeOptions options, string stringValue, object model) + { + var transformedString = transformerContext.ParseAndRender(stringValue, model); + + if (!string.Equals(stringValue, transformedString)) + { + return ReplaceNodeValue(options, transformedString); + } + + return JsonValue.Create(stringValue); + } + + private JsonNode? WalkNode(ITransformerContext transformerContext, ReplaceNodeOptions options, JsonNode? node, object model) + { + switch (node) + { + case JsonObject jsonObject: + foreach (var property in jsonObject.ToArray()) + { + jsonObject[property.Key] = WalkNode(transformerContext, options, property.Value, model); + } + return jsonObject; + + case JsonArray jsonArray: + for (var i = 0; i < jsonArray.Count; i++) + { + jsonArray[i] = WalkNode(transformerContext, options, jsonArray[i], model); + } + return jsonArray; + + case JsonValue jsonValue when jsonValue.TryGetValue(out var stringValue): + if (string.IsNullOrEmpty(stringValue)) + { + return jsonValue; + } + + var transformed = transformerContext.ParseAndEvaluate(stringValue!, model); + return !Equals(stringValue, transformed) ? ReplaceNodeValue(options, transformed) ?? jsonValue : jsonValue; + + default: + return node; + } + } + + private JsonNode? ReplaceNodeValue(ReplaceNodeOptions options, object? transformedValue) + { + switch (transformedValue) + { + case JsonNode jsonNode: + return CloneNode(jsonNode); + + case string transformedString: + var (isConvertedFromString, convertedValueFromString) = TryConvert(options, transformedString); + return isConvertedFromString + ? JsonSerializer.SerializeToNode(convertedValueFromString) + : ParseAsJsonObject(transformedString); + + case WireMockList strings: + switch (strings.Count) + { + case 1: + return ParseAsJsonObject(strings[0]); + + case > 1: + return JsonSerializer.SerializeToNode(strings.ToArray()); + } + break; + + case { }: + var (isConverted, convertedValue) = TryConvert(options, transformedValue); + if (isConverted) + { + return JsonSerializer.SerializeToNode(convertedValue); + } + break; + } + + return null; + } + + private static JsonNode? CloneNode(JsonNode? node) + { +#if NET8_0_OR_GREATER + return node?.DeepClone(); +#else + return node == null ? null : JsonNode.Parse(node.ToJsonString()); +#endif + } + + private static (bool IsConverted, object ConvertedValue) TryConvert(ReplaceNodeOptions options, object value) + { + var valueAsString = value as string; + + if (options == ReplaceNodeOptions.Evaluate) + { + if (valueAsString != null && WrappedString.TryDecode(valueAsString, out var decoded)) + { + return (true, decoded); + } + + return (false, value); + } + + if (valueAsString != null) + { + return WrappedString.TryDecode(valueAsString, out var decoded) + ? (true, decoded) + : NewtonsoftJsonBodyTransformer.TryConvertToKnownType(valueAsString); + } + + return (false, value); + } +} \ No newline at end of file diff --git a/src/WireMock.Net.Shared/Util/BodyParser.cs b/src/WireMock.Net.Shared/Util/BodyParser.cs index 9271de8f5..18a4ae162 100644 --- a/src/WireMock.Net.Shared/Util/BodyParser.cs +++ b/src/WireMock.Net.Shared/Util/BodyParser.cs @@ -1,6 +1,5 @@ // Copyright © WireMock.Net -using System.Linq; using System.Net.Http.Headers; using System.Text; using Stef.Validation; @@ -129,11 +128,11 @@ public static async Task ParseAsync(BodyParserSettings settings) { Guard.NotNull(settings); - var bodyWithContentEncoding = await ReadBytesAsync(settings.Stream, settings.ContentEncoding, settings.DecompressGZipAndDeflate).ConfigureAwait(false); + var (ContentType, Bytes) = await ReadBytesAsync(settings.Stream, settings.ContentEncoding, settings.DecompressGZipAndDeflate).ConfigureAwait(false); var data = new BodyData { - BodyAsBytes = bodyWithContentEncoding.Bytes, - DetectedCompression = bodyWithContentEncoding.ContentType, + BodyAsBytes = Bytes, + DetectedCompression = ContentType, DetectedBodyType = BodyType.Bytes, DetectedBodyTypeFromContentType = DetectBodyTypeFromContentType(settings.ContentType) }; @@ -169,17 +168,17 @@ public static async Task ParseAsync(BodyParserSettings settings) data.DetectedBodyType = BodyType.FormUrlEncoded; } - // If string is not null or empty, try to deserialize the string to a JObject - if (settings.DeserializeJson && JsonUtils.IsJson(data.BodyAsString)) + // If string is not null or empty, try to deserialize the string + if (settings.DeserializeJson && settings.DefaultJsonConverter.IsValidJson(data.BodyAsString)) { try { - data.BodyAsJson = JsonUtils.DeserializeObject(data.BodyAsString); + data.BodyAsJson = settings.DefaultJsonConverter.Deserialize(data.BodyAsString); data.DetectedBodyType = BodyType.Json; } catch { - // JsonConvert failed, just ignore. + // JsonConverter failed, just ignore. } } } @@ -202,7 +201,7 @@ public static async Task ParseAsync(BodyParserSettings settings) return (null, data); } - public static bool IsProbablyText(byte[] data) + private static bool IsProbablyText(byte[] data) { if (data.Length == 0) { diff --git a/src/WireMock.Net.Shared/Util/BodyParserSettings.cs b/src/WireMock.Net.Shared/Util/BodyParserSettings.cs index 2fdaea0bc..3b625fde1 100644 --- a/src/WireMock.Net.Shared/Util/BodyParserSettings.cs +++ b/src/WireMock.Net.Shared/Util/BodyParserSettings.cs @@ -1,6 +1,7 @@ // Copyright © WireMock.Net -using System.IO; +using JsonConverter.Abstractions; +using JsonConverter.Newtonsoft.Json; namespace WireMock.Util; @@ -35,4 +36,13 @@ internal class BodyParserSettings /// Try to deserialize the body as FormUrlEncoded. /// public bool DeserializeFormUrlEncoded { get; set; } = true; + + /// + /// Gets or sets the default JSON converter used for deserialization. + /// + /// + /// Set this property to customize how objects are serialized to and deserialized from JSON during mapping. + /// Default is . + /// + public IJsonConverter DefaultJsonConverter { get; set; } = new NewtonsoftJsonConverter(); } \ No newline at end of file diff --git a/src/WireMock.Net.Shared/Util/JsonUtils.cs b/src/WireMock.Net.Shared/Util/JsonUtils.cs deleted file mode 100644 index 0c5e95715..000000000 --- a/src/WireMock.Net.Shared/Util/JsonUtils.cs +++ /dev/null @@ -1,137 +0,0 @@ -// Copyright © WireMock.Net - -using System; -using System.Collections; -using System.Diagnostics.CodeAnalysis; -using System.Text; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using WireMock.Serialization; - -namespace WireMock.Util; - -internal static class JsonUtils -{ - public static bool IsJson(string? value) - { - if (string.IsNullOrWhiteSpace(value)) - { - return false; - } - - value = value!.Trim(); - - return (value.StartsWith("{") && value.EndsWith("}")) || (value.StartsWith("[") && value.EndsWith("]")); - } - - public static bool TryParseAsJObject(string? strInput, [NotNullWhen(true)] out JObject? value) - { - value = null; - - if (!IsJson(strInput)) - { - return false; - } - - try - { - // Try to convert this string into a JToken - value = JObject.Parse(strInput!); - return true; - } - catch - { - return false; - } - } - - public static string Serialize(object value) - { - return JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsIncludeNullValues); - } - - public static byte[] SerializeAsPactFile(object value) - { - var json = JsonConvert.SerializeObject(value, JsonSerializationConstants.JsonSerializerSettingsPact); - return Encoding.UTF8.GetBytes(json); - } - - /// - /// Load a Newtonsoft.Json.Linq.JObject from a string that contains JSON. - /// Using : DateParseHandling = DateParseHandling.None - /// - /// A System.String that contains JSON. - /// A Newtonsoft.Json.Linq.JToken populated from the string that contains JSON. - public static JToken Parse(string json) - { - return JsonConvert.DeserializeObject(json, JsonSerializationConstants.JsonDeserializerSettingsWithDateParsingNone)!; - } - - /// - /// Deserializes the JSON to a .NET object. - /// Using : DateParseHandling = DateParseHandling.None - /// - /// A System.String that contains JSON. - /// The deserialized object from the JSON string. - public static object DeserializeObject(string json) - { - return JsonConvert.DeserializeObject(json, JsonSerializationConstants.JsonDeserializerSettingsWithDateParsingNone)!; - } - - /// - /// Deserializes the JSON to the specified .NET type. - /// Using : DateParseHandling = DateParseHandling.None - /// - /// A System.String that contains JSON. - /// The deserialized object from the JSON string. - public static T DeserializeObject(string json) - { - return JsonConvert.DeserializeObject(json, JsonSerializationConstants.JsonDeserializerSettingsWithDateParsingNone)!; - } - - public static T? TryDeserializeObject(string json) - { - try - { - return JsonConvert.DeserializeObject(json); - } - catch - { - return default; - } - } - - public static T ParseJTokenToObject(object? value) - { - if (value != null && value.GetType() == typeof(T)) - { - return (T)value; - } - - return value switch - { - JToken tokenValue => tokenValue.ToObject()!, - - _ => throw new NotSupportedException($"Unable to convert value to {typeof(T)}.") - }; - } - - public static JToken ConvertValueToJToken(object value) - { - // Check if JToken, string, IEnumerable or object - switch (value) - { - case JToken tokenValue: - return tokenValue; - - case string stringValue: - return Parse(stringValue); - - case IEnumerable enumerableValue: - return JArray.FromObject(enumerableValue); - - default: - return JObject.FromObject(value); - } - } -} \ No newline at end of file diff --git a/src/WireMock.Net.Shared/Util/TypeLoader.cs b/src/WireMock.Net.Shared/Util/TypeLoader.cs index 772697332..b50a6af38 100644 --- a/src/WireMock.Net.Shared/Util/TypeLoader.cs +++ b/src/WireMock.Net.Shared/Util/TypeLoader.cs @@ -3,7 +3,6 @@ using System.Collections.Concurrent; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; -using System.Linq; using System.Reflection; using Stef.Validation; diff --git a/src/WireMock.Net.Shared/WireMock.Net.Shared.csproj b/src/WireMock.Net.Shared/WireMock.Net.Shared.csproj index 10d5b8a5b..8bfea33a9 100644 --- a/src/WireMock.Net.Shared/WireMock.Net.Shared.csproj +++ b/src/WireMock.Net.Shared/WireMock.Net.Shared.csproj @@ -30,7 +30,8 @@ - + + diff --git a/src/WireMock.Net/WireMock.Net.csproj b/src/WireMock.Net/WireMock.Net.csproj index cf0f3a6ba..e6c1ff72d 100644 --- a/src/WireMock.Net/WireMock.Net.csproj +++ b/src/WireMock.Net/WireMock.Net.csproj @@ -34,5 +34,6 @@ + \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Matchers/SystemTextJsonMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/SystemTextJsonMatcherTests.cs new file mode 100644 index 000000000..6a97284a7 --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/SystemTextJsonMatcherTests.cs @@ -0,0 +1,370 @@ +// Copyright © WireMock.Net + +using System.Text.Json; +using WireMock.Matchers; + +namespace WireMock.Net.Tests.Matchers; + +public class SystemTextJsonMatcherTests +{ + public enum NormalEnumStj + { + Abc + } + + public class Test1Stj + { + public NormalEnumStj NormalEnum { get; set; } + } + + [Fact] + public void SystemTextJsonMatcher_GetName() + { + // Assign + var matcher = new SystemTextJsonMatcher("{}"); + + // Act + var name = matcher.Name; + + // Assert + name.Should().Be("SystemTextJsonMatcher"); + } + + [Fact] + public void SystemTextJsonMatcher_GetValue() + { + // Assign + var matcher = new SystemTextJsonMatcher("{}"); + + // Act + var value = matcher.Value; + + // Assert + value.Should().Be("{}"); + } + + [Fact] + public void SystemTextJsonMatcher_WithInvalidStringValue_Should_ThrowException() + { + // Act + Action action = () => new SystemTextJsonMatcher(MatchBehaviour.AcceptOnMatch, "{ \"Id\""); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void SystemTextJsonMatcher_WithInvalidObjectValue_Should_ThrowException() + { + // Act + Action action = () => new SystemTextJsonMatcher(MatchBehaviour.AcceptOnMatch, new MemoryStream()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_WithInvalidValue_Should_ReturnMismatch_And_Exception_ShouldBeSet() + { + // Assign + var matcher = new SystemTextJsonMatcher("{}"); + using var stream = new MemoryStream(); + + // Act + var result = matcher.IsMatch(stream); + + // Assert + result.Score.Should().Be(MatchScores.Mismatch); + result.Exception.Should().NotBeNull(); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_ByteArray() + { + // Assign + var bytes = new byte[0]; + var matcher = new SystemTextJsonMatcher("{}"); + + // Act + var match = matcher.IsMatch(bytes).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_NullString() + { + // Assign + string? s = null; + var matcher = new SystemTextJsonMatcher("{}"); + + // Act + var match = matcher.IsMatch(s).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_NullObject() + { + // Assign + object? o = null; + var matcher = new SystemTextJsonMatcher("{}"); + + // Act + var match = matcher.IsMatch(o).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_JsonArrayAsString() + { + // Assign + var matcher = new SystemTextJsonMatcher("[ \"x\", \"y\" ]"); + + // Act + var jsonElement = JsonDocument.Parse("[ \"x\", \"y\" ]").RootElement; + var match = matcher.IsMatch(jsonElement).Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_JsonObjectAsString_ShouldMatch() + { + // Assign + var matcher = new SystemTextJsonMatcher("{ \"Id\" : 1, \"Name\" : \"Test\" }"); + + // Act + var jsonElement = JsonDocument.Parse("{ \"Id\" : 1, \"Name\" : \"Test\" }").RootElement; + var match = matcher.IsMatch(jsonElement).Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_AnonymousObject_ShouldMatch() + { + // Assign + var matcher = new SystemTextJsonMatcher(new { Id = 1, Name = "Test" }); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_AnonymousObject_ShouldNotMatch() + { + // Assign + var matcher = new SystemTextJsonMatcher(new { Id = 1, Name = "Test" }); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\", \"Other\" : \"abc\" }").Score; + + // Assert + Assert.Equal(MatchScores.Mismatch, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_WithIgnoreCaseTrue_JsonObject() + { + // Assign + var matcher = new SystemTextJsonMatcher(new { id = 1, Name = "test" }, true); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"NaMe\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_WithIgnoreCaseTrue_JsonObjectParsed() + { + // Assign + var matcher = new SystemTextJsonMatcher(new { Id = 1, Name = "TESt" }, true); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_JsonObjectAsString_RejectOnMatch() + { + // Assign + var matcher = new SystemTextJsonMatcher(MatchBehaviour.RejectOnMatch, "{ \"Id\" : 1, \"Name\" : \"Test\" }"); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(0.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_JsonObjectWithDateTimeOffsetAsString() + { + // Assign + var matcher = new SystemTextJsonMatcher("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }"); + + // Act + var match = matcher.IsMatch("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_NormalEnum() + { + // Assign + var matcher = new SystemTextJsonMatcher(new Test1Stj { NormalEnum = NormalEnumStj.Abc }); + + // Act + var match = matcher.IsMatch("{ \"NormalEnum\" : 0 }").Score; + + // Assert + match.Should().Be(1.0); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_WithRegexTrue_ShouldMatch() + { + // Assign + var matcher = new SystemTextJsonMatcher(new { Id = "^\\d+$", Name = "Test" }, regex: true); + + // Act + var match = matcher.IsMatch("{ \"Id\" : \"42\", \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_WithRegexTrue_Complex_ShouldMatch() + { + // Assign + var matcher = new SystemTextJsonMatcher(new + { + Complex = new + { + Id = "^\\d+$", + Name = ".*" + } + }, regex: true); + + // Act + var match = matcher.IsMatch("{ \"Complex\" : { \"Id\" : \"42\", \"Name\" : \"Test\" } }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_WithRegexTrue_Complex_ShouldNotMatch() + { + // Assign + var matcher = new SystemTextJsonMatcher(new + { + Complex = new + { + Id = "^\\d+$", + Name = ".*" + } + }, regex: true); + + // Act + var match = matcher.IsMatch("{ \"Complex\" : { \"Id\" : \"42\", \"Name\" : \"Test\", \"Other\" : \"Other\" } }").Score; + + // Assert + Assert.Equal(MatchScores.Mismatch, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_WithRegexTrue_Array_ShouldMatch() + { + // Assign + var matcher = new SystemTextJsonMatcher(new + { + Array = new[] { "^\\d+$", ".*" } + }, regex: true); + + // Act + var match = matcher.IsMatch("{ \"Array\" : [ \"42\", \"test\" ] }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_WithRegexTrue_Array_ShouldNotMatch() + { + // Assign + var matcher = new SystemTextJsonMatcher(new + { + Array = new[] { "^\\d+$", ".*" } + }, regex: true); + + // Act + var match = matcher.IsMatch("{ \"Array\" : [ \"42\", \"test\", \"other\" ] }").Score; + + // Assert + Assert.Equal(MatchScores.Mismatch, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_GuidAndString() + { + // Assign + var id = Guid.NewGuid(); + var idAsString = id.ToString(); + var matcher = new SystemTextJsonMatcher(new { Id = id }); + + // Act + var match = matcher.IsMatch($"{{ \"Id\" : \"{idAsString}\" }}").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_StringAndGuid() + { + // Assign + var id = Guid.NewGuid(); + var idAsString = id.ToString(); + var matcher = new SystemTextJsonMatcher(new { Id = idAsString }); + + // Act + var match = matcher.IsMatch($"{{ \"Id\" : \"{id}\" }}").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonMatcher_IsMatch_JsonElement_ShouldMatch() + { + // Assign + var matcher = new SystemTextJsonMatcher(new { Id = 1, Name = "Test" }); + + // Act + var jsonElement = JsonDocument.Parse("{ \"Id\" : 1, \"Name\" : \"Test\" }").RootElement; + var match = matcher.IsMatch(jsonElement).Score; + + // Assert + Assert.Equal(1.0, match); + } +} diff --git a/test/WireMock.Net.Tests/Matchers/SystemTextJsonPartialMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/SystemTextJsonPartialMatcherTests.cs new file mode 100644 index 000000000..81b1ec7dc --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/SystemTextJsonPartialMatcherTests.cs @@ -0,0 +1,411 @@ +// Copyright © WireMock.Net + +using System.Text.Json; +using WireMock.Matchers; + +namespace WireMock.Net.Tests.Matchers; + +public class SystemTextJsonPartialMatcherTests +{ + [Fact] + public void SystemTextJsonPartialMatcher_GetName() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher("{}"); + + // Act + string name = matcher.Name; + + // Assert + name.Should().Be("SystemTextJsonPartialMatcher"); + } + + [Fact] + public void SystemTextJsonPartialMatcher_GetValue() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher("{}"); + + // Act + object value = matcher.Value; + + // Assert + value.Should().Be("{}"); + } + + [Fact] + public void SystemTextJsonPartialMatcher_WithInvalidStringValue_Should_ThrowException() + { + // Act + Action action = () => new SystemTextJsonPartialMatcher(MatchBehaviour.AcceptOnMatch, "{ \"Id\""); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void SystemTextJsonPartialMatcher_WithInvalidObjectValue_Should_ThrowException() + { + // Act + Action action = () => new SystemTextJsonPartialMatcher(MatchBehaviour.AcceptOnMatch, new MemoryStream()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_WithInvalidValue_Should_ReturnMismatch_And_Exception_ShouldBeSet() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher("{}"); + using var stream = new MemoryStream(); + + // Act + var result = matcher.IsMatch(stream); + + // Assert + result.Score.Should().Be(MatchScores.Mismatch); + result.Exception.Should().NotBeNull(); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_ByteArray() + { + // Assign + var bytes = new byte[0]; + var matcher = new SystemTextJsonPartialMatcher("{}"); + + // Act + double match = matcher.IsMatch(bytes).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_NullString() + { + // Assign + string? s = null; + var matcher = new SystemTextJsonPartialMatcher("{}"); + + // Act + double match = matcher.IsMatch(s).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_NullObject() + { + // Assign + object? o = null; + var matcher = new SystemTextJsonPartialMatcher("{}"); + + // Act + double match = matcher.IsMatch(o).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_JsonArray() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(new[] { "x", "y" }); + + // Act + double match = matcher.IsMatch("[ \"x\", \"y\" ]").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_JsonObject() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(new { Id = 1, Name = "Test" }); + + // Act + double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_WithRegexTrue() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(new { Id = "^\\d+$", Name = "Test" }, false, true); + + // Act + double match = matcher.IsMatch("{ \"Id\" : \"1\", \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_WithRegexFalse() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(new { Id = "^\\d+$", Name = "Test" }); + + // Act + double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(0.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_GuidAsString_UsingRegex() + { + var guid = new Guid("1111238e-b775-44a9-a263-95e570135c94"); + var matcher = new SystemTextJsonPartialMatcher(new + { + Id = 1, + Name = "^1111[a-fA-F0-9]{4}(-[a-fA-F0-9]{4}){3}-[a-fA-F0-9]{12}$" + }, false, true); + + // Act + double match = matcher.IsMatch($"{{ \"Id\" : 1, \"Name\" : \"{guid}\" }}").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JsonObject() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(new { id = 1, Name = "test" }, true); + + // Act + double match = matcher.IsMatch("{ \"Id\" : 1, \"NaMe\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_JsonObjectParsed() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(new { Id = 1, Name = "Test" }); + + // Act + double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JsonObjectParsed() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(new { Id = 1, Name = "TESt" }, true); + + // Act + double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_JsonArrayAsString() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher("[ \"x\", \"y\" ]"); + + // Act + double match = matcher.IsMatch("[ \"x\", \"y\" ]").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_JsonObjectAsString() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher("{ \"Id\" : 1, \"Name\" : \"Test\" }"); + + // Act + double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_JsonObjectAsStringWithDottedPropertyName() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher("{ \"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\" : \"Test\" }"); + + // Act + double match = matcher.IsMatch("{ \"urn:ietf:params:scim:schemas:extension:enterprise:2.0:User\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_GuidAsString() + { + // Assign + var guid = Guid.NewGuid(); + var matcher = new SystemTextJsonPartialMatcher(new { Id = 1, Name = guid }); + + // Act + double match = matcher.IsMatch($"{{ \"Id\" : 1, \"Name\" : \"{guid}\" }}").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JsonObjectAsString() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher("{ \"Id\" : 1, \"Name\" : \"test\" }", true); + + // Act + double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_JsonObjectAsString_RejectOnMatch() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(MatchBehaviour.RejectOnMatch, "{ \"Id\" : 1, \"Name\" : \"Test\" }"); + + // Act + double match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(0.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_JsonObjectWithDateTimeOffsetAsString() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }"); + + // Act + double match = matcher.IsMatch("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Theory] + [InlineData("{\"test\":\"abc\"}", "{\"test\":\"abc\",\"other\":\"xyz\"}")] + [InlineData("\"test\"", "\"test\"")] + [InlineData("123", "123")] + [InlineData("[\"test\"]", "[\"test\"]")] + [InlineData("[\"test\"]", "[\"test\", \"other\"]")] + [InlineData("[123]", "[123]")] + [InlineData("[123]", "[123, 456]")] + [InlineData("{ \"test\":\"value\" }", "{\"test\":\"value\",\"other\":123}")] + [InlineData("{ \"test\":\"value\" }", "{\"test\":\"value\"}")] + [InlineData("{\"test\":{\"nested\":\"value\"}}", "{\"test\":{\"nested\":\"value\"}}")] + public void SystemTextJsonPartialMatcher_IsMatch_StringInputValidMatch(string value, string input) + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(value); + + // Act + double match = matcher.IsMatch(input).Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Theory] + [InlineData("\"test\"", null)] + [InlineData("\"test1\"", "\"test2\"")] + [InlineData("123", "1234")] + [InlineData("[\"test\"]", "[\"test1\"]")] + [InlineData("[\"test\"]", "[\"test1\", \"test2\"]")] + [InlineData("[123]", "[1234]")] + [InlineData("{}", "\"test\"")] + [InlineData("{ \"test\":\"value\" }", "{\"test\":\"value2\"}")] + [InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value1\"}}")] + [InlineData("{\"test\":{\"test1\":\"value\"}}", "{\"test\":{\"test1\":\"value1\"}}")] + [InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value1\"}}]")] + public void SystemTextJsonPartialMatcher_IsMatch_StringInputWithInvalidMatch(string value, string? input) + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(value); + + // Act + double match = matcher.IsMatch(input).Score; + + // Assert + Assert.Equal(0.0, match); + } + + [Theory] + [InlineData("{ \"test.nested\":123 }", "{\"test\":{\"nested\":123}}")] + [InlineData("{ \"test.nested\":[123, 456] }", "{\"test\":{\"nested\":[123, 456]}}")] + [InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value\"}}")] + [InlineData("{ \"['name.with.dot']\":\"value\" }", "{\"name.with.dot\":\"value\"}")] + [InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value\"}}]")] + [InlineData("[{ \"['name.with.dot']\":\"value\" }]", "[{\"name.with.dot\":\"value\"}]")] + public void SystemTextJsonPartialMatcher_IsMatch_ValueAsPathValidMatch(string value, string input) + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(value); + + // Act + double match = matcher.IsMatch(input).Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Theory] + [InlineData("{ \"test.nested\":123 }", "{\"test\":{\"nested\":456}}")] + [InlineData("{ \"test.nested\":[123, 456] }", "{\"test\":{\"nested\":[1, 2]}}")] + [InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value1\"}}")] + [InlineData("{ \"['name.with.dot']\":\"value\" }", "{\"name.with.dot\":\"value1\"}")] + [InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value1\"}}]")] + [InlineData("[{ \"['name.with.dot']\":\"value\" }]", "[{\"name.with.dot\":\"value1\"}]")] + public void SystemTextJsonPartialMatcher_IsMatch_ValueAsPathInvalidMatch(string value, string input) + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(value); + + // Act + double match = matcher.IsMatch(input).Score; + + // Assert + Assert.Equal(0.0, match); + } + + [Fact] + public void SystemTextJsonPartialMatcher_IsMatch_JsonElement_ShouldMatch() + { + // Assign + var matcher = new SystemTextJsonPartialMatcher(new { Id = 1, Name = "Test" }); + + // Act + var jsonElement = JsonDocument.Parse("{ \"Id\" : 1, \"Name\" : \"Test\", \"Extra\" : \"value\" }").RootElement; + double match = matcher.IsMatch(jsonElement).Score; + + // Assert + Assert.Equal(1.0, match); + } +} diff --git a/test/WireMock.Net.Tests/Matchers/SystemTextJsonPartialWildcardMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/SystemTextJsonPartialWildcardMatcherTests.cs new file mode 100644 index 000000000..057a9ecdd --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/SystemTextJsonPartialWildcardMatcherTests.cs @@ -0,0 +1,382 @@ +// Copyright © WireMock.Net + +using System.Text.Json; +using WireMock.Matchers; + +namespace WireMock.Net.Tests.Matchers; + +public class SystemTextJsonPartialWildcardMatcherTests +{ + [Fact] + public void SystemTextJsonPartialWildcardMatcher_GetName() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher("{}"); + + // Act + var name = matcher.Name; + + // Assert + name.Should().Be("SystemTextJsonPartialWildcardMatcher"); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_GetValue() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher("{}"); + + // Act + var value = matcher.Value; + + // Assert + value.Should().Be("{}"); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_WithInvalidStringValue_Should_ThrowException() + { + // Act + Action action = () => new SystemTextJsonPartialWildcardMatcher(MatchBehaviour.AcceptOnMatch, "{ \"Id\""); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_WithInvalidObjectValue_Should_ThrowException() + { + // Act + Action action = () => new SystemTextJsonPartialWildcardMatcher(MatchBehaviour.AcceptOnMatch, new MemoryStream()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithInvalidValue_Should_ReturnMismatch_And_Exception_ShouldBeSet() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher("{}"); + + // Act + using var stream = new MemoryStream(); + var result = matcher.IsMatch(stream); + + // Assert + result.Score.Should().Be(MatchScores.Mismatch); + result.Exception.Should().NotBeNull(); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_ByteArray() + { + // Assign + var bytes = new byte[0]; + var matcher = new SystemTextJsonPartialWildcardMatcher("{}"); + + // Act + var match = matcher.IsMatch(bytes).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_NullString() + { + // Assign + string? s = null; + var matcher = new SystemTextJsonPartialWildcardMatcher("{}"); + + // Act + var match = matcher.IsMatch(s).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_NullObject() + { + // Assign + object? o = null; + var matcher = new SystemTextJsonPartialWildcardMatcher("{}"); + + // Act + var match = matcher.IsMatch(o).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonArray() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(new[] { "x", "y" }); + + // Act + var match = matcher.IsMatch("[ \"x\", \"y\" ]").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonObject() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(new { Id = 1, Name = "Test" }); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithIgnoreCaseTrue_JsonObject() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(new { id = 1, Name = "test" }, true); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"NaMe\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonObjectParsed() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(new { Id = 1, Name = "Test" }); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithIgnoreCaseTrue_JsonObjectParsed() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(new { Id = 1, Name = "TESt" }, true); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonArrayAsString() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher("[ \"x\", \"y\" ]"); + + // Act + var match = matcher.IsMatch("[ \"x\", \"y\" ]").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonObjectAsString() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher("{ \"Id\" : 1, \"Name\" : \"Test\" }"); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithIgnoreCaseTrue_JsonObjectAsString() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher("{ \"Id\" : 1, \"Name\" : \"test\" }", true); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonObjectAsString_RejectOnMatch() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(MatchBehaviour.RejectOnMatch, "{ \"Id\" : 1, \"Name\" : \"Test\" }"); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Name\" : \"Test\" }").Score; + + // Assert + Assert.Equal(0.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonObjectWithDateTimeOffsetAsString() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }"); + + // Act + var match = matcher.IsMatch("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Theory] + [InlineData("{\"test\":\"abc\"}", "{\"test\":\"abc\",\"other\":\"xyz\"}")] + [InlineData("\"test\"", "\"test\"")] + [InlineData("123", "123")] + [InlineData("[\"test\"]", "[\"test\"]")] + [InlineData("[\"test\"]", "[\"test\", \"other\"]")] + [InlineData("[123]", "[123]")] + [InlineData("[123]", "[123, 456]")] + [InlineData("{ \"test\":\"value\" }", "{\"test\":\"value\",\"other\":123}")] + [InlineData("{ \"test\":\"value\" }", "{\"test\":\"value\"}")] + [InlineData("{\"test\":{\"nested\":\"value\"}}", "{\"test\":{\"nested\":\"value\"}}")] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_StringInput_IsValidMatch(string value, string input) + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(value); + + // Act + var match = matcher.IsMatch(input).Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Theory] + [InlineData("{\"test\":\"*\"}", "{\"test\":\"xxx\",\"other\":\"xyz\"}")] + [InlineData("\"t*t\"", "\"test\"")] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_StringInputWithWildcard_IsValidMatch(string value, string input) + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(value); + + // Act + var match = matcher.IsMatch(input).Score; + + // Assert + match.Should().Be(1.0); + } + + [Theory] + [InlineData("\"test\"", null)] + [InlineData("\"test1\"", "\"test2\"")] + [InlineData("123", "1234")] + [InlineData("[\"test\"]", "[\"test1\"]")] + [InlineData("[\"test\"]", "[\"test1\", \"test2\"]")] + [InlineData("[123]", "[1234]")] + [InlineData("{}", "\"test\"")] + [InlineData("{ \"test\":\"value\" }", "{\"test\":\"value2\"}")] + [InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value1\"}}")] + [InlineData("{\"test\":{\"test1\":\"value\"}}", "{\"test\":{\"test1\":\"value1\"}}")] + [InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value1\"}}]")] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_StringInputWithInvalidMatch(string value, string? input) + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(value); + + // Act + var match = matcher.IsMatch(input).Score; + + // Assert + Assert.Equal(0.0, match); + } + + [Theory] + [InlineData("{ \"test.nested\":123 }", "{\"test\":{\"nested\":123}}")] + [InlineData("{ \"test.nested\":[123, 456] }", "{\"test\":{\"nested\":[123, 456]}}")] + [InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value\"}}")] + [InlineData("{ \"['name.with.dot']\":\"value\" }", "{\"name.with.dot\":\"value\"}")] + [InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value\"}}]")] + [InlineData("[{ \"['name.with.dot']\":\"value\" }]", "[{\"name.with.dot\":\"value\"}]")] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_ValueAsPathValidMatch(string value, string input) + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(value); + + // Act + var match = matcher.IsMatch(input).Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Theory] + [InlineData("{ \"test.nested\":123 }", "{\"test\":{\"nested\":456}}")] + [InlineData("{ \"test.nested\":[123, 456] }", "{\"test\":{\"nested\":[1, 2]}}")] + [InlineData("{ \"test.nested\":\"value\" }", "{\"test\":{\"nested\":\"value1\"}}")] + [InlineData("{ \"['name.with.dot']\":\"value\" }", "{\"name.with.dot\":\"value1\"}")] + [InlineData("[{ \"test.nested\":\"value\" }]", "[{\"test\":{\"nested\":\"value1\"}}]")] + [InlineData("[{ \"['name.with.dot']\":\"value\" }]", "[{\"name.with.dot\":\"value1\"}]")] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_ValueAsPathInvalidMatch(string value, string input) + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(value); + + // Act + var match = matcher.IsMatch(input).Score; + + // Assert + Assert.Equal(0.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithIgnoreCaseTrueAndRegexTrue_JsonObject1() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(new { id = 1, Number = "^\\d+$" }, ignoreCase: true, regex: true); + + // Act + var match = matcher.IsMatch("{ \"Id\" : 1, \"Number\" : \"42\" }").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_WithIgnoreCaseTrueAndRegexTrue_JsonObject2() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(new { method = "initialize", id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true); + + // Act + var match = matcher.IsMatch("{\"jsonrpc\":\"2.0\",\"id\":\"ec475f56d4694b48bc737500ba575b35-1\",\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2024-11-05\",\"capabilities\":{},\"clientInfo\":{\"name\":\"GitHub Test\",\"version\":\"1.0.0\"}}}").Score; + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void SystemTextJsonPartialWildcardMatcher_IsMatch_JsonElement_ShouldMatch() + { + // Assign + var matcher = new SystemTextJsonPartialWildcardMatcher(new { Id = 1, Name = "Test" }); + + // Act + var jsonElement = JsonDocument.Parse("{ \"Id\" : 1, \"Name\" : \"Test\", \"Extra\" : \"value\" }").RootElement; + var match = matcher.IsMatch(jsonElement).Score; + + // Assert + Assert.Equal(1.0, match); + } +} diff --git a/test/WireMock.Net.Tests/Matchers/SystemTextJsonPathMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/SystemTextJsonPathMatcherTests.cs new file mode 100644 index 000000000..e4849a960 --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/SystemTextJsonPathMatcherTests.cs @@ -0,0 +1,400 @@ +// Copyright © WireMock.Net + +using System.Text.Json.Nodes; +using WireMock.Matchers; + +namespace WireMock.Net.Tests.Matchers; + +public class SystemTextJsonPathMatcherTests +{ + [Fact] + public void SystemTextJsonPathMatcher_GetName() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("X"); + + // Act + string name = matcher.Name; + + // Assert + name.Should().Be("SystemTextJsonPathMatcher"); + } + + [Fact] + public void SystemTextJsonPathMatcher_GetPatterns() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("X"); + + // Act + var patterns = matcher.GetPatterns(); + + // Assert + patterns.Should().ContainSingle("X"); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_ByteArray() + { + // Arrange + var bytes = new byte[0]; + var matcher = new SystemTextJsonPathMatcher("$.Id"); + + // Act + double match = matcher.IsMatch(bytes).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_NullString() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$.Id"); + + // Act + double match = matcher.IsMatch(null).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_EmptyString() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$.Id"); + + // Act + double match = matcher.IsMatch(string.Empty).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_NullObject() + { + // Arrange + object? o = null; + var matcher = new SystemTextJsonPathMatcher("$.Id"); + + // Act + double match = matcher.IsMatch(o).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_String_Exception_Mismatch() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$.Id"); + + // Act + double match = matcher.IsMatch("not-json").Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_AnonymousObject() + { + // Arrange - RFC 9535: filter expression requires an array context + var matcher = new SystemTextJsonPathMatcher("$[?(@.Id == 1)]"); + + // Act + double match = matcher.IsMatch(new[] { new { Id = 1, Name = "Test" } }).Score; + + // Assert + match.Should().Be(1); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_AnonymousObject_WithNestedObject() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$.things[?(@.name == 'x')]"); + + // Act + double match = matcher.IsMatch(new { things = new { name = "x" } }).Score; + + // Assert + match.Should().Be(1); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_String_WithNestedObject() + { + // Arrange + var json = "{ \"things\": { \"name\": \"x\" } }"; + var matcher = new SystemTextJsonPathMatcher("$.things[?(@.name == 'x')]"); + + // Act + double match = matcher.IsMatch(json).Score; + + // Assert + match.Should().Be(1); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsNoMatch_String_WithNestedObject() + { + // Arrange + var json = "{ \"things\": { \"name\": \"y\" } }"; + var matcher = new SystemTextJsonPathMatcher("$.things[?(@.name == 'x')]"); + + // Act + double match = matcher.IsMatch(json).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_JsonNode() + { + // Arrange - RFC 9535: filter expression requires an array context + string[] patterns = { "$[?(@.Id == 1)]" }; + var matcher = new SystemTextJsonPathMatcher(patterns); + + // Act + var node = JsonNode.Parse("[{\"Id\":1,\"Name\":\"Test\"}]"); + double match = matcher.IsMatch(node).Score; + + // Assert + match.Should().Be(1); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_JsonNode_Parsed() + { + // Arrange - RFC 9535: filter expression requires an array context + var matcher = new SystemTextJsonPathMatcher("$[?(@.Id == 1)]"); + + // Act + double match = matcher.IsMatch(JsonNode.Parse("[{\"Id\":1,\"Name\":\"Test\"}]")).Score; + + // Assert + match.Should().Be(1); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_RejectOnMatch() + { + // Arrange - RFC 9535: filter expression requires an array context + var matcher = new SystemTextJsonPathMatcher(MatchBehaviour.RejectOnMatch, MatchOperator.Or, "$[?(@.Id == 1)]"); + + // Act + double match = matcher.IsMatch(JsonNode.Parse("[{\"Id\":1,\"Name\":\"Test\"}]")).Score; + + // Assert + match.Should().Be(0.0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_ArrayOneLevel() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$.arr[0].line1"); + + // Act + double match = matcher.IsMatch(JsonNode.Parse(@"{ + ""name"": ""PathSelectorTest"", + ""test"": ""test"", + ""test2"": ""test2"", + ""arr"": [{ + ""line1"": ""line1"" + }] + }")).Score; + + // Assert + match.Should().Be(1.0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_ObjectMatch() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$.test"); + + // Act + double match = matcher.IsMatch(JsonNode.Parse(@"{ + ""name"": ""PathSelectorTest"", + ""test"": ""test"", + ""test2"": ""test2"", + ""arr"": [ + { + ""line1"": ""line1"" + } + ] + }")).Score; + + // Assert + match.Should().Be(1.0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_DoesntMatch() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$.test3"); + + // Act + double match = matcher.IsMatch(JsonNode.Parse(@"{ + ""name"": ""PathSelectorTest"", + ""test"": ""test"", + ""test2"": ""test2"", + ""arr"": [ + { + ""line1"": ""line1"" + } + ] + }")).Score; + + // Assert + match.Should().Be(0.0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_DoesntMatchInArray() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$arr[0].line1"); + + // Act + double match = matcher.IsMatch(JsonNode.Parse(@"{ + ""name"": ""PathSelectorTest"", + ""test"": ""test"", + ""test2"": ""test2"", + ""arr"": [] + }")).Score; + + // Assert + match.Should().Be(0.0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_DoesntMatchNoObjectsInArray() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$arr[2].line1"); + + // Act + double match = matcher.IsMatch(JsonNode.Parse(@"{ + ""name"": ""PathSelectorTest"", + ""test"": ""test"", + ""test2"": ""test2"", + ""arr"": [] + }")).Score; + + // Assert + match.Should().Be(0.0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_NestedArrays() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$.arr[0].sub[0].subline1"); + + // Act + double match = matcher.IsMatch(JsonNode.Parse(@"{ + ""name"": ""PathSelectorTest"", + ""test"": ""test"", + ""test2"": ""test2"", + ""arr"": [{ + ""line1"": ""line1"", + ""sub"":[ + { + ""subline1"":""subline1"" + }] + }] + }")).Score; + + // Assert + match.Should().Be(1.0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_MultiplePatternsUsingMatchOperatorAnd() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher(MatchBehaviour.AcceptOnMatch, MatchOperator.And, "$.arr[0].sub[0].subline1", "$.arr[0].line2"); + + // Act + double match = matcher.IsMatch(JsonNode.Parse(@"{ + ""name"": ""PathSelectorTest"", + ""test"": ""test"", + ""test2"": ""test2"", + ""arr"": [{ + ""line1"": ""line1"", + ""sub"":[ + { + ""subline1"":""subline1"" + }] + }] + }")).Score; + + // Assert + match.Should().Be(0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_MultiplePatternsUsingMatchOperatorOr() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher(MatchBehaviour.AcceptOnMatch, MatchOperator.Or, "$.arr[0].sub[0].subline2", "$.arr[0].line1"); + + // Act + double match = matcher.IsMatch(JsonNode.Parse(@"{ + ""name"": ""PathSelectorTest"", + ""test"": ""test"", + ""test2"": ""test2"", + ""arr"": [{ + ""line1"": ""line1"", + ""sub"":[ + { + ""subline1"":""subline1"" + }] + }] + }")).Score; + + // Assert + match.Should().Be(1); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_String_ArrayOneLevel() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$.arr[0].line1"); + + // Act + double match = matcher.IsMatch(@"{ + ""name"": ""PathSelectorTest"", + ""arr"": [{ + ""line1"": ""line1"" + }] + }").Score; + + // Assert + match.Should().Be(1.0); + } + + [Fact] + public void SystemTextJsonPathMatcher_IsMatch_String_DoesntMatch() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher("$.test3"); + + // Act + double match = matcher.IsMatch(@"{ ""test"": ""test"" }").Score; + + // Assert + match.Should().Be(0.0); + } +} diff --git a/test/WireMock.Net.Tests/Pact/PactTests.cs b/test/WireMock.Net.Tests/Pact/PactTests.cs index e986cb87d..2b2cb0537 100644 --- a/test/WireMock.Net.Tests/Pact/PactTests.cs +++ b/test/WireMock.Net.Tests/Pact/PactTests.cs @@ -11,6 +11,7 @@ namespace WireMock.Net.Tests.Pact; +[Collection(nameof(PactTests))] public class PactTests { [Fact] diff --git a/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs index 65348ebad..317671525 100644 --- a/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs +++ b/test/WireMock.Net.Tests/RequestBuilders/RequestBuilderWithBodyTests.cs @@ -3,7 +3,6 @@ using System.Text; using Newtonsoft.Json; using Newtonsoft.Json.Linq; - using WireMock.Matchers; using WireMock.Matchers.Request; using WireMock.Models; @@ -323,7 +322,7 @@ public void Request_WithBody_JsonPathMatcher_true() } [Fact] - public void Request_WithBodyJson_PathMatcher_false() + public void Request_WithBodyJson_JsonPathMatcher_false() { // Arrange var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$.things[?(@.name == 'RequiredThing')]")); @@ -368,10 +367,10 @@ public void Request_WithBody_Object_JsonPathMatcher_true() public void Request_WithBody_Array_JsonPathMatcher_1() { // Arrange - var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$..books[?(@.price < 10)]")); + var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$[?(@.Id == 1)]")); // Act - string jsonString = "{ \"books\": [ { \"category\": \"test1\", \"price\": 8.95 }, { \"category\": \"test2\", \"price\": 20 } ] }"; + string jsonString = "[{\"Id\": 1, \"Name\": \"Test\"}, {\"Id\": 2, \"Name\": \"Test2\"}]"; var bodyData = new BodyData { BodyAsJson = JsonConvert.DeserializeObject(jsonString), @@ -391,10 +390,10 @@ public void Request_WithBody_Array_JsonPathMatcher_1() public void Request_WithBody_Array_JsonPathMatcher_2() { // Arrange - var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$..[?(@.Id == 1)]")); + var spec = Request.Create().UsingAnyMethod().WithBody(new JsonPathMatcher("$.test")); // Act - string jsonString = "{ \"Id\": 1, \"Name\": \"Test\" }"; + string jsonString = "{\"name\": \"PathSelectorTest\", \"test\": \"test\", \"test2\": \"test2\", \"arr\": [{\"line1\": \"line1\"}]}"; var bodyData = new BodyData { BodyAsJson = JsonConvert.DeserializeObject(jsonString), @@ -479,5 +478,132 @@ public void Request_WithBodyAsBytes_ExactObjectMatcher_true(byte[] bytes, BodyTy var requestMatchResult = new RequestMatchResult(); requestBuilder.GetMatchingScore(request, requestMatchResult).Should().Be(1.0); } + + [Fact] + public void Request_WithBody_SystemTextJsonPathMatcher_true() + { + // Arrange + var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$..things[?(@.name == 'RequiredThing')]")); + + // Act + var body = new BodyData + { + BodyAsString = "{ \"things\": [ { \"name\": \"RequiredThing\" }, { \"name\": \"Wiremock\" } ] }", + DetectedBodyType = BodyType.String + }; + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, body); + + // Assert + var requestMatchResult = new RequestMatchResult(); + spec.GetMatchingScore(request, requestMatchResult).Should().Be(1.0); + } + + [Fact] + public void Request_WithBodyJson_SystemTextJsonPathMatcher_false() + { + // Arrange + var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$.things[?(@.name == 'RequiredThing')]")); + + // Act + var body = new BodyData + { + BodyAsString = "{ \"things\": { \"name\": \"Wiremock\" } }", + DetectedBodyType = BodyType.String + }; + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, body); + + // Assert + var requestMatchResult = new RequestMatchResult(); + spec.GetMatchingScore(request, requestMatchResult).Should().NotBe(1.0); + } + + [Fact] + public void Request_WithBody_Object_SystemTextJsonPathMatcher_true() + { + // Arrange + var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$.arr[0].line1")); + + // Act + string jsonString = "{\"name\": \"PathSelectorTest\", \"test\": \"test\", \"test2\": \"test2\", \"arr\": [{\"line1\": \"line1\"}]}"; + var bodyData = new BodyData + { + BodyAsString = jsonString, + Encoding = Encoding.UTF8, + DetectedBodyType = BodyType.String + }; + + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData); + + // Assert + var requestMatchResult = new RequestMatchResult(); + spec.GetMatchingScore(request, requestMatchResult).Should().Be(1.0); + } + + [Fact] + public void Request_WithBody_Array_SystemTextJsonPathMatcher_1() + { + // Arrange - RFC 9535: filter expression requires an array context + var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$[?(@.Id == 1)]")); + + // Act + string jsonString = "[{\"Id\": 1, \"Name\": \"Test\"}, {\"Id\": 2, \"Name\": \"Test2\"}]"; + var bodyData = new BodyData + { + BodyAsString = jsonString, + Encoding = Encoding.UTF8, + DetectedBodyType = BodyType.String + }; + + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData); + + // Assert + var requestMatchResult = new RequestMatchResult(); + spec.GetMatchingScore(request, requestMatchResult).Should().Be(1.0); + } + + [Fact] + public void Request_WithBody_Array_SystemTextJsonPathMatcher_2() + { + // Arrange + var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$.test")); + + // Act + string jsonString = "{\"name\": \"PathSelectorTest\", \"test\": \"test\", \"test2\": \"test2\", \"arr\": [{\"line1\": \"line1\"}]}"; + var bodyData = new BodyData + { + BodyAsString = jsonString, + Encoding = Encoding.UTF8, + DetectedBodyType = BodyType.String + }; + + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData); + + // Assert + var requestMatchResult = new RequestMatchResult(); + double result = spec.GetMatchingScore(request, requestMatchResult); + result.Should().Be(1.0); + } + + [Fact] + public void Request_WithBody_SystemTextJsonPathMatcher_false() + { + // Arrange + var spec = Request.Create().UsingAnyMethod().WithBody(new SystemTextJsonPathMatcher("$.nonexistent")); + + // Act + string jsonString = "{\"name\": \"Test\", \"arr\": [{\"line1\": \"line1\"}]}"; + var bodyData = new BodyData + { + BodyAsString = jsonString, + Encoding = Encoding.UTF8, + DetectedBodyType = BodyType.String + }; + + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "PUT", ClientIp, bodyData); + + // Assert + var requestMatchResult = new RequestMatchResult(); + spec.GetMatchingScore(request, requestMatchResult).Should().NotBe(1.0); + } } diff --git a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs index d32e85170..46e7b9166 100644 --- a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs @@ -510,12 +510,11 @@ public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Pattern_As_Object( } [Fact] - public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Patterns_As_Object() + public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Patterns_1_Value_As_Object() { // Assign - object pattern1 = new { AccountIds = new[] { 1, 2, 3 } }; - object pattern2 = new { X = "x" }; - var patterns = new[] { pattern1, pattern2 }; + object pattern = new { post1 = "value1", post2 = "value2" }; + var patterns = new[] { pattern }; var model = new MatcherModel { Name = "JsonPartialMatcher", @@ -523,7 +522,7 @@ public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Patterns_As_Object }; // Act - var matcher = (JsonMatcher)_sut.Map(model)!; + var matcher = (JsonPartialMatcher)_sut.Map(model)!; // Assert matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); @@ -531,15 +530,16 @@ public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Patterns_As_Object } [Fact] - public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_StringPattern_With_PatternAsFile() + public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Patterns_2_Values_As_Object() { // Assign - var pattern = new StringPattern { Pattern = "{ \"AccountIds\": [ 1, 2, 3 ] }", PatternAsFile = "pf" }; + object pattern1 = new { AccountIds = new[] { 1, 2, 3 } }; + object pattern2 = new { post1 = "value1", post2 = "value2" }; + var patterns = new[] { pattern1, pattern2 }; var model = new MatcherModel { Name = "JsonPartialMatcher", - Pattern = pattern, - Regex = true + Patterns = patterns }; // Act @@ -547,20 +547,37 @@ public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_StringPattern_With // Assert matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); - matcher.Value.Should().BeEquivalentTo(pattern); - matcher.Regex.Should().BeTrue(); + matcher.Value.Should().BeEquivalentTo(patterns); } [Fact] - public void MatcherMapper_Map_MatcherModel_JsonPartialWildcardMatcher_Patterns_As_Object() + public void MatcherMapper_Map_MatcherModel_JsonPartialWildcardMatcher_Pattern_As_String() + { + // Assign + var pattern = "{ \"Name\": \"T*\" }"; + var model = new MatcherModel + { + Name = "JsonPartialWildcardMatcher", + Pattern = pattern + }; + + // Act + var matcher = (JsonPartialWildcardMatcher)_sut.Map(model)!; + + // Assert + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Value.Should().Be(pattern); + } + + [Fact] + public void MatcherMapper_Map_MatcherModel_JsonPartialWildcardMatcher_Pattern_As_Object() { // Assign object pattern = new { X = "*" }; var model = new MatcherModel { Name = "JsonPartialWildcardMatcher", - Pattern = pattern, - Regex = false + Pattern = pattern }; // Act @@ -569,563 +586,654 @@ public void MatcherMapper_Map_MatcherModel_JsonPartialWildcardMatcher_Patterns_A // Assert matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); matcher.Value.Should().BeEquivalentTo(pattern); - matcher.Regex.Should().BeFalse(); } [Fact] - public void MatcherMapper_Map_MatcherModel_NotNullOrEmptyMatcher() + public void MatcherMapper_Map_MatcherModel_JsonPartialWildcardMatcher_RegexTrue() { // Assign + var pattern = "{ \"x\": \"^\\\\d+$\" }"; var model = new MatcherModel { - Name = "NotNullOrEmptyMatcher", - RejectOnMatch = true + Name = "JsonPartialWildcardMatcher", + Regex = true, + Pattern = pattern }; // Act - var matcher = _sut.Map(model)!; + var matcher = (JsonPartialWildcardMatcher)_sut.Map(model)!; // Assert - matcher.Should().BeAssignableTo(); - matcher.MatchBehaviour.Should().Be(MatchBehaviour.RejectOnMatch); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Regex.Should().BeTrue(); + matcher.Value.Should().Be(pattern); } [Fact] - public void MatcherMapper_Map_MatcherModel_MimePartMatcher() + public void MatcherMapper_Map_MatcherModel_JsonPartialWildcardMatcher_RejectOnMatch() { // Assign + var pattern = "{ \"x\": \"*\" }"; var model = new MatcherModel { - Name = "MimePartMatcher", - ContentMatcher = new MatcherModel - { - Name = "ExactMatcher", - Pattern = "x" - }, - ContentDispositionMatcher = new MatcherModel - { - Name = "WildcardMatcher", - Pattern = "y" - }, - ContentTransferEncodingMatcher = new MatcherModel - { - Name = "RegexMatcher", - Pattern = "z" - }, - ContentTypeMatcher = new MatcherModel - { - Name = "ContentTypeMatcher", - Pattern = "text/json" - } + Name = "JsonPartialWildcardMatcher", + Pattern = pattern, + RejectOnMatch = true }; // Act - var matcher = (MimePartMatcher)_sut.Map(model)!; + var matcher = (JsonPartialWildcardMatcher)_sut.Map(model)!; // Assert - matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); - matcher.ContentMatcher.Should().BeAssignableTo().Which.GetPatterns().Should().ContainSingle("x"); - matcher.ContentDispositionMatcher.Should().BeAssignableTo().Which.GetPatterns().Should().ContainSingle("y"); - matcher.ContentTransferEncodingMatcher.Should().BeAssignableTo().Which.GetPatterns().Should().ContainSingle("z"); - matcher.ContentTypeMatcher.Should().BeAssignableTo().Which.GetPatterns().Should().ContainSingle("text/json"); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.RejectOnMatch); + matcher.Value.Should().Be(pattern); } [Fact] - public void MatcherMapper_Map_MatcherModel_XPathMatcher_WithXmlNamespaces_As_String() + public void MatcherMapper_Map_MatcherModel_JsonPartialWildcardMatcher_IgnoreCaseTrue() { // Assign - var pattern = "/s:Envelope/s:Body/*[local-name()='QueryRequest']"; + var pattern = "{ \"name\": \"t*\" }"; var model = new MatcherModel { - Name = "XPathMatcher", + Name = "JsonPartialWildcardMatcher", Pattern = pattern, - XmlNamespaceMap = - [ - new XmlNamespace { Prefix = "s", Uri = "http://schemas.xmlsoap.org/soap/envelope/" } - ] + IgnoreCase = true }; // Act - var matcher = (XPathMatcher)_sut.Map(model)!; + var matcher = (JsonPartialWildcardMatcher)_sut.Map(model)!; // Assert - matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); - matcher.XmlNamespaceMap.Should().NotBeNull(); - matcher.XmlNamespaceMap.Should().HaveCount(1); + matcher.IgnoreCase.Should().BeTrue(); + matcher.Value.Should().Be(pattern); } + #region SystemTextJsonMatcher + [Fact] - public void MatcherMapper_Map_MatcherModel_XPathMatcher_WithoutXmlNamespaces_As_String() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonMatcher_Pattern_As_String() { // Assign - var pattern = "/s:Envelope/s:Body/*[local-name()='QueryRequest']"; + var pattern = "{ \"AccountIds\": [ 1, 2, 3 ] }"; var model = new MatcherModel { - Name = "XPathMatcher", + Name = "SystemTextJsonMatcher", Pattern = pattern }; // Act - var matcher = (XPathMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonMatcher)_sut.Map(model)!; // Assert matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); - matcher.XmlNamespaceMap.Should().BeNull(); + matcher.Value.Should().BeEquivalentTo(pattern); + matcher.Regex.Should().BeFalse(); } [Fact] - public void MatcherMapper_Map_MatcherModel_CSharpCodeMatcher() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonMatcher_Pattern_As_Object() { // Assign + var pattern = new { AccountIds = new[] { 1, 2, 3 } }; var model = new MatcherModel { - Name = "CSharpCodeMatcher", - Patterns = ["return it == \"x\";"] + Name = "SystemTextJsonMatcher", + Pattern = pattern }; - var sut = new MatcherMapper(new WireMockServerSettings { AllowCSharpCodeMatcher = true }); - // Act 1 - var matcher1 = (ICSharpCodeMatcher)sut.Map(model)!; + // Act + var matcher = (SystemTextJsonMatcher)_sut.Map(model)!; - // Assert 1 - matcher1.Should().NotBeNull(); - matcher1.IsMatch("x").Score.Should().Be(1.0d); + // Assert + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Value.Should().BeEquivalentTo(pattern); + } - // Act 2 - var matcher2 = (ICSharpCodeMatcher)sut.Map(model)!; + [Fact] + public void MatcherMapper_Map_MatcherModel_SystemTextJsonMatcher_Patterns_As_String() + { + // Assign + var pattern1 = "{ \"AccountIds\": [ 1, 2, 3 ] }"; + var pattern2 = "{ \"post1\": \"value1\" }"; + object[] patterns = [pattern1, pattern2]; + var model = new MatcherModel + { + Name = "SystemTextJsonMatcher", + Patterns = patterns + }; - // Assert 2 - matcher2.Should().NotBeNull(); - matcher2.IsMatch("x").Score.Should().Be(1.0d); + // Act + var matcher = (SystemTextJsonMatcher)_sut.Map(model)!; + + // Assert + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Value.Should().BeEquivalentTo(patterns); } [Fact] - public void MatcherMapper_Map_MatcherModel_CSharpCodeMatcher_NotAllowed_ThrowsException() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonMatcher_RejectOnMatch() { // Assign + var pattern = "{ \"x\": 1 }"; var model = new MatcherModel { - Name = "CSharpCodeMatcher", - Patterns = ["x"] + Name = "SystemTextJsonMatcher", + Pattern = pattern, + RejectOnMatch = true }; - var sut = new MatcherMapper(new WireMockServerSettings { AllowCSharpCodeMatcher = false }); // Act - Action action = () => sut.Map(model); + var matcher = (SystemTextJsonMatcher)_sut.Map(model)!; // Assert - action.Should().Throw(); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.RejectOnMatch); + matcher.Value.Should().Be(pattern); } [Fact] - public void MatcherMapper_Map_MatcherModel_ExactMatcher_Pattern() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonMatcher_IgnoreCaseTrue() { // Assign + var pattern = "{ \"x\": 1 }"; var model = new MatcherModel { - Name = "ExactMatcher", - Patterns = ["x"] + Name = "SystemTextJsonMatcher", + Pattern = pattern, + IgnoreCase = true }; // Act - var matcher = (ExactMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonMatcher)_sut.Map(model)!; // Assert - matcher.GetPatterns().Should().ContainSingle("x"); + matcher.IgnoreCase.Should().BeTrue(); + matcher.Value.Should().Be(pattern); } [Fact] - public void MatcherMapper_Map_MatcherModel_ExactMatcher_Patterns() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonMatcher_RegexTrue() { // Assign + var pattern = "{ \"x\": \"^\\\\d+$\" }"; var model = new MatcherModel { - Name = "ExactMatcher", - Patterns = ["x", "y"] + Name = "SystemTextJsonMatcher", + Pattern = pattern, + Regex = true }; // Act - var matcher = (ExactMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonMatcher)_sut.Map(model)!; // Assert - matcher.GetPatterns().Should().ContainInOrder("x", "y"); + matcher.Regex.Should().BeTrue(); + matcher.Value.Should().Be(pattern); } [Fact] - public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_RegexFalse() + public void MatcherMapper_Map_Matcher_SystemTextJsonMatcher_To_MatcherModel() { // Assign - var pattern = "{ \"x\": 1 }"; + var pattern = new { Id = 1, Name = "Test" }; + var matcher = new SystemTextJsonMatcher(MatchBehaviour.AcceptOnMatch, pattern, ignoreCase: true, regex: true); + + // Act + var model = _sut.Map(matcher)!; + + // Assert + model.Name.Should().Be(nameof(SystemTextJsonMatcher)); + model.Pattern.Should().BeEquivalentTo(pattern); + model.IgnoreCase.Should().BeTrue(); + model.Regex.Should().BeTrue(); + model.RejectOnMatch.Should().BeNull(); + } + + [Fact] + public void MatcherMapper_Map_Matcher_SystemTextJsonMatcher_RejectOnMatch_To_MatcherModel() + { + // Assign + var pattern = "{ \"Id\": 1 }"; + var matcher = new SystemTextJsonMatcher(MatchBehaviour.RejectOnMatch, pattern); + + // Act + var model = _sut.Map(matcher)!; + + // Assert + model.Name.Should().Be(nameof(SystemTextJsonMatcher)); + model.Pattern.Should().Be(pattern); + model.RejectOnMatch.Should().BeTrue(); + model.Regex.Should().BeFalse(); + } + + #endregion + + #region SystemTextJsonPartialMatcher + + [Fact] + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialMatcher_Pattern_As_String() + { + // Assign + var pattern = "{ \"AccountIds\": [ 1, 2, 3 ] }"; var model = new MatcherModel { - Name = "JsonPartialMatcher", - Regex = false, + Name = "SystemTextJsonPartialMatcher", Pattern = pattern }; // Act - var matcher = (JsonPartialMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonPartialMatcher)_sut.Map(model)!; // Assert matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); - matcher.IgnoreCase.Should().BeFalse(); - matcher.Value.Should().Be(pattern); + matcher.Value.Should().BeEquivalentTo(pattern); matcher.Regex.Should().BeFalse(); } [Fact] - public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_RegexTrue() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialMatcher_Pattern_As_Object() { // Assign - var pattern = "{ \"x\": 1 }"; + var pattern = new { AccountIds = new[] { 1, 2, 3 } }; var model = new MatcherModel { - Name = "JsonPartialMatcher", - Regex = true, + Name = "SystemTextJsonPartialMatcher", Pattern = pattern }; // Act - var matcher = (JsonPartialMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonPartialMatcher)_sut.Map(model)!; // Assert matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); - matcher.IgnoreCase.Should().BeFalse(); - matcher.Value.Should().Be(pattern); - matcher.Regex.Should().BeTrue(); + matcher.Value.Should().BeEquivalentTo(pattern); } [Fact] - public void MatcherMapper_Map_MatcherModel_ExactObjectMatcher_ValidBase64StringPattern() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialMatcher_Patterns_As_String() { // Assign + var pattern1 = "{ \"AccountIds\": [ 1, 2, 3 ] }"; + var pattern2 = "{ \"X\": \"x\" }"; + object[] patterns = [pattern1, pattern2]; var model = new MatcherModel { - Name = "ExactObjectMatcher", - Patterns = ["c3RlZg=="] + Name = "SystemTextJsonPartialMatcher", + Patterns = patterns }; // Act - var matcher = (ExactObjectMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonPartialMatcher)_sut.Map(model)!; // Assert - ((byte[])matcher.Value).Should().BeEquivalentTo(new byte[] { 115, 116, 101, 102 }); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Value.Should().BeEquivalentTo(patterns); } [Fact] - public void MatcherMapper_Map_MatcherModel_ExactObjectMatcher_InvalidBase64StringPattern() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialMatcher_RegexFalse() { // Assign + var pattern = "{ \"x\": 1 }"; var model = new MatcherModel { - Name = "ExactObjectMatcher", - Patterns = ["_"] + Name = "SystemTextJsonPartialMatcher", + Regex = false, + Pattern = pattern }; // Act - Action act = () => _sut.Map(model); + var matcher = (SystemTextJsonPartialMatcher)_sut.Map(model)!; // Assert - act.Should().Throw(); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.IgnoreCase.Should().BeFalse(); + matcher.Value.Should().Be(pattern); + matcher.Regex.Should().BeFalse(); } - [Theory] - [InlineData(MatchOperator.Or, 1.0d)] - [InlineData(MatchOperator.And, 0.0d)] - [InlineData(MatchOperator.Average, 0.5d)] - public void MatcherMapper_Map_MatcherModel_RegexMatcher(MatchOperator matchOperator, double expected) + [Fact] + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialMatcher_RegexTrue() { // Assign + var pattern = "{ \"x\": 1 }"; var model = new MatcherModel { - Name = "RegexMatcher", - Patterns = ["x", "y"], - IgnoreCase = true, - MatchOperator = matchOperator.ToString() + Name = "SystemTextJsonPartialMatcher", + Regex = true, + Pattern = pattern }; // Act - var matcher = (RegexMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonPartialMatcher)_sut.Map(model)!; // Assert - matcher.GetPatterns().Should().ContainInOrder("x", "y"); - - var result = matcher.IsMatch("X"); - result.Score.Should().Be(expected); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.IgnoreCase.Should().BeFalse(); + matcher.Value.Should().Be(pattern); + matcher.Regex.Should().BeTrue(); } - [Theory] - [InlineData(MatchOperator.Or, 1.0d)] - [InlineData(MatchOperator.And, 0.0d)] - [InlineData(MatchOperator.Average, 0.5d)] - public void MatcherMapper_Map_MatcherModel_WildcardMatcher_IgnoreCase(MatchOperator matchOperator, double expected) + [Fact] + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialMatcher_RejectOnMatch() { // Assign + var pattern = "{ \"x\": 1 }"; var model = new MatcherModel { - Name = "WildcardMatcher", - Patterns = ["x", "y"], - IgnoreCase = true, - MatchOperator = matchOperator.ToString() + Name = "SystemTextJsonPartialMatcher", + Pattern = pattern, + RejectOnMatch = true }; // Act - var matcher = (WildcardMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonPartialMatcher)_sut.Map(model)!; // Assert - matcher.GetPatterns().Should().ContainInOrder("x", "y"); - - var result = matcher.IsMatch("X"); - result.Score.Should().Be(expected); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.RejectOnMatch); + matcher.Value.Should().Be(pattern); } [Fact] - public void MatcherMapper_Map_MatcherModel_WildcardMatcher_With_PatternAsFile() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialMatcher_IgnoreCaseTrue() { - // Arrange - var file = "c:\\test.txt"; - var fileContent = "c"; - var stringPattern = new StringPattern - { - Pattern = fileContent, - PatternAsFile = file - }; - var fileSystemHandleMock = new Mock(); - fileSystemHandleMock.Setup(f => f.ReadFileAsString(file)).Returns(fileContent); - + // Assign + var pattern = "{ \"x\": 1 }"; var model = new MatcherModel { - Name = "WildcardMatcher", - PatternAsFile = file + Name = "SystemTextJsonPartialMatcher", + Pattern = pattern, + IgnoreCase = true }; - var settings = new WireMockServerSettings - { - FileSystemHandler = fileSystemHandleMock.Object - }; - var sut = new MatcherMapper(settings); + // Act + var matcher = (SystemTextJsonPartialMatcher)_sut.Map(model)!; + + // Assert + matcher.IgnoreCase.Should().BeTrue(); + matcher.Value.Should().Be(pattern); + } + + [Fact] + public void MatcherMapper_Map_Matcher_SystemTextJsonPartialMatcher_To_MatcherModel() + { + // Assign + var pattern = new { Id = 1, Name = "Test" }; + var matcher = new SystemTextJsonPartialMatcher(MatchBehaviour.AcceptOnMatch, pattern, ignoreCase: true, regex: true); // Act - var matcher = (WildcardMatcher)sut.Map(model)!; + var model = _sut.Map(matcher)!; // Assert - matcher.GetPatterns().Should().HaveCount(1).And.Contain(new AnyOf(stringPattern)); + model.Name.Should().Be(nameof(SystemTextJsonPartialMatcher)); + model.Pattern.Should().BeEquivalentTo(pattern); + model.IgnoreCase.Should().BeTrue(); + model.Regex.Should().BeTrue(); + model.RejectOnMatch.Should().BeNull(); + } - var result = matcher.IsMatch("c"); - result.Score.Should().Be(MatchScores.Perfect); + [Fact] + public void MatcherMapper_Map_Matcher_SystemTextJsonPartialMatcher_RejectOnMatch_To_MatcherModel() + { + // Assign + var pattern = "{ \"Id\": 1 }"; + var matcher = new SystemTextJsonPartialMatcher(MatchBehaviour.RejectOnMatch, pattern); + + // Act + var model = _sut.Map(matcher)!; + + // Assert + model.Name.Should().Be(nameof(SystemTextJsonPartialMatcher)); + model.Pattern.Should().Be(pattern); + model.RejectOnMatch.Should().BeTrue(); + model.Regex.Should().BeFalse(); } + #endregion + + #region SystemTextJsonPartialWildcardMatcher + [Fact] - public void MatcherMapper_Map_MatcherModel_SimMetricsMatcher() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialWildcardMatcher_Pattern_As_Object() { // Assign + object pattern = new { X = "*" }; var model = new MatcherModel { - Name = "SimMetricsMatcher", - Pattern = "x" + Name = "SystemTextJsonPartialWildcardMatcher", + Pattern = pattern, + Regex = false }; // Act - var matcher = (SimMetricsMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonPartialWildcardMatcher)_sut.Map(model)!; // Assert - matcher.GetPatterns().Should().ContainSingle("x"); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Value.Should().BeEquivalentTo(pattern); + matcher.Regex.Should().BeFalse(); } [Fact] - public void MatcherMapper_Map_MatcherModel_SimMetricsMatcher_BlockDistance() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialWildcardMatcher_Pattern_As_String() { // Assign + var pattern = "{ \"Name\": \"T*\" }"; var model = new MatcherModel { - Name = "SimMetricsMatcher.BlockDistance", - Pattern = "x" + Name = "SystemTextJsonPartialWildcardMatcher", + Pattern = pattern }; // Act - var matcher = (SimMetricsMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonPartialWildcardMatcher)_sut.Map(model)!; // Assert - matcher.GetPatterns().Should().ContainSingle("x"); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Value.Should().Be(pattern); } [Fact] - public void MatcherMapper_Map_MatcherModel_SimMetricsMatcher_Throws1() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialWildcardMatcher_RegexTrue() { // Assign + var pattern = "{ \"x\": \"^\\\\d+$\" }"; var model = new MatcherModel { - Name = "error", - Pattern = "x" + Name = "SystemTextJsonPartialWildcardMatcher", + Regex = true, + Pattern = pattern }; // Act - Action act = () => _sut.Map(model); + var matcher = (SystemTextJsonPartialWildcardMatcher)_sut.Map(model)!; // Assert - act.Should().Throw(); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Regex.Should().BeTrue(); + matcher.Value.Should().Be(pattern); } [Fact] - public void MatcherMapper_Map_MatcherModel_SimMetricsMatcher_Throws2() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialWildcardMatcher_RejectOnMatch() { // Assign + var pattern = "{ \"x\": \"*\" }"; var model = new MatcherModel { - Name = "SimMetricsMatcher.error", - Pattern = "x" + Name = "SystemTextJsonPartialWildcardMatcher", + Pattern = pattern, + RejectOnMatch = true }; // Act - Action act = () => _sut.Map(model); + var matcher = (SystemTextJsonPartialWildcardMatcher)_sut.Map(model)!; // Assert - act.Should().Throw(); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.RejectOnMatch); + matcher.Value.Should().Be(pattern); } [Fact] - public void MatcherMapper_Map_MatcherModel_MatcherModelToCustomMatcher() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPartialWildcardMatcher_IgnoreCaseTrue() { - // Arrange - var patternModel = new CustomPathParamMatcherModel("/customer/{customerId}/document/{documentId}", - new Dictionary(2) - { - { "customerId", @"^[0-9]+$" }, - { "documentId", @"^[0-9a-zA-Z\-\_]+\.[a-zA-Z]+$" } - }); + // Assign + var pattern = "{ \"name\": \"t*\" }"; var model = new MatcherModel { - Name = nameof(CustomPathParamMatcher), - Pattern = JsonConvert.SerializeObject(patternModel) + Name = "SystemTextJsonPartialWildcardMatcher", + Pattern = pattern, + IgnoreCase = true }; - var settings = new WireMockServerSettings(); - settings.CustomMatcherMappings = settings.CustomMatcherMappings ?? new Dictionary>(); - settings.CustomMatcherMappings[nameof(CustomPathParamMatcher)] = matcherModel => - { - var matcherParams = JsonConvert.DeserializeObject((string)matcherModel.Pattern!)!; - return new CustomPathParamMatcher( - matcherModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch, - matcherParams.Path, - matcherParams.PathParams - ); - }; - var sut = new MatcherMapper(settings); + // Act + var matcher = (SystemTextJsonPartialWildcardMatcher)_sut.Map(model)!; + + // Assert + matcher.IgnoreCase.Should().BeTrue(); + matcher.Value.Should().Be(pattern); + } + + [Fact] + public void MatcherMapper_Map_Matcher_SystemTextJsonPartialWildcardMatcher_To_MatcherModel() + { + // Assign + var pattern = new { Id = 1, Name = "T*" }; + var matcher = new SystemTextJsonPartialWildcardMatcher(MatchBehaviour.AcceptOnMatch, pattern, ignoreCase: true); // Act - var matcher = sut.Map(model) as CustomPathParamMatcher; + var model = _sut.Map(matcher)!; // Assert - matcher.Should().NotBeNull(); + model.Name.Should().Be(nameof(SystemTextJsonPartialWildcardMatcher)); + model.Pattern.Should().BeEquivalentTo(pattern); + model.IgnoreCase.Should().BeTrue(); + model.Regex.Should().BeFalse(); + model.RejectOnMatch.Should().BeNull(); } [Fact] - public void MatcherMapper_Map_MatcherModel_CustomMatcherToMatcherModel() + public void MatcherMapper_Map_Matcher_SystemTextJsonPartialWildcardMatcher_RejectOnMatch_To_MatcherModel() { - // Arrange - var matcher = new CustomPathParamMatcher("/customer/{customerId}/document/{documentId}", - new Dictionary(2) - { - { "customerId", @"^[0-9]+$" }, - { "documentId", @"^[0-9a-zA-Z\-\_]+\.[a-zA-Z]+$" } - }); + // Assign + var pattern = "{ \"Name\": \"T*\" }"; + var matcher = new SystemTextJsonPartialWildcardMatcher(MatchBehaviour.RejectOnMatch, pattern); // Act var model = _sut.Map(matcher)!; // Assert - using (new AssertionScope()) + model.Name.Should().Be(nameof(SystemTextJsonPartialWildcardMatcher)); + model.Pattern.Should().Be(pattern); + model.RejectOnMatch.Should().BeTrue(); + model.Regex.Should().BeFalse(); + } + + #endregion + + #region SystemTextJsonPathMatcher + + [Fact] + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPathMatcher_SinglePattern() + { + // Arrange + var model = new MatcherModel { - model.Should().NotBeNull(); - model.Name.Should().Be(nameof(CustomPathParamMatcher)); - - var matcherParams = JsonConvert.DeserializeObject((string)model.Pattern!)!; - matcherParams.Path.Should().Be("/customer/{customerId}/document/{documentId}"); - matcherParams.PathParams.Should().BeEquivalentTo(new Dictionary(2) - { - { "customerId", @"^[0-9]+$" }, - { "documentId", @"^[0-9a-zA-Z\-\_]+\.[a-zA-Z]+$" } - }); - } + Name = "SystemTextJsonPathMatcher", + Pattern = "$.Id" + }; + + // Act + var matcher = (SystemTextJsonPathMatcher)_sut.Map(model)!; + + // Assert + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.GetPatterns().Should().ContainSingle().Which.First.Should().Be("$.Id"); } [Fact] - public void MatcherMapper_Map_MatcherModel_GraphQLMatcher() + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPathMatcher_MultiplePatterns() { // Arrange - const string testSchema = @" - scalar DateTime - scalar MyCustomScalar + var model = new MatcherModel + { + Name = "SystemTextJsonPathMatcher", + Patterns = ["$.Id", "$.Name"], + MatchOperator = "And" + }; - type Message { - id: ID! - } + // Act + var matcher = (SystemTextJsonPathMatcher)_sut.Map(model)!; - type Mutation { - createMessage(x: MyCustomScalar, dt: DateTime): Message - }"; + // Assert + matcher.MatchOperator.Should().Be(MatchOperator.And); + matcher.GetPatterns().Select(p => p.First).Should().BeEquivalentTo("$.Id", "$.Name"); + } - var customScalars = new Dictionary { { "MyCustomScalar", typeof(string) } }; + [Fact] + public void MatcherMapper_Map_MatcherModel_SystemTextJsonPathMatcher_RejectOnMatch() + { + // Arrange var model = new MatcherModel { - Name = nameof(GraphQLMatcher), - Pattern = testSchema, - CustomScalars = customScalars + Name = "SystemTextJsonPathMatcher", + Pattern = "$.Id", + RejectOnMatch = true }; // Act - var matcher = (IGraphQLMatcher)_sut.Map(model)!; + var matcher = (SystemTextJsonPathMatcher)_sut.Map(model)!; // Assert - matcher.GetPatterns().Should().HaveElementAt(0, testSchema); - matcher.Name.Should().Be(nameof(GraphQLMatcher)); - matcher.CustomScalars.Should().BeEquivalentTo(customScalars); + matcher.MatchBehaviour.Should().Be(MatchBehaviour.RejectOnMatch); } [Fact] - public void MatcherMapper_Map_MatcherModel_ProtoBufMatcher() + public void MatcherMapper_Map_Matcher_SystemTextJsonPathMatcher_To_MatcherModel_SinglePattern() { // Arrange - const string protoDefinition = @" -syntax = ""proto3""; + var matcher = new SystemTextJsonPathMatcher("$.Id"); -package greet; + // Act + var model = _sut.Map(matcher)!; -service Greeter { - rpc SayHello (HelloRequest) returns (HelloReply); -} + // Assert + model.Name.Should().Be(nameof(SystemTextJsonPathMatcher)); + model.Pattern.Should().Be("$.Id"); + model.Patterns.Should().BeNull(); + model.RejectOnMatch.Should().BeNull(); + } -message HelloRequest { - string name = 1; -} + [Fact] + public void MatcherMapper_Map_Matcher_SystemTextJsonPathMatcher_To_MatcherModel_MultiplePatterns() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher(MatchBehaviour.AcceptOnMatch, MatchOperator.And, "$.Id", "$.Name"); -message HelloReply { - string message = 1; -} -"; - const string messageType = "greet.HelloRequest"; + // Act + var model = _sut.Map(matcher)!; - var jsonMatcherPattern = new { name = "stef" }; + // Assert + model.Name.Should().Be(nameof(SystemTextJsonPathMatcher)); + model.Pattern.Should().BeNull(); + model.Patterns.Should().BeEquivalentTo(["$.Id", "$.Name"]); + model.MatchOperator.Should().Be("And"); + } - var model = new MatcherModel - { - Name = nameof(ProtoBufMatcher), - Pattern = protoDefinition, - ProtoBufMessageType = messageType, - ContentMatcher = new MatcherModel - { - Name = nameof(JsonMatcher), - Pattern = jsonMatcherPattern - } - }; + [Fact] + public void MatcherMapper_Map_Matcher_SystemTextJsonPathMatcher_RejectOnMatch_To_MatcherModel() + { + // Arrange + var matcher = new SystemTextJsonPathMatcher(MatchBehaviour.RejectOnMatch, MatchOperator.Or, "$.Id"); // Act - var matcher = (ProtoBufMatcher)_sut.Map(model)!; + var model = _sut.Map(matcher)!; // Assert - matcher.ProtoDefinition().Texts.Should().ContainSingle(protoDefinition); - matcher.Name.Should().Be(nameof(ProtoBufMatcher)); - matcher.MessageType.Should().Be(messageType); - matcher.Matcher?.Value.Should().Be(jsonMatcherPattern); + model.Name.Should().Be(nameof(SystemTextJsonPathMatcher)); + model.Pattern.Should().Be("$.Id"); + model.RejectOnMatch.Should().BeTrue(); } + + #endregion } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Settings/SimpleSettingsParserTests.cs b/test/WireMock.Net.Tests/Settings/SimpleSettingsParserTests.cs index 2da5d56ae..0f5276985 100644 --- a/test/WireMock.Net.Tests/Settings/SimpleSettingsParserTests.cs +++ b/test/WireMock.Net.Tests/Settings/SimpleSettingsParserTests.cs @@ -1,5 +1,6 @@ // Copyright © WireMock.Net +using JsonConverter.Newtonsoft.Json; using WireMock.Settings; using WireMock.Types; @@ -18,7 +19,7 @@ public SimpleSettingsParserTests() public void SimpleCommandLineParser_Parse_Arguments() { // Assign - _parser.Parse(new[] { "--test1", "one", "--test2", "2", "--test3", "3", "--test4", "true", "--test5", "Https" }); + _parser.Parse(["--test1", "one", "--test2", "2", "--test3", "3", "--test4", "true", "--test5", "Https"]); // Act string? stringValue = _parser.GetStringValue("test1"); @@ -46,7 +47,7 @@ public void SimpleCommandLineParser_Parse_Environment() { "WireMockServerSettings__test1", "one" }, { "WireMockServerSettings__test2", "two" } }; - _parser.Parse(new string[0], env); + _parser.Parse([], env); // Act string? value1 = _parser.GetStringValue("test1"); @@ -61,7 +62,7 @@ public void SimpleCommandLineParser_Parse_Environment() public void SimpleCommandLineParser_Parse_ArgumentsAsCombinedKeyAndValue() { // Assign - _parser.Parse(new[] { "--test1 one", "--test2 two", "--test3 three" }); + _parser.Parse(["--test1 one", "--test2 two", "--test3 three"]); // Act string? value1 = _parser.GetStringValue("test1"); @@ -78,7 +79,7 @@ public void SimpleCommandLineParser_Parse_ArgumentsAsCombinedKeyAndValue() public void SimpleCommandLineParser_Parse_ArgumentsMixed() { // Assign - _parser.Parse(new[] { "--test1 one", "--test2", "two", "--test3 three" }); + _parser.Parse(["--test1 one", "--test2", "two", "--test3 three"]); // Act string? value1 = _parser.GetStringValue("test1"); @@ -95,7 +96,7 @@ public void SimpleCommandLineParser_Parse_ArgumentsMixed() public void SimpleCommandLineParser_Parse_GetBoolValue() { // Assign - _parser.Parse(new[] { "'--test1", "false'", "--test2 true" }); + _parser.Parse(["'--test1", "false'", "--test2 true"]); // Act bool value1 = _parser.GetBoolValue("test1"); @@ -112,7 +113,7 @@ public void SimpleCommandLineParser_Parse_GetBoolValue() public void SimpleCommandLineParser_Parse_GetBoolWithDefault() { // Assign - _parser.Parse(new[] { "--test1", "true", "--test2", "false" }); + _parser.Parse(["--test1", "true", "--test2", "false"]); // Act bool value1 = _parser.GetBoolWithDefault("test1", "test1_fallback", defaultValue: false); @@ -134,7 +135,7 @@ public void SimpleCommandLineParser_Parse_Environment_GetBoolValue() { "WireMockServerSettings__test1", "false" }, { "WireMockServerSettings__test2", "true" } }; - _parser.Parse(new string[0], env); + _parser.Parse([], env); // Act bool value1 = _parser.GetBoolValue("test1"); @@ -151,7 +152,7 @@ public void SimpleCommandLineParser_Parse_Environment_GetBoolValue() public void SimpleCommandLineParser_Parse_GetIntValue() { // Assign - _parser.Parse(new[] { "--test1", "42", "--test2 55" }); + _parser.Parse(["--test1", "42", "--test2 55"]); // Act int? value1 = _parser.GetIntValue("test1"); @@ -175,7 +176,7 @@ public void SimpleCommandLineParser_Parse_Environment_GetIntValue() { "WireMockServerSettings__test1", "42" }, { "WireMockServerSETTINGS__TEST2", "55" } }; - _parser.Parse(new string[0], env); + _parser.Parse([], env); // Act int? value1 = _parser.GetIntValue("test1"); @@ -194,10 +195,10 @@ public void SimpleCommandLineParser_Parse_Environment_GetIntValue() public void SimpleCommandLineParser_Parse_GetObjectValueFromJson() { // Assign - _parser.Parse(new[] { @"--json {""k1"":""v1"",""k2"":""v2""}" }); + _parser.Parse([@"--json {""k1"":""v1"",""k2"":""v2""}"]); // Act - var value = _parser.GetObjectValueFromJson>("json"); + var value = _parser.GetObjectValueFromJson>("json", new NewtonsoftJsonConverter()); // Assert var expected = new Dictionary @@ -207,4 +208,4 @@ public void SimpleCommandLineParser_Parse_GetObjectValueFromJson() }; value.Should().BeEquivalentTo(expected); } -} +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Transformers/JsonBodyTransformerTests.cs b/test/WireMock.Net.Tests/Transformers/JsonBodyTransformerTests.cs new file mode 100644 index 000000000..1de0db5d0 --- /dev/null +++ b/test/WireMock.Net.Tests/Transformers/JsonBodyTransformerTests.cs @@ -0,0 +1,166 @@ +// Copyright © WireMock.Net + +using System.Text; +using System.Text.Json.Nodes; +using Moq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WireMock.Handlers; +using WireMock.Settings; +using WireMock.Transformers; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Net.Tests.Transformers; + +public class JsonBodyTransformerTests +{ + public static TheoryData Transformers + { + get + { + var settings = new WireMockServerSettings(); + + return + [ + new JsonBodyTransformerTestContext( + () => new NewtonsoftJsonBodyTransformer(settings), + JObject.Parse, + body => ((JToken)body).ToString(Formatting.None)), + + new JsonBodyTransformerTestContext( + () => new SystemTextJsonBodyTransformer(settings), + json => JsonNode.Parse(json)!, + body => ((JsonNode)body).ToJsonString()) + ]; + } + } + + [Theory] + [MemberData(nameof(Transformers))] + public void TransformBodyAsJson_Replaces_String_Value_And_Preserves_Original(JsonBodyTransformerTestContext testContext) + { + // Arrange + var transformer = testContext.CreateTransformer(); + var originalJson = testContext.ParseJson("{\"value\":\"{{number}}\"}"); + var bodyData = new BodyData + { + Encoding = Encoding.UTF8, + DetectedBodyType = BodyType.Json, + DetectedBodyTypeFromContentType = BodyType.Json, + ProtoBufMessageType = "My.Message", + BodyAsJson = originalJson + }; + + var transformerContext = new FakeTransformerContext( + text => text, + text => text == "{{number}}" ? "123" : text); + + // Act + var result = transformer.TransformBodyAsJson(transformerContext, ReplaceNodeOptions.EvaluateAndTryToConvert, new { }, bodyData); + + // Assert + result.Encoding.Should().Be(Encoding.UTF8); + result.DetectedBodyType.Should().Be(BodyType.Json); + result.DetectedBodyTypeFromContentType.Should().Be(BodyType.Json); + result.ProtoBufMessageType.Should().Be("My.Message"); + result.BodyAsJson.Should().NotBeNull(); + testContext.SerializeJson(result.BodyAsJson).Should().Be("{\"value\":123}"); + testContext.SerializeJson(originalJson).Should().Be("{\"value\":\"{{number}}\"}"); + } + + [Theory] + [MemberData(nameof(Transformers))] + public void TransformBodyAsJson_With_String_Body_Replaces_Single_Node_With_Object(JsonBodyTransformerTestContext testContext) + { + // Arrange + var transformer = testContext.CreateTransformer(); + var bodyData = new BodyData + { + DetectedBodyType = BodyType.Json, + BodyAsJson = "{{json}}" + }; + + var transformerContext = new FakeTransformerContext( + text => text == "{{json}}" ? "{\"name\":\"test\"}" : text, + text => text); + + // Act + var result = transformer.TransformBodyAsJson(transformerContext, ReplaceNodeOptions.EvaluateAndTryToConvert, new { }, bodyData); + + // Assert + result.BodyAsJson.Should().NotBeNull(); + testContext.SerializeJson(result.BodyAsJson).Should().Be("{\"name\":\"test\"}"); + } + + [Theory] + [MemberData(nameof(Transformers))] + public void TransformBodyAsJson_Replaces_String_Value_With_WireMockList_As_Array(JsonBodyTransformerTestContext testContext) + { + // Arrange + var transformer = testContext.CreateTransformer(); + var bodyData = new BodyData + { + DetectedBodyType = BodyType.Json, + BodyAsJson = testContext.ParseJson("{\"values\":\"{{list}}\"}") + }; + + var transformerContext = new FakeTransformerContext( + text => text, + text => text == "{{list}}" ? new WireMockList(["a", "b"]) : text); + + // Act + var result = transformer.TransformBodyAsJson(transformerContext, ReplaceNodeOptions.EvaluateAndTryToConvert, new { }, bodyData); + + // Assert + result.BodyAsJson.Should().NotBeNull(); + testContext.SerializeJson(result.BodyAsJson).Should().Be("{\"values\":[\"a\",\"b\"]}"); + } + + public sealed class JsonBodyTransformerTestContext + { + private readonly Func _createTransformer; + private readonly Func _parseJson; + private readonly Func _serializeJson; + + public JsonBodyTransformerTestContext( + Func createTransformer, + Func parseJson, + Func serializeJson) + { + _createTransformer = createTransformer; + _parseJson = parseJson; + _serializeJson = serializeJson; + } + + public IJsonBodyTransformer CreateTransformer() + { + return _createTransformer(); + } + + public object ParseJson(string json) + { + return _parseJson(json); + } + + public string SerializeJson(object body) + { + return _serializeJson(body); + } + } + + private sealed class FakeTransformerContext(Func parseAndRender, Func parseAndEvaluate) : ITransformerContext + { + public IFileSystemHandler FileSystemHandler { get; private set; } = Mock.Of(); + + public string ParseAndRender(string text, object model) + { + return parseAndRender(text); + } + + public object ParseAndEvaluate(string text, object model) + { + return parseAndEvaluate(text); + } + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WebSockets/WebSocketIntegrationTests.cs b/test/WireMock.Net.Tests/WebSockets/WebSocketIntegrationTests.cs index 201e652ab..e8a8a9d38 100644 --- a/test/WireMock.Net.Tests/WebSockets/WebSocketIntegrationTests.cs +++ b/test/WireMock.Net.Tests/WebSockets/WebSocketIntegrationTests.cs @@ -750,6 +750,8 @@ public async Task WithWebSocketProxy_Should_Proxy_Multiple_TextMessages() { await client.SendAsync(testMessage, cancellationToken: _ct); + await Task.Delay(250, _ct); + var received = await client.ReceiveAsTextAsync(cancellationToken: _ct); received.Should().Be(testMessage, $"message '{testMessage}' should be proxied and echoed back"); } diff --git a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj index e8bc4c084..10dc7f75b 100644 --- a/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj +++ b/test/WireMock.Net.Tests/WireMock.Net.Tests.csproj @@ -50,6 +50,7 @@ + @@ -75,7 +76,6 @@ - diff --git a/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs b/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs index 582613335..7bba8581d 100644 --- a/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs +++ b/test/WireMock.Net.Tests/WireMockServerTests.WithBody.cs @@ -7,6 +7,7 @@ using System.Text.Json.Serialization; using JsonConverter.System.Text.Json; using WireMock.Matchers; +using WireMock.Net.Xunit; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Server; @@ -232,9 +233,13 @@ public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_JsonPartialW public async Task WireMockServer_WithBodyAsJson_Using_PostAsync_And_JsonPartialWildcardMatcher_And_SystemTextJson_ShouldMatch() { // Arrange - using var server = WireMockServer.Start(x => x.DefaultJsonSerializer = new SystemTextJsonConverter()); + using var server = WireMockServer.Start(settings => + { + settings.Logger = new TestOutputHelperWireMockLogger(testOutputHelper); + settings.DefaultJsonSerializer = new SystemTextJsonConverter(); + }); - var matcher = new JsonPartialWildcardMatcher(new { id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true); + var matcher = new SystemTextJsonPartialWildcardMatcher(new { id = "^[a-f0-9]{32}-[0-9]$" }, ignoreCase: true, regex: true); server.Given(Request.Create() .WithHeader("Content-Type", "application/json*") .UsingPost()