diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs index 6e133ab4..d7837ffc 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/MatcherModel.cs @@ -48,7 +48,7 @@ public class MatcherModel /// public string? MatchOperator { get; set; } - #region JsonPartialMatcher and JsonPartialWildcardMatcher + #region JsonMatcher, JsonPartialMatcher and JsonPartialWildcardMatcher /// /// Support Regex. /// diff --git a/src/WireMock.Net/Matchers/AbstractJsonPartialMatcher.cs b/src/WireMock.Net/Matchers/AbstractJsonPartialMatcher.cs index 0555285a..1cbdd6df 100644 --- a/src/WireMock.Net/Matchers/AbstractJsonPartialMatcher.cs +++ b/src/WireMock.Net/Matchers/AbstractJsonPartialMatcher.cs @@ -10,20 +10,15 @@ namespace WireMock.Matchers; /// public abstract class AbstractJsonPartialMatcher : JsonMatcher { - /// - /// Support Regex - /// - public bool Regex { get; } - /// /// 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. - protected AbstractJsonPartialMatcher(string value, bool ignoreCase = false, bool regex = false) : base(value, ignoreCase) + protected AbstractJsonPartialMatcher(string value, bool ignoreCase = false, bool regex = false) : + base(value, ignoreCase, regex) { - Regex = regex; } /// @@ -32,9 +27,9 @@ protected AbstractJsonPartialMatcher(string value, bool ignoreCase = false, bool /// The object value to check for equality. /// Ignore the case from the PropertyName and PropertyValue (string only). /// Support Regex. - protected AbstractJsonPartialMatcher(object value, bool ignoreCase = false, bool regex = false) : base(value, ignoreCase) + protected AbstractJsonPartialMatcher(object value, bool ignoreCase = false, bool regex = false) : + base(value, ignoreCase, regex) { - Regex = regex; } /// @@ -44,15 +39,15 @@ protected AbstractJsonPartialMatcher(object value, bool ignoreCase = false, bool /// The value to check for equality. /// Ignore the case from the PropertyName and PropertyValue (string only). /// Support Regex. - protected AbstractJsonPartialMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) : base(matchBehaviour, value, ignoreCase) + protected AbstractJsonPartialMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) : + base(matchBehaviour, value, ignoreCase, regex) { - Regex = regex; } /// - protected override bool IsMatch(JToken? value, JToken? input) + protected override bool IsMatch(JToken value, JToken? input) { - if (value == null || value == input) + if (value == input) { return true; } @@ -72,7 +67,7 @@ protected override bool IsMatch(JToken? value, JToken? input) ((value.Type == JTokenType.Guid && input.Type == JTokenType.String) || (value.Type == JTokenType.String && input.Type == JTokenType.Guid))) { - return IsMatch(value.ToString(), input.ToString()); + return IsMatch(value.ToString().ToUpperInvariant(), input.ToString().ToUpperInvariant()); } if (input == null || value.Type != input.Type) diff --git a/src/WireMock.Net/Matchers/JsonMatcher.cs b/src/WireMock.Net/Matchers/JsonMatcher.cs index a259ab5d..5d1f7040 100644 --- a/src/WireMock.Net/Matchers/JsonMatcher.cs +++ b/src/WireMock.Net/Matchers/JsonMatcher.cs @@ -1,8 +1,10 @@ using System; +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; using Stef.Validation; using WireMock.Util; +using JsonUtils = WireMock.Util.JsonUtils; namespace WireMock.Matchers; @@ -23,6 +25,11 @@ public class JsonMatcher : IJsonMatcher /// public bool IgnoreCase { get; } + /// + /// Support Regex + /// + public bool Regex { get; } + private readonly JToken _valueAsJToken; private readonly Func _jTokenConverter; @@ -31,7 +38,8 @@ public class JsonMatcher : IJsonMatcher /// /// The string value to check for equality. /// Ignore the case from the PropertyName and PropertyValue (string only). - public JsonMatcher(string value, bool ignoreCase = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase) + /// Support Regex. + public JsonMatcher(string value, bool ignoreCase = false, bool regex = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex) { } @@ -40,7 +48,8 @@ public JsonMatcher(string value, bool ignoreCase = false) : this(MatchBehaviour. /// /// The object value to check for equality. /// Ignore the case from the PropertyName and PropertyValue (string only). - public JsonMatcher(object value, bool ignoreCase = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase) + /// Support Regex. + public JsonMatcher(object value, bool ignoreCase = false, bool regex = false) : this(MatchBehaviour.AcceptOnMatch, value, ignoreCase, regex) { } @@ -50,12 +59,14 @@ public JsonMatcher(object value, bool ignoreCase = false) : this(MatchBehaviour. /// The match behaviour. /// The value to check for equality. /// Ignore the case from the PropertyName and PropertyValue (string only). - public JsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false) + /// Support Regex. + public JsonMatcher(MatchBehaviour matchBehaviour, object value, bool ignoreCase = false, bool regex = false) { Guard.NotNull(value); MatchBehaviour = matchBehaviour; IgnoreCase = ignoreCase; + Regex = regex; Value = value; _valueAsJToken = JsonUtils.ConvertValueToJToken(value); @@ -93,9 +104,79 @@ public MatchResult IsMatch(object? input) /// Matcher value /// Input value /// - protected virtual bool IsMatch(JToken value, JToken input) + protected virtual bool IsMatch(JToken value, JToken? input) { - return JToken.DeepEquals(value, input); + // If equal, return true. + if (input == value) + { + return true; + } + + // If input, return false. + if (input == null) + { + return false; + } + + // If using Regex and the value is a string, use the MatchRegex method. + if (Regex && value.Type == JTokenType.String) + { + var valueAsString = value.ToString(); + + var (valid, result) = RegexUtils.MatchRegex(valueAsString, input.ToString()); + if (valid) + { + return result; + } + } + + // If the value is a Guid and the input is a string, or vice versa, convert them to strings and compare the string values. + if ((value.Type == JTokenType.Guid && input.Type == JTokenType.String) || (value.Type == JTokenType.String && input.Type == JTokenType.Guid)) + { + return JToken.DeepEquals(value.ToString().ToUpperInvariant(), input.ToString().ToUpperInvariant()); + } + + switch (value.Type) + { + // If the value is an object, compare all properties. + case JTokenType.Object: + var valueProperties = value.ToObject>() ?? new Dictionary(); + var inputProperties = input.ToObject>() ?? new Dictionary(); + + // If the number of properties is different, return false. + if (valueProperties.Count != inputProperties.Count) + { + return false; + } + + // Compare all properties. The input must match all properties of the value. + foreach (var pair in valueProperties) + { + if (!IsMatch(pair.Value, inputProperties[pair.Key])) + { + return false; + } + } + + return true; + + // If the value is an array, compare all elements. + case JTokenType.Array: + var valueArray = value.ToObject() ?? EmptyArray.Value; + var inputArray = input.ToObject() ?? EmptyArray.Value; + + // If the number of elements is different, return false. + if (valueArray.Length != inputArray.Length) + { + return false; + } + + return !valueArray.Where((valueToken, index) => !IsMatch(valueToken, inputArray[index])).Any(); + + default: + // Use JToken.DeepEquals() for all other types. + return JToken.DeepEquals(value, input); + } } private static string? ToUpper(string? input) diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs index 94c068d2..0041e20d 100644 --- a/src/WireMock.Net/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net/Serialization/MatcherMapper.cs @@ -84,7 +84,7 @@ public MatcherMapper(WireMockServerSettings settings) case nameof(JsonMatcher): var valueForJsonMatcher = matcherModel.Pattern ?? matcherModel.Patterns; - return new JsonMatcher(matchBehaviour, valueForJsonMatcher!, ignoreCase); + return new JsonMatcher(matchBehaviour, valueForJsonMatcher!, ignoreCase, useRegex); case nameof(JsonPartialMatcher): var valueForJsonPartialMatcher = matcherModel.Pattern ?? matcherModel.Patterns; @@ -152,12 +152,8 @@ public MatcherMapper(WireMockServerSettings settings) switch (matcher) { - case JsonPartialMatcher jsonPartialMatcher: - model.Regex = jsonPartialMatcher.Regex; - break; - - case JsonPartialWildcardMatcher jsonPartialWildcardMatcher: - model.Regex = jsonPartialWildcardMatcher.Regex; + case JsonMatcher jsonMatcher: + model.Regex = jsonMatcher.Regex; break; case XPathMatcher xpathMatcher: diff --git a/test/WireMock.Net.Tests/Matchers/JsonMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/JsonMatcherTests.cs index 266c590b..257f23af 100644 --- a/test/WireMock.Net.Tests/Matchers/JsonMatcherTests.cs +++ b/test/WireMock.Net.Tests/Matchers/JsonMatcherTests.cs @@ -40,7 +40,7 @@ public void JsonMatcher_GetName() var matcher = new JsonMatcher("{}"); // Act - string name = matcher.Name; + var name = matcher.Name; // Assert Check.That(name).Equals("JsonMatcher"); @@ -53,7 +53,7 @@ public void JsonMatcher_GetValue() var matcher = new JsonMatcher("{}"); // Act - object value = matcher.Value; + var value = matcher.Value; // Assert Check.That(value).Equals("{}"); @@ -90,7 +90,7 @@ public void JsonMatcher_IsMatch_WithInvalidValue_Should_ReturnMismatch_And_Excep // Act var result = matcher.IsMatch(new MemoryStream()); - // Assert + // Assert result.Score.Should().Be(MatchScores.Mismatch); result.Exception.Should().BeAssignableTo(); } @@ -102,10 +102,10 @@ public void JsonMatcher_IsMatch_ByteArray() var bytes = EmptyArray.Value; var matcher = new JsonMatcher(""); - // Act - double match = matcher.IsMatch(bytes).Score; + // Act + var match = matcher.IsMatch(bytes).Score; - // Assert + // Assert Check.That(match).IsEqualTo(0); } @@ -116,10 +116,10 @@ public void JsonMatcher_IsMatch_NullString() string? s = null; var matcher = new JsonMatcher(""); - // Act - double match = matcher.IsMatch(s).Score; + // Act + var match = matcher.IsMatch(s).Score; - // Assert + // Assert Check.That(match).IsEqualTo(0); } @@ -130,215 +130,399 @@ public void JsonMatcher_IsMatch_NullObject() object? o = null; var matcher = new JsonMatcher(""); - // Act - double match = matcher.IsMatch(o).Score; + // Act + var match = matcher.IsMatch(o).Score; - // Assert + // Assert Check.That(match).IsEqualTo(0); } [Fact] public void JsonMatcher_IsMatch_JArray() { - // Assign + // Assign var matcher = new JsonMatcher(new[] { "x", "y" }); - // Act + // Act var jArray = new JArray { "x", "y" }; - double match = matcher.IsMatch(jArray).Score; + var match = matcher.IsMatch(jArray).Score; - // Assert + // Assert Assert.Equal(1.0, match); } [Fact] - public void JsonMatcher_IsMatch_JObject() + public void JsonMatcher_IsMatch_JObject_ShouldMatch() { - // Assign + // Assign var matcher = new JsonMatcher(new { Id = 1, Name = "Test" }); - // Act + // Act var jObject = new JObject { { "Id", new JValue(1) }, { "Name", new JValue("Test") } }; - double match = matcher.IsMatch(jObject).Score; + var match = matcher.IsMatch(jObject).Score; - // Assert + // Assert Assert.Equal(1.0, match); } + [Fact] + public void JsonMatcher_IsMatch_JObject_ShouldNotMatch() + { + // Assign + var matcher = new JsonMatcher(new { Id = 1, Name = "Test" }); + + // Act + var jObject = new JObject + { + { "Id", new JValue(1) }, + { "Name", new JValue("Test") }, + { "Other", new JValue("abc") } + }; + var score = matcher.IsMatch(jObject).Score; + + // Assert + Assert.Equal(MatchScores.Mismatch, score); + } + [Fact] public void JsonMatcher_IsMatch_WithIgnoreCaseTrue_JObject() { - // Assign + // Assign var matcher = new JsonMatcher(new { id = 1, Name = "test" }, true); - // Act + // Act var jObject = new JObject { { "Id", new JValue(1) }, { "NaMe", new JValue("Test") } }; - double match = matcher.IsMatch(jObject).Score; + var match = matcher.IsMatch(jObject).Score; - // Assert + // Assert Assert.Equal(1.0, match); } [Fact] public void JsonMatcher_IsMatch_JObjectParsed() { - // Assign + // Assign var matcher = new JsonMatcher(new { Id = 1, Name = "Test" }); - // Act + // Act var jObject = JObject.Parse("{ \"Id\" : 1, \"Name\" : \"Test\" }"); - double match = matcher.IsMatch(jObject).Score; + var match = matcher.IsMatch(jObject).Score; - // Assert + // Assert Assert.Equal(1.0, match); } [Fact] public void JsonMatcher_IsMatch_WithIgnoreCaseTrue_JObjectParsed() { - // Assign + // Assign var matcher = new JsonMatcher(new { Id = 1, Name = "TESt" }, true); - // Act + // Act var jObject = JObject.Parse("{ \"Id\" : 1, \"Name\" : \"Test\" }"); - double match = matcher.IsMatch(jObject).Score; + var match = matcher.IsMatch(jObject).Score; - // Assert + // Assert Assert.Equal(1.0, match); } [Fact] public void JsonMatcher_IsMatch_JArrayAsString() { - // Assign + // Assign var matcher = new JsonMatcher("[ \"x\", \"y\" ]"); - // Act + // Act var jArray = new JArray { "x", "y" }; - double match = matcher.IsMatch(jArray).Score; + var match = matcher.IsMatch(jArray).Score; - // Assert + // Assert Assert.Equal(1.0, match); } [Fact] public void JsonMatcher_IsMatch_JObjectAsString() { - // Assign + // Assign var matcher = new JsonMatcher("{ \"Id\" : 1, \"Name\" : \"Test\" }"); - // Act + // Act var jObject = new JObject { { "Id", new JValue(1) }, { "Name", new JValue("Test") } }; - double match = matcher.IsMatch(jObject).Score; + var match = matcher.IsMatch(jObject).Score; - // Assert + // Assert Assert.Equal(1.0, match); } [Fact] public void JsonMatcher_IsMatch_WithIgnoreCaseTrue_JObjectAsString() { - // Assign + // Assign var matcher = new JsonMatcher("{ \"Id\" : 1, \"Name\" : \"test\" }", true); - // Act + // Act var jObject = new JObject { { "Id", new JValue(1) }, { "Name", new JValue("Test") } }; - double match = matcher.IsMatch(jObject).Score; + var match = matcher.IsMatch(jObject).Score; - // Assert + // Assert Assert.Equal(1.0, match); } [Fact] public void JsonMatcher_IsMatch_JObjectAsString_RejectOnMatch() { - // Assign + // Assign var matcher = new JsonMatcher(MatchBehaviour.RejectOnMatch, "{ \"Id\" : 1, \"Name\" : \"Test\" }"); - // Act + // Act var jObject = new JObject { { "Id", new JValue(1) }, { "Name", new JValue("Test") } }; - double match = matcher.IsMatch(jObject).Score; + var match = matcher.IsMatch(jObject).Score; - // Assert + // Assert Assert.Equal(0.0, match); } [Fact] public void JsonMatcher_IsMatch_JObjectWithDateTimeOffsetAsString() { - // Assign + // Assign var matcher = new JsonMatcher("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }"); - // Act + // Act var jObject = new JObject { { "preferredAt", new JValue("2019-11-21T10:32:53.2210009+00:00") } }; - double match = matcher.IsMatch(jObject).Score; + var match = matcher.IsMatch(jObject).Score; - // Assert + // Assert Assert.Equal(1.0, match); } [Fact] public void JsonMatcher_IsMatch_NormalEnum() { - // Assign - var matcher = new JsonMatcher(new Test1 { NormalEnum = NormalEnum.Abc}); + // Assign + var matcher = new JsonMatcher(new Test1 { NormalEnum = NormalEnum.Abc }); - // Act + // Act var jObject = new JObject { { "NormalEnum", new JValue(0) } }; - double match = matcher.IsMatch(jObject).Score; + var match = matcher.IsMatch(jObject).Score; - // Assert + // Assert match.Should().Be(1.0); } [Fact] public void JsonMatcher_IsMatch_EnumWithJsonConverter() { - // Assign + // Assign var matcher = new JsonMatcher(new Test2 { EnumWithJsonConverter = EnumWithJsonConverter.Type1 }); - // Act + // Act var jObject = new JObject { { "EnumWithJsonConverter", new JValue("Type1") } }; - double match = matcher.IsMatch(jObject).Score; + var match = matcher.IsMatch(jObject).Score; - // Assert + // Assert match.Should().Be(1.0); } + + [Fact] + public void JsonMatcher_IsMatch_WithRegexTrue_ShouldMatch() + { + // Assign + var matcher = new JsonMatcher(new { Id = "^\\d+$", Name = "Test" }, regex: true); + + // Act + var jObject = new JObject + { + { "Id", new JValue(42) }, + { "Name", new JValue("Test") } + }; + var score = matcher.IsMatch(jObject).Score; + + // Assert + Assert.Equal(1.0, score); + } + + [Fact] + public void JsonMatcher_IsMatch_WithRegexTrue_Complex_ShouldMatch() + { + // Assign + var matcher = new JsonMatcher(new + { + Complex = new + { + Id = "^\\d+$", + Name = ".*" + } + }, regex: true); + + // Act + var jObject = new JObject + { + { + "Complex", new JObject + { + { "Id", new JValue(42) }, + { "Name", new JValue("Test") } + } + } + }; + var score = matcher.IsMatch(jObject).Score; + + // Assert + Assert.Equal(1.0, score); + } + + [Fact] + public void JsonMatcher_IsMatch_WithRegexTrue_Complex_ShouldNotMatch() + { + // Assign + var matcher = new JsonMatcher(new + { + Complex = new + { + Id = "^\\d+$", + Name = ".*" + } + }, regex: true); + + // Act + var jObject = new JObject + { + { + "Complex", new JObject + { + { "Id", new JValue(42) }, + { "Name", new JValue("Test") }, + { "Other", new JValue("Other") } + } + } + }; + var score = matcher.IsMatch(jObject).Score; + + // Assert + Assert.Equal(MatchScores.Mismatch, score); + } + + [Fact] + public void JsonMatcher_IsMatch_WithRegexTrue_Array_ShouldMatch() + { + // Assign + var matcher = new JsonMatcher(new + { + Array = new[] + { + "^\\d+$", + ".*" + } + }, regex: true); + + // Act + var jObject = new JObject + { + { "Array", new JArray("42", "test") } + }; + var score = matcher.IsMatch(jObject).Score; + + // Assert + Assert.Equal(1.0, score); + } + + [Fact] + public void JsonMatcher_IsMatch_WithRegexTrue_Array_ShouldNotMatch() + { + // Assign + var matcher = new JsonMatcher(new + { + Array = new[] + { + "^\\d+$", + ".*" + } + }, regex: true); + + // Act + var jObject = new JObject + { + { "Array", new JArray("42", "test", "other") } + }; + var score = matcher.IsMatch(jObject).Score; + + // Assert + Assert.Equal(MatchScores.Mismatch, score); + } + + [Fact] + public void JsonMatcher_IsMatch_GuidAndString() + { + // Assign + var id = Guid.NewGuid(); + var idAsString = id.ToString(); + var matcher = new JsonMatcher(new { Id = id }); + + // Act + var jObject = new JObject + { + { "Id", new JValue(idAsString) } + }; + var score = matcher.IsMatch(jObject).Score; + + // Assert + Assert.Equal(1.0, score); + } + + [Fact] + public void JsonMatcher_IsMatch_StringAndGuid() + { + // Assign + var id = Guid.NewGuid(); + var idAsString = id.ToString(); + var matcher = new JsonMatcher(new { Id = idAsString }); + + // Act + var jObject = new JObject + { + { "Id", new JValue(id) } + }; + var score = matcher.IsMatch(jObject).Score; + + // Assert + Assert.Equal(1.0, score); + } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Mapping_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Mapping_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt index ef641b26..2de9ec07 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Mapping_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Mapping_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt @@ -25,7 +25,8 @@ Pattern: { name: stef }, - IgnoreCase: false + IgnoreCase: false, + Regex: false }, ProtoBufMessageType: greet.HelloRequest } diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt index 26ff2648..da0cea1c 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.ToMappingModel_Request_WithBodyAsProtoBuf_ReturnsCorrectModel.verified.txt @@ -42,7 +42,8 @@ message HelloReply { Pattern: { name: stef }, - IgnoreCase: false + IgnoreCase: false, + Regex: false }, ProtoBufMessageType: greet.HelloRequest }