diff --git a/src/WireMock.Net.Minimal/Matchers/AbstractJsonPartialMatcher.cs b/src/WireMock.Net.Minimal/Matchers/AbstractJsonPartialMatcher.cs index f2f853cb..f1e25ccc 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/JsonMatcher.cs b/src/WireMock.Net.Minimal/Matchers/JsonMatcher.cs index 64a04733..51635dac 100644 --- a/src/WireMock.Net.Minimal/Matchers/JsonMatcher.cs +++ b/src/WireMock.Net.Minimal/Matchers/JsonMatcher.cs @@ -1,6 +1,5 @@ // Copyright © WireMock.Net -using System.Linq; using Newtonsoft.Json.Linq; using Stef.Validation; using WireMock.Extensions; diff --git a/src/WireMock.Net.Minimal/Matchers/SystemTextJsonMatcher.cs b/src/WireMock.Net.Minimal/Matchers/SystemTextJsonMatcher.cs new file mode 100644 index 00000000..1f472343 --- /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/test/WireMock.Net.Tests/Matchers/SystemTextJsonMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/SystemTextJsonMatcherTests.cs new file mode 100644 index 00000000..cbc7ba73 --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/SystemTextJsonMatcherTests.cs @@ -0,0 +1,369 @@ +// 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("{}"); + + // Act + var result = matcher.IsMatch(new MemoryStream()); + + // 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/Serialization/MatcherMapperTests.cs b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs index d32e8517..580bfdf8 100644 --- a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs @@ -523,7 +523,7 @@ message HelloReply { }; // Act - var matcher = (JsonMatcher)_sut.Map(model)!; + var matcher = (IJsonMatcher)_sut.Map(model)!; // Assert matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch);