diff --git a/src/WireMock.Net/Matchers/JsonMatcher.cs b/src/WireMock.Net/Matchers/JsonMatcher.cs index 85cfe6c0..e37d7fbc 100644 --- a/src/WireMock.Net/Matchers/JsonMatcher.cs +++ b/src/WireMock.Net/Matchers/JsonMatcher.cs @@ -1,4 +1,5 @@ -using System.Collections; +using System; +using System.Collections; using System.Linq; using JetBrains.Annotations; using Newtonsoft.Json; @@ -17,7 +18,7 @@ namespace WireMock.Matchers public object Value { get; } /// - public string Name => "JsonMatcher"; + public virtual string Name => "JsonMatcher"; /// public MatchBehaviour MatchBehaviour { get; } @@ -29,6 +30,7 @@ namespace WireMock.Matchers public bool ThrowException { get; } private readonly JToken _valueAsJToken; + private readonly Func _jTokenConverter; /// /// Initializes a new instance of the class. @@ -67,6 +69,9 @@ namespace WireMock.Matchers Value = value; _valueAsJToken = ConvertValueToJToken(value); + _jTokenConverter = ignoreCase + ? (Func)Rename + : jToken => jToken; } /// @@ -81,7 +86,9 @@ namespace WireMock.Matchers { var inputAsJToken = ConvertValueToJToken(input); - match = DeepEquals(_valueAsJToken, inputAsJToken); + match = IsMatch( + _jTokenConverter(_valueAsJToken), + _jTokenConverter(inputAsJToken)); } catch (JsonException) { @@ -95,6 +102,17 @@ namespace WireMock.Matchers return MatchBehaviourHelper.Convert(MatchBehaviour, MatchScores.ToScore(match)); } + /// + /// Compares the input against the matcher value + /// + /// Matcher value + /// Input value + /// + protected virtual bool IsMatch(JToken value, JToken input) + { + return JToken.DeepEquals(value, input); + } + private static JToken ConvertValueToJToken(object value) { // Check if JToken, string, IEnumerable or object @@ -114,19 +132,6 @@ namespace WireMock.Matchers } } - private bool DeepEquals(JToken value, JToken input) - { - if (!IgnoreCase) - { - return JToken.DeepEquals(value, input); - } - - JToken renamedValue = Rename(value); - JToken renamedInput = Rename(input); - - return JToken.DeepEquals(renamedValue, renamedInput); - } - private static string ToUpper(string input) { return input?.ToUpperInvariant(); diff --git a/src/WireMock.Net/Matchers/JsonPartialMatcher.cs b/src/WireMock.Net/Matchers/JsonPartialMatcher.cs new file mode 100644 index 00000000..54b04cb5 --- /dev/null +++ b/src/WireMock.Net/Matchers/JsonPartialMatcher.cs @@ -0,0 +1,87 @@ +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; + +namespace WireMock.Matchers +{ + /// + /// JsonPartialMatcher + /// + public class JsonPartialMatcher : JsonMatcher + { + /// + public override string Name => "JsonPartialMatcher"; + + /// + /// Initializes a new instance of the class. + /// + /// The string value to check for equality. + /// Ignore the case from the PropertyName and PropertyValue (string only). + /// Throw an exception when the internal matching fails because of invalid input. + public JsonPartialMatcher([NotNull] string value, bool ignoreCase = false, bool throwException = false) + : base(value, ignoreCase, throwException) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The object value to check for equality. + /// Ignore the case from the PropertyName and PropertyValue (string only). + /// Throw an exception when the internal matching fails because of invalid input. + public JsonPartialMatcher([NotNull] object value, bool ignoreCase = false, bool throwException = false) + : base(value, ignoreCase, throwException) + { + } + + /// + /// 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). + /// Throw an exception when the internal matching fails because of invalid input. + public JsonPartialMatcher(MatchBehaviour matchBehaviour, [NotNull] object value, bool ignoreCase = false, bool throwException = false) + : base(matchBehaviour, value, ignoreCase, throwException) + { + } + + /// + protected override bool IsMatch(JToken value, JToken input) + { + if (value == null || value == input) + { + return true; + } + + if (input == null || value.Type != input.Type) + { + return false; + } + + switch (value.Type) + { + case JTokenType.Object: + var nestedValues = value.ToObject>(); + return nestedValues?.Any() != true || + nestedValues.All(pair => IsMatch(pair.Value, input.SelectToken(pair.Key))); + + case JTokenType.Array: + var valuesArray = value.ToObject(); + var tokenArray = input.ToObject(); + + if (valuesArray?.Any() != true) + { + return true; + } + + return tokenArray?.Any() == true && + valuesArray.All(subFilter => tokenArray.Any(subToken => IsMatch(subFilter, subToken))); + + default: + return value.ToString() == input.ToString(); + } + } + } +} diff --git a/src/WireMock.Net/Serialization/MatcherMapper.cs b/src/WireMock.Net/Serialization/MatcherMapper.cs index 9a0c9bcb..3021401c 100644 --- a/src/WireMock.Net/Serialization/MatcherMapper.cs +++ b/src/WireMock.Net/Serialization/MatcherMapper.cs @@ -67,6 +67,10 @@ namespace WireMock.Serialization object value = matcher.Pattern ?? matcher.Patterns; return new JsonMatcher(matchBehaviour, value, ignoreCase, throwExceptionWhenMatcherFails); + case "JsonPartialMatcher": + object matcherValue = matcher.Pattern ?? matcher.Patterns; + return new JsonPartialMatcher(matchBehaviour, matcherValue, ignoreCase, throwExceptionWhenMatcherFails); + case "JsonPathMatcher": return new JsonPathMatcher(matchBehaviour, throwExceptionWhenMatcherFails, stringPatterns); diff --git a/test/WireMock.Net.Tests/Matchers/JsonPartialMatcherTests.cs b/test/WireMock.Net.Tests/Matchers/JsonPartialMatcherTests.cs new file mode 100644 index 00000000..0e274035 --- /dev/null +++ b/test/WireMock.Net.Tests/Matchers/JsonPartialMatcherTests.cs @@ -0,0 +1,385 @@ +using System; +using System.Collections.Generic; +using System.IO; +using FluentAssertions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using NFluent; +using WireMock.Matchers; +using Xunit; + +namespace WireMock.Net.Tests.Matchers +{ + public class JsonPartialMatcherTests + { + [Fact] + public void JsonPartialMatcher_GetName() + { + // Assign + var matcher = new JsonPartialMatcher("{}"); + + // Act + string name = matcher.Name; + + // Assert + Check.That(name).Equals("JsonPartialMatcher"); + } + + [Fact] + public void JsonPartialMatcher_GetValue() + { + // Assign + var matcher = new JsonPartialMatcher("{}"); + + // Act + object value = matcher.Value; + + // Assert + Check.That(value).Equals("{}"); + } + + [Fact] + public void JsonPartialMatcher_WithInvalidStringValue_Should_ThrowException() + { + // Act + Action action = () => new JsonPartialMatcher(MatchBehaviour.AcceptOnMatch, "{ \"Id\""); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void JsonPartialMatcher_WithInvalidObjectValue_Should_ThrowException() + { + // Act + Action action = () => new JsonPartialMatcher(MatchBehaviour.AcceptOnMatch, new MemoryStream()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_WithInvalidValue_And_ThrowExceptionIsFalse_Should_ReturnMismatch() + { + // Assign + var matcher = new JsonPartialMatcher(""); + + // Act + double match = matcher.IsMatch(new MemoryStream()); + + // Assert + Check.That(match).IsEqualTo(0); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_WithInvalidValue_And_ThrowExceptionIsTrue_Should_ReturnMismatch() + { + // Assign + var matcher = new JsonPartialMatcher("", false, true); + + // Act + Action action = () => matcher.IsMatch(new MemoryStream()); + + // Assert + action.Should().Throw(); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_ByteArray() + { + // Assign + var bytes = new byte[0]; + var matcher = new JsonPartialMatcher(""); + + // Act + double match = matcher.IsMatch(bytes); + + // Assert + Check.That(match).IsEqualTo(0); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_NullString() + { + // Assign + string s = null; + var matcher = new JsonPartialMatcher(""); + + // Act + double match = matcher.IsMatch(s); + + // Assert + Check.That(match).IsEqualTo(0); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_NullObject() + { + // Assign + object o = null; + var matcher = new JsonPartialMatcher(""); + + // Act + double match = matcher.IsMatch(o); + + // Assert + Check.That(match).IsEqualTo(0); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_JArray() + { + // Assign + var matcher = new JsonPartialMatcher(new[] { "x", "y" }); + + // Act + var jArray = new JArray + { + "x", + "y" + }; + double match = matcher.IsMatch(jArray); + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_JObject() + { + // Assign + var matcher = new JsonPartialMatcher(new { Id = 1, Name = "Test" }); + + // Act + var jobject = new JObject + { + { "Id", new JValue(1) }, + { "Name", new JValue("Test") } + }; + double match = matcher.IsMatch(jobject); + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JObject() + { + // Assign + var matcher = new JsonPartialMatcher(new { id = 1, Name = "test" }, true); + + // Act + var jobject = new JObject + { + { "Id", new JValue(1) }, + { "NaMe", new JValue("Test") } + }; + double match = matcher.IsMatch(jobject); + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_JObjectParsed() + { + // Assign + var matcher = new JsonPartialMatcher(new { Id = 1, Name = "Test" }); + + // Act + var jobject = JObject.Parse("{ \"Id\" : 1, \"Name\" : \"Test\" }"); + double match = matcher.IsMatch(jobject); + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JObjectParsed() + { + // Assign + var matcher = new JsonPartialMatcher(new { Id = 1, Name = "TESt" }, true); + + // Act + var jobject = JObject.Parse("{ \"Id\" : 1, \"Name\" : \"Test\" }"); + double match = matcher.IsMatch(jobject); + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_JArrayAsString() + { + // Assign + var matcher = new JsonPartialMatcher("[ \"x\", \"y\" ]"); + + // Act + var jArray = new JArray + { + "x", + "y" + }; + double match = matcher.IsMatch(jArray); + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_JObjectAsString() + { + // Assign + var matcher = new JsonPartialMatcher("{ \"Id\" : 1, \"Name\" : \"Test\" }"); + + // Act + var jobject = new JObject + { + { "Id", new JValue(1) }, + { "Name", new JValue("Test") } + }; + double match = matcher.IsMatch(jobject); + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_WithIgnoreCaseTrue_JObjectAsString() + { + // Assign + var matcher = new JsonPartialMatcher("{ \"Id\" : 1, \"Name\" : \"test\" }", true); + + // Act + var jobject = new JObject + { + { "Id", new JValue(1) }, + { "Name", new JValue("Test") } + }; + double match = matcher.IsMatch(jobject); + + // Assert + Assert.Equal(1.0, match); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_JObjectAsString_RejectOnMatch() + { + // Assign + var matcher = new JsonPartialMatcher(MatchBehaviour.RejectOnMatch, "{ \"Id\" : 1, \"Name\" : \"Test\" }"); + + // Act + var jobject = new JObject + { + { "Id", new JValue(1) }, + { "Name", new JValue("Test") } + }; + double match = matcher.IsMatch(jobject); + + // Assert + Assert.Equal(0.0, match); + } + + [Fact] + public void JsonPartialMatcher_IsMatch_JObjectWithDateTimeOffsetAsString() + { + // Assign + var matcher = new JsonPartialMatcher("{ \"preferredAt\" : \"2019-11-21T10:32:53.2210009+00:00\" }"); + + // Act + var jobject = new JObject + { + { "preferredAt", new JValue("2019-11-21T10:32:53.2210009+00:00") } + }; + double match = matcher.IsMatch(jobject); + + // 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 JsonPartialMatcher_IsMatch_StringInputValidMatch(string value, string input) + { + // Assign + var matcher = new JsonPartialMatcher(value); + + // Act + double match = matcher.IsMatch(input); + + // 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 JsonPartialMatcher_IsMatch_StringInputWithInvalidMatch(string value, string input) + { + // Assign + var matcher = new JsonPartialMatcher(value); + + // Act + double match = matcher.IsMatch(input); + + // 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 JsonPartialMatcher_IsMatch_ValueAsJPathValidMatch(string value, string input) + { + // Assign + var matcher = new JsonPartialMatcher(value); + + // Act + double match = matcher.IsMatch(input); + + // 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 JsonPartialMatcher_IsMatch_ValueAsJPathInvalidMatch(string value, string input) + { + // Assign + var matcher = new JsonPartialMatcher(value); + + // Act + double match = matcher.IsMatch(input); + + // Assert + Assert.Equal(0.0, match); + } + } +} diff --git a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs index 14dd0554..d1d9f049 100644 --- a/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MatcherMapperTests.cs @@ -221,5 +221,85 @@ namespace WireMock.Net.Tests.Serialization matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); matcher.Value.Should().BeEquivalentTo(patterns); } + + [Fact] + public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Pattern_As_String() + { + // Assign + var pattern = "{ \"AccountIds\": [ 1, 2, 3 ] }"; + var model = new MatcherModel + { + Name = "JsonPartialMatcher", + Pattern = pattern + }; + + // Act + var matcher = (JsonPartialMatcher)_sut.Map(model); + + // Assert + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Value.Should().BeEquivalentTo(pattern); + } + + [Fact] + public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Patterns_As_String() + { + // Assign + var pattern1 = "{ \"AccountIds\": [ 1, 2, 3 ] }"; + var pattern2 = "{ \"X\": \"x\" }"; + var patterns = new[] { pattern1, pattern2 }; + var model = new MatcherModel + { + Name = "JsonPartialMatcher", + Pattern = patterns + }; + + // Act + var matcher = (JsonPartialMatcher)_sut.Map(model); + + // Assert + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Value.Should().BeEquivalentTo(patterns); + } + + [Fact] + public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Pattern_As_Object() + { + // Assign + var pattern = new { AccountIds = new[] { 1, 2, 3 } }; + var model = new MatcherModel + { + Name = "JsonPartialMatcher", + Pattern = pattern + }; + + // Act + var matcher = (JsonPartialMatcher)_sut.Map(model); + + // Assert + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Value.Should().BeEquivalentTo(pattern); + } + + [Fact] + public void MatcherMapper_Map_MatcherModel_JsonPartialMatcher_Patterns_As_Object() + { + // Assign + object pattern1 = new { AccountIds = new[] { 1, 2, 3 } }; + object pattern2 = new { X = "x" }; + var patterns = new[] { pattern1, pattern2 }; + var model = new MatcherModel + { + Name = "JsonPartialMatcher", + Patterns = patterns + }; + + // Act + var matcher = (JsonMatcher)_sut.Map(model); + + // Assert + matcher.MatchBehaviour.Should().Be(MatchBehaviour.AcceptOnMatch); + matcher.Value.Should().BeEquivalentTo(patterns); + } } -} \ No newline at end of file +}