JsonPathMatcher (#6)

This commit is contained in:
Stef Heyenrath
2017-01-19 22:23:10 +01:00
parent 72335d48d6
commit 95b93e80ce
9 changed files with 152 additions and 41 deletions

View File

@@ -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
<todo-list>
<todo-item id='a1'>abc</todo-item>
<todo-item id='a2'>def</todo-item>
<todo-item id='a3'>xyz</todo-item>
</todo-list>
```
### Response Templating
Response headers and bodies can optionally be rendered using [Handlebars.Net](https://github.com/rexm/Handlebars.Net) templates.

View File

@@ -9,6 +9,12 @@ namespace WireMock.Extensions
/// </summary>
public static class DictionaryExtensions
{
/// <summary>
/// Converts IDictionary to an ExpandObject.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="dictionary">The dictionary.</param>
/// <returns></returns>
public static dynamic ToExpandoObject<T>(this IDictionary<string, T> dictionary)
{
dynamic expando = new ExpandoObject();

View File

@@ -0,0 +1,44 @@
using JetBrains.Annotations;
using Newtonsoft.Json.Linq;
using WireMock.Validation;
namespace WireMock.Matchers
{
/// <summary>
/// JSONPathMatcher
/// </summary>
/// <seealso cref="WireMock.Matchers.IMatcher" />
public class JsonPathMatcher : IMatcher
{
private readonly string _pattern;
/// <summary>
/// Initializes a new instance of the <see cref="JsonPathMatcher"/> class.
/// </summary>
/// <param name="pattern">The pattern.</param>
public JsonPathMatcher([NotNull] string pattern)
{
Check.NotNull(pattern, nameof(pattern));
_pattern = pattern;
}
/// <summary>
/// Determines whether the specified input is match.
/// </summary>
/// <param name="input">The input.</param>
/// <returns>
/// <c>true</c> if the specified input is match; otherwise, <c>false</c>.
/// </returns>
public bool IsMatch(string input)
{
if (input == null)
return false;
JObject o = JObject.Parse(input);
JToken token = o.SelectToken(_pattern);
return token != null;
}
}
}

View File

@@ -33,6 +33,9 @@ namespace WireMock.Matchers
/// </returns>
public bool IsMatch(string input)
{
if (input == null)
return false;
var nav = new XmlDocument { InnerXml = input }.CreateNavigator();
object result = nav.XPath2Evaluate($"boolean({_pattern})");

View File

@@ -1,6 +1,4 @@
using System;
using System.Diagnostics.CodeAnalysis;
using System.Text.RegularExpressions;
using JetBrains.Annotations;
using WireMock.Matchers;
using WireMock.Validation;

View File

@@ -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
{
/// <summary>
@@ -27,12 +13,12 @@ namespace WireMock
/// <summary>
/// The pathRegex.
/// </summary>
private readonly Regex pathRegex;
private readonly Regex _pathRegex;
/// <summary>
/// The url function
/// </summary>
private readonly Func<string, bool> pathFunc;
private readonly Func<string, bool> _pathFunc;
/// <summary>
/// Initializes a new instance of the <see cref="RequestPathSpec"/> 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);
}
/// <summary>
@@ -55,7 +41,7 @@ namespace WireMock
public RequestPathSpec([NotNull] Func<string, bool> func)
{
Check.NotNull(func, nameof(func));
pathFunc = func;
_pathFunc = func;
}
/// <summary>
@@ -69,7 +55,7 @@ namespace WireMock
/// </returns>
public bool IsSatisfiedBy(RequestMessage requestMessage)
{
return pathRegex?.IsMatch(requestMessage.Path) ?? pathFunc(requestMessage.Path);
return _pathRegex?.IsMatch(requestMessage.Path) ?? _pathFunc(requestMessage.Path);
}
}
}

View File

@@ -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
{
/// <summary>
@@ -33,8 +19,9 @@ namespace WireMock
/// <param name="verb">
/// The verb.
/// </param>
public RequestVerbSpec(string verb)
public RequestVerbSpec([NotNull] string verb)
{
Check.NotNull(verb, nameof(verb));
_verb = verb.ToLower();
}
@@ -47,9 +34,9 @@ namespace WireMock
/// <returns>
/// The <see cref="bool"/>.
/// </returns>
public bool IsSatisfiedBy([NotNull] RequestMessage requestMessage)
public bool IsSatisfiedBy(RequestMessage requestMessage)
{
return requestMessage.Verb == _verb;
}
}
}
}

View File

@@ -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": {
}

View File

@@ -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()
{