From 95b93e80ce7f37dc6eaae4ec48ade2df448efad4 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Thu, 19 Jan 2017 22:23:10 +0100 Subject: [PATCH] JsonPathMatcher (#6) --- README.md | 56 +++++++++++++++++++ .../Extensions/DictionaryExtensions.cs | 6 ++ src/WireMock/Matchers/JSONPathMatcher.cs | 44 +++++++++++++++ src/WireMock/Matchers/XPathMatcher.cs | 3 + src/WireMock/RequestBodySpec.cs | 2 - src/WireMock/RequestPathSpec.cs | 24 ++------ src/WireMock/RequestVerbSpec.cs | 25 ++------- src/WireMock/project.json | 3 +- test/WireMock.Net.Tests/RequestTests.cs | 30 ++++++++++ 9 files changed, 152 insertions(+), 41 deletions(-) create mode 100644 src/WireMock/Matchers/JSONPathMatcher.cs diff --git a/README.md b/README.md index 6e4db247..af106c41 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,62 @@ server ); ``` +### Request Matching +WireMock supports matching of requests to stubs and verification queries using the following attributes: + +* URL +* HTTP Method +* Query parameters +* Headers +* Request body + + +#### JSON Path +Deems a match if the attribute value is valid JSON and matches the JSON Path expression supplied. +A JSON body will be considered to match a path expression if the expression returns either a non-null single value (string, integer etc.), or a non-empty object or array. + +```csharp +var server = FluentMockServer.Start(); +server + .Given( + Request.WithUrl("/some/thing").UsingGet() + .WithBody(new JsonPathMatcher("$.things[?(@.name == 'RequiredThing')]")); + ) + .RespondWith(Response.WithBody("Hello")); +``` + +``` +// matching +{ "things": { "name": "RequiredThing" } } +{ "things": [ { "name": "RequiredThing" }, { "name": "Wiremock" } ] } +// not matching +{ "price": 15 } +{ "things": { "name": "Wiremock" } } +``` + +#### XPath +Deems a match if the attribute value is valid XML and matches the XPath expression supplied. +An XML document will be considered to match if any elements are returned by the XPath evaluation. +WireMock delegates to [XPath2.Net](https://github.com/StefH/XPath2.Net), therefore it support up to XPath version 2.0. + +```csharp +var server = FluentMockServer.Start(); +server + .Given( + Request.WithUrl("/some/thing").UsingGet() + .WithBody(new XPathMatcher("/todo-list[count(todo-item) = 3]")); + ) + .RespondWith(Response.WithBody("Hello")); +``` + +Will match xml below: +```xml + + abc + def + xyz + +``` ### Response Templating Response headers and bodies can optionally be rendered using [Handlebars.Net](https://github.com/rexm/Handlebars.Net) templates. diff --git a/src/WireMock/Extensions/DictionaryExtensions.cs b/src/WireMock/Extensions/DictionaryExtensions.cs index e95db0c2..16e841fc 100644 --- a/src/WireMock/Extensions/DictionaryExtensions.cs +++ b/src/WireMock/Extensions/DictionaryExtensions.cs @@ -9,6 +9,12 @@ namespace WireMock.Extensions /// public static class DictionaryExtensions { + /// + /// Converts IDictionary to an ExpandObject. + /// + /// + /// The dictionary. + /// public static dynamic ToExpandoObject(this IDictionary dictionary) { dynamic expando = new ExpandoObject(); diff --git a/src/WireMock/Matchers/JSONPathMatcher.cs b/src/WireMock/Matchers/JSONPathMatcher.cs new file mode 100644 index 00000000..e2c6c306 --- /dev/null +++ b/src/WireMock/Matchers/JSONPathMatcher.cs @@ -0,0 +1,44 @@ +using JetBrains.Annotations; +using Newtonsoft.Json.Linq; +using WireMock.Validation; + +namespace WireMock.Matchers +{ + /// + /// JSONPathMatcher + /// + /// + public class JsonPathMatcher : IMatcher + { + private readonly string _pattern; + + /// + /// Initializes a new instance of the class. + /// + /// The pattern. + public JsonPathMatcher([NotNull] string pattern) + { + Check.NotNull(pattern, nameof(pattern)); + + _pattern = pattern; + } + + /// + /// Determines whether the specified input is match. + /// + /// The input. + /// + /// true if the specified input is match; otherwise, false. + /// + public bool IsMatch(string input) + { + if (input == null) + return false; + + JObject o = JObject.Parse(input); + JToken token = o.SelectToken(_pattern); + + return token != null; + } + } +} \ No newline at end of file diff --git a/src/WireMock/Matchers/XPathMatcher.cs b/src/WireMock/Matchers/XPathMatcher.cs index 1334b74d..992b3fea 100644 --- a/src/WireMock/Matchers/XPathMatcher.cs +++ b/src/WireMock/Matchers/XPathMatcher.cs @@ -33,6 +33,9 @@ namespace WireMock.Matchers /// public bool IsMatch(string input) { + if (input == null) + return false; + var nav = new XmlDocument { InnerXml = input }.CreateNavigator(); object result = nav.XPath2Evaluate($"boolean({_pattern})"); diff --git a/src/WireMock/RequestBodySpec.cs b/src/WireMock/RequestBodySpec.cs index 117748ff..55286060 100644 --- a/src/WireMock/RequestBodySpec.cs +++ b/src/WireMock/RequestBodySpec.cs @@ -1,6 +1,4 @@ using System; -using System.Diagnostics.CodeAnalysis; -using System.Text.RegularExpressions; using JetBrains.Annotations; using WireMock.Matchers; using WireMock.Validation; diff --git a/src/WireMock/RequestPathSpec.cs b/src/WireMock/RequestPathSpec.cs index 6a83848e..85cb3b13 100644 --- a/src/WireMock/RequestPathSpec.cs +++ b/src/WireMock/RequestPathSpec.cs @@ -1,22 +1,8 @@ using System; -using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using JetBrains.Annotations; using WireMock.Validation; -[module: - SuppressMessage("StyleCop.CSharp.ReadabilityRules", - "SA1101:PrefixLocalCallsWithThis", - Justification = "Reviewed. Suppression is OK here, as it conflicts with internal naming rules.")] -[module: - SuppressMessage("StyleCop.CSharp.NamingRules", - "SA1309:FieldNamesMustNotBeginWithUnderscore", - Justification = "Reviewed. Suppression is OK here, as it conflicts with internal naming rules.")] -[module: - SuppressMessage("StyleCop.CSharp.DocumentationRules", - "SA1633:FileMustHaveHeader", - Justification = "Reviewed. Suppression is OK here, as unknown copyright and company.")] - namespace WireMock { /// @@ -27,12 +13,12 @@ namespace WireMock /// /// The pathRegex. /// - private readonly Regex pathRegex; + private readonly Regex _pathRegex; /// /// The url function /// - private readonly Func pathFunc; + private readonly Func _pathFunc; /// /// Initializes a new instance of the class. @@ -43,7 +29,7 @@ namespace WireMock public RequestPathSpec([NotNull, RegexPattern] string path) { Check.NotNull(path, nameof(path)); - pathRegex = new Regex(path); + _pathRegex = new Regex(path); } /// @@ -55,7 +41,7 @@ namespace WireMock public RequestPathSpec([NotNull] Func func) { Check.NotNull(func, nameof(func)); - pathFunc = func; + _pathFunc = func; } /// @@ -69,7 +55,7 @@ namespace WireMock /// public bool IsSatisfiedBy(RequestMessage requestMessage) { - return pathRegex?.IsMatch(requestMessage.Path) ?? pathFunc(requestMessage.Path); + return _pathRegex?.IsMatch(requestMessage.Path) ?? _pathFunc(requestMessage.Path); } } } \ No newline at end of file diff --git a/src/WireMock/RequestVerbSpec.cs b/src/WireMock/RequestVerbSpec.cs index 15dd43e5..e1aec14c 100644 --- a/src/WireMock/RequestVerbSpec.cs +++ b/src/WireMock/RequestVerbSpec.cs @@ -1,20 +1,6 @@ -using System.Diagnostics.CodeAnalysis; -using JetBrains.Annotations; +using JetBrains.Annotations; +using WireMock.Validation; -[module: - SuppressMessage("StyleCop.CSharp.ReadabilityRules", - "SA1101:PrefixLocalCallsWithThis", - Justification = "Reviewed. Suppression is OK here, as it conflicts with internal naming rules.")] -[module: - SuppressMessage("StyleCop.CSharp.NamingRules", - "SA1309:FieldNamesMustNotBeginWithUnderscore", - Justification = "Reviewed. Suppression is OK here, as it conflicts with internal naming rules.")] -[module: - SuppressMessage("StyleCop.CSharp.DocumentationRules", - "SA1633:FileMustHaveHeader", - Justification = "Reviewed. Suppression is OK here, as unknown copyright and company.")] -// ReSharper disable ArrangeThisQualifier -// ReSharper disable InconsistentNaming namespace WireMock { /// @@ -33,8 +19,9 @@ namespace WireMock /// /// The verb. /// - public RequestVerbSpec(string verb) + public RequestVerbSpec([NotNull] string verb) { + Check.NotNull(verb, nameof(verb)); _verb = verb.ToLower(); } @@ -47,9 +34,9 @@ namespace WireMock /// /// The . /// - public bool IsSatisfiedBy([NotNull] RequestMessage requestMessage) + public bool IsSatisfiedBy(RequestMessage requestMessage) { return requestMessage.Verb == _verb; } } -} +} \ No newline at end of file diff --git a/src/WireMock/project.json b/src/WireMock/project.json index 385fac96..7c85d66a 100644 --- a/src/WireMock/project.json +++ b/src/WireMock/project.json @@ -26,13 +26,14 @@ "version": "10.2.1", "type": "build" }, + "Handlebars.Net": "1.8.0", + "Newtonsoft.Json": "9.0.1", "XPath2": "1.0.3.1" }, "frameworks": { "net45": { "dependencies": { - "Handlebars.Net": "1.8.0" }, "frameworkAssemblies": { } diff --git a/test/WireMock.Net.Tests/RequestTests.cs b/test/WireMock.Net.Tests/RequestTests.cs index e07799c4..ed361f97 100644 --- a/test/WireMock.Net.Tests/RequestTests.cs +++ b/test/WireMock.Net.Tests/RequestTests.cs @@ -321,6 +321,36 @@ namespace WireMock.Net.Tests Check.That(spec.IsSatisfiedBy(request)).IsFalse(); } + [Test] + public void Should_specify_requests_matching_given_body_as_jsonpathmatcher_true() + { + // given + var spec = Request.WithUrl("/foo").UsingAnyVerb().WithBody(new JsonPathMatcher("$.things[?(@.name == 'RequiredThing')]")); + + // when + string bodyAsString = "{ \"things\": [ { \"name\": \"RequiredThing\" }, { \"name\": \"Wiremock\" } ] }"; + byte[] body = Encoding.UTF8.GetBytes(bodyAsString); + var request = new RequestMessage(new Uri("http://localhost/foo"), "PUT", body, bodyAsString); + + // then + Check.That(spec.IsSatisfiedBy(request)).IsTrue(); + } + + [Test] + public void Should_specify_requests_matching_given_body_as_jsonpathmatcher_false() + { + // given + var spec = Request.WithUrl("/foo").UsingAnyVerb().WithBody(new JsonPathMatcher("$.things[?(@.name == 'RequiredThing')]")); + + // when + string bodyAsString = "{ \"things\": { \"name\": \"Wiremock\" } }"; + byte[] body = Encoding.UTF8.GetBytes(bodyAsString); + var request = new RequestMessage(new Uri("http://localhost/foo"), "PUT", body, bodyAsString); + + // then + Check.That(spec.IsSatisfiedBy(request)).IsFalse(); + } + [Test] public void Should_exclude_requests_not_matching_given_body() {