Added support of custom matchers in static mappings (#713)

* Added support of custom matchers in static mappings

* Fixed code style issues

* Fixed naming and code style

* added empty line

* Ignore serialization of CustomMatcherMappings property in WireMockServerSettings

* Added integration tests for CustomMatcherMappings
This commit is contained in:
Levan Nozadze
2022-01-17 19:04:37 +04:00
committed by GitHub
parent 60bdc06d29
commit 6b393ebc1d
7 changed files with 321 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using AnyOfTypes;
using Newtonsoft.Json;
using WireMock.Matchers;
using WireMock.Models;
namespace WireMock.Net.Tests.Serialization
{
/// <summary>
/// This matcher is only for unit test purposes
/// </summary>
public class CustomPathParamMatcher : IStringMatcher
{
public string Name => nameof(CustomPathParamMatcher);
public MatchBehaviour MatchBehaviour { get; }
public bool ThrowException { get; }
private readonly string _path;
private readonly string[] _pathParts;
private readonly Dictionary<string, string> _pathParams;
public CustomPathParamMatcher(string path, Dictionary<string, string> pathParams) : this(MatchBehaviour.AcceptOnMatch, path, pathParams)
{
}
public CustomPathParamMatcher(MatchBehaviour matchBehaviour, string path, Dictionary<string, string> pathParams, bool throwException = false)
{
MatchBehaviour = matchBehaviour;
ThrowException = throwException;
_path = path;
_pathParts = GetPathParts(path);
_pathParams = pathParams.ToDictionary(x => x.Key, x => x.Value, StringComparer.OrdinalIgnoreCase);
}
public double IsMatch(string input)
{
var inputParts = GetPathParts(input);
if (inputParts.Length != _pathParts.Length)
{
return MatchScores.Mismatch;
}
try
{
for (int i = 0; i < inputParts.Length; i++)
{
var inputPart = inputParts[i];
var pathPart = _pathParts[i];
if (pathPart.StartsWith("{") && pathPart.EndsWith("}"))
{
var pathParamName = pathPart.Trim('{').Trim('}');
if (!_pathParams.ContainsKey(pathParamName))
{
return MatchScores.Mismatch;
}
if (!Regex.IsMatch(inputPart, _pathParams[pathParamName], RegexOptions.IgnoreCase))
{
return MatchScores.Mismatch;
}
}
else
{
if (!inputPart.Equals(pathPart, StringComparison.InvariantCultureIgnoreCase))
{
return MatchScores.Mismatch;
}
}
}
}
catch
{
if (ThrowException)
{
throw;
}
return MatchScores.Mismatch;
}
return MatchScores.Perfect;
}
public AnyOf<string, StringPattern>[] GetPatterns()
{
return new[] { new AnyOf<string, StringPattern>(JsonConvert.SerializeObject(new CustomPathParamMatcherModel(_path, _pathParams))) };
}
private string[] GetPathParts(string path)
{
var hashMarkIndex = path.IndexOf('#');
if (hashMarkIndex != -1)
{
path = path.Substring(0, hashMarkIndex);
}
var queryParamsIndex = path.IndexOf('?');
if (queryParamsIndex != -1)
{
path = path.Substring(0, queryParamsIndex);
}
return path.Trim().Trim('/').ToLower().Split('/');
}
}
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace WireMock.Net.Tests.Serialization
{
public class CustomPathParamMatcherModel
{
public string Path { get; set; }
public Dictionary<string, string> PathParams { get; set; }
public CustomPathParamMatcherModel()
{
}
public CustomPathParamMatcherModel(string path, Dictionary<string, string> pathParams)
{
Path = path;
PathParams = pathParams;
}
}
}

View File

@@ -1,7 +1,10 @@
using System;
using System.Collections.Generic;
using AnyOfTypes;
using FluentAssertions;
using FluentAssertions.Execution;
using Moq;
using Newtonsoft.Json;
using NFluent;
using WireMock.Admin.Mappings;
using WireMock.Handlers;
@@ -310,5 +313,71 @@ namespace WireMock.Net.Tests.Serialization
// Act
Check.ThatCode(() => _sut.Map(model)).Throws<NotSupportedException>();
}
[Fact]
public void MatcherModelMapper_Map_MatcherModelToCustomMatcher()
{
// Arrange
var patternModel = new CustomPathParamMatcherModel("/customer/{customerId}/document/{documentId}",
new Dictionary<string, string>(2)
{
{ "customerId", @"^[0-9]+$" },
{ "documentId", @"^[0-9a-zA-Z\-\_]+\.[a-zA-Z]+$" }
});
var model = new MatcherModel
{
Name = nameof(CustomPathParamMatcher),
Pattern = JsonConvert.SerializeObject(patternModel)
};
var settings = new WireMockServerSettings();
settings.CustomMatcherMappings = settings.CustomMatcherMappings ?? new Dictionary<string, Func<MatcherModel, IMatcher>>();
settings.CustomMatcherMappings[nameof(CustomPathParamMatcher)] = matcherModel =>
{
var matcherParams = JsonConvert.DeserializeObject<CustomPathParamMatcherModel>((string)matcherModel.Pattern);
return new CustomPathParamMatcher(
matcherModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch,
matcherParams.Path, matcherParams.PathParams,
settings.ThrowExceptionWhenMatcherFails == true
);
};
var sut = new MatcherMapper(settings);
// Act
var matcher = sut.Map(model) as CustomPathParamMatcher;
// Assert
matcher.Should().NotBeNull();
}
[Fact]
public void MatcherModelMapper_Map_CustomMatcherToMatcherModel()
{
// Arrange
var matcher = new CustomPathParamMatcher("/customer/{customerId}/document/{documentId}",
new Dictionary<string, string>(2)
{
{ "customerId", @"^[0-9]+$" },
{ "documentId", @"^[0-9a-zA-Z\-\_]+\.[a-zA-Z]+$" }
});
// Act
var model = _sut.Map(matcher);
// Assert
using (new AssertionScope())
{
model.Should().NotBeNull();
model.Name.Should().Be(nameof(CustomPathParamMatcher));
var matcherParams = JsonConvert.DeserializeObject<CustomPathParamMatcherModel>((string)model.Pattern);
matcherParams.Path.Should().Be("/customer/{customerId}/document/{documentId}");
matcherParams.PathParams.Should().BeEquivalentTo(new Dictionary<string, string>(2)
{
{ "customerId", @"^[0-9]+$" },
{ "documentId", @"^[0-9a-zA-Z\-\_]+\.[a-zA-Z]+$" }
});
}
}
}
}

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Net;
@@ -7,10 +8,15 @@ using System.Net.Http.Headers;
using System.Text;
using System.Threading.Tasks;
using FluentAssertions;
using Newtonsoft.Json;
using NFluent;
using WireMock.Admin.Mappings;
using WireMock.Matchers;
using WireMock.Net.Tests.Serialization;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Server;
using WireMock.Settings;
using WireMock.Util;
using Xunit;
@@ -383,6 +389,102 @@ namespace WireMock.Net.Tests
server.Stop();
}
[Fact]
public async Task WireMockServer_Using_JsonMapping_And_CustomMatcher_WithCorrectParams_ShouldMatch()
{
// Arrange
var settings = new WireMockServerSettings();
settings.WatchStaticMappings = true;
settings.WatchStaticMappingsInSubdirectories = true;
settings.CustomMatcherMappings = new Dictionary<string, Func<MatcherModel, IMatcher>>();
settings.CustomMatcherMappings[nameof(CustomPathParamMatcher)] = matcherModel =>
{
var matcherParams = JsonConvert.DeserializeObject<CustomPathParamMatcherModel>((string)matcherModel.Pattern);
return new CustomPathParamMatcher(
matcherModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch,
matcherParams.Path, matcherParams.PathParams,
settings.ThrowExceptionWhenMatcherFails == true
);
};
var server = WireMockServer.Start(settings);
server.WithMapping(@"{
""Request"": {
""Path"": {
""Matchers"": [
{
""Name"": ""CustomPathParamMatcher"",
""Pattern"": ""{\""path\"":\""/customer/{customerId}/document/{documentId}\"",\""pathParams\"":{\""customerId\"":\""^[0-9]+$\"",\""documentId\"":\""^[0-9a-zA-Z\\\\-_]+\\\\.[a-zA-Z]+$\""}}""
}
]
}
},
""Response"": {
""StatusCode"": 200,
""Headers"": {
""Content-Type"": ""application/json""
},
""Body"": ""OK""
}
}");
// Act
var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/customer/132/document/pic.jpg", new StringContent("{ Hi = \"Hello World\" }")).ConfigureAwait(false);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.OK);
server.Stop();
}
[Fact]
public async Task WireMockServer_Using_JsonMapping_And_CustomMatcher_WithIncorrectParams_ShouldNotMatch()
{
// Arrange
var settings = new WireMockServerSettings();
settings.WatchStaticMappings = true;
settings.WatchStaticMappingsInSubdirectories = true;
settings.CustomMatcherMappings = new Dictionary<string, Func<MatcherModel, IMatcher>>();
settings.CustomMatcherMappings[nameof(CustomPathParamMatcher)] = matcherModel =>
{
var matcherParams = JsonConvert.DeserializeObject<CustomPathParamMatcherModel>((string)matcherModel.Pattern);
return new CustomPathParamMatcher(
matcherModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch,
matcherParams.Path, matcherParams.PathParams,
settings.ThrowExceptionWhenMatcherFails == true
);
};
var server = WireMockServer.Start(settings);
server.WithMapping(@"{
""Request"": {
""Path"": {
""Matchers"": [
{
""Name"": ""CustomPathParamMatcher"",
""Pattern"": ""{\""path\"":\""/customer/{customerId}/document/{documentId}\"",\""pathParams\"":{\""customerId\"":\""^[0-9]+$\"",\""documentId\"":\""^[0-9a-zA-Z\\\\-_]+\\\\.[a-zA-Z]+$\""}}""
}
]
}
},
""Response"": {
""StatusCode"": 200,
""Headers"": {
""Content-Type"": ""application/json""
},
""Body"": ""OK""
}
}");
// Act
var response = await new HttpClient().PostAsync("http://localhost:" + server.Ports[0] + "/customer/132/document/pic", new StringContent("{ Hi = \"Hello World\" }")).ConfigureAwait(false);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.NotFound);
server.Stop();
}
#endif
}
}