Add UseDefinedRequestMatchers to ProxyAndRecordSettings (#821)

* .

* UseDefinedRequestMatchers

* ok

* .

* ClientIP

* t

* fix ut

* .

* cf

* cf2
This commit is contained in:
Stef Heyenrath
2022-09-30 11:25:11 +02:00
committed by GitHub
parent c0b18631a3
commit f7b04f3234
20 changed files with 486 additions and 131 deletions

View File

@@ -1,8 +1,7 @@
using Stef.Validation;
using System;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using Stef.Validation;
namespace WireMock.Matchers.Request;
@@ -13,12 +12,13 @@ namespace WireMock.Matchers.Request;
public class RequestMessageCookieMatcher : IRequestMatcher
{
private readonly MatchBehaviour _matchBehaviour;
private readonly bool _ignoreCase;
/// <summary>
/// The functions
/// </summary>
public Func<IDictionary<string, string>, bool>[] Funcs { get; }
public Func<IDictionary<string, string>, bool>[]? Funcs { get; }
/// <summary>
/// The name
@@ -28,7 +28,7 @@ public class RequestMessageCookieMatcher : IRequestMatcher
/// <value>
/// The matchers.
/// </value>
public IStringMatcher[] Matchers { get; }
public IStringMatcher[]? Matchers { get; }
/// <summary>
/// Initializes a new instance of the <see cref="RequestMessageCookieMatcher"/> class.
@@ -37,15 +37,12 @@ public class RequestMessageCookieMatcher : IRequestMatcher
/// <param name="pattern">The pattern.</param>
/// <param name="ignoreCase">Ignore the case from the pattern.</param>
/// <param name="matchBehaviour">The match behaviour.</param>
public RequestMessageCookieMatcher(MatchBehaviour matchBehaviour, [NotNull] string name, [NotNull] string pattern, bool ignoreCase)
public RequestMessageCookieMatcher(MatchBehaviour matchBehaviour, string name, string pattern, bool ignoreCase)
{
Guard.NotNull(name, nameof(name));
Guard.NotNull(pattern, nameof(pattern));
_matchBehaviour = matchBehaviour;
_ignoreCase = ignoreCase;
Name = name;
Matchers = new IStringMatcher[] { new WildcardMatcher(matchBehaviour, pattern, ignoreCase) };
Name = Guard.NotNull(name);
Matchers = new IStringMatcher[] { new WildcardMatcher(matchBehaviour, Guard.NotNull(pattern), ignoreCase) };
}
/// <summary>
@@ -55,10 +52,10 @@ public class RequestMessageCookieMatcher : IRequestMatcher
/// <param name="name">The name.</param>
/// <param name="patterns">The patterns.</param>
/// <param name="ignoreCase">Ignore the case from the pattern.</param>
public RequestMessageCookieMatcher(MatchBehaviour matchBehaviour, [NotNull] string name, bool ignoreCase, [NotNull] params string[] patterns) :
public RequestMessageCookieMatcher(MatchBehaviour matchBehaviour, string name, bool ignoreCase, params string[] patterns) :
this(matchBehaviour, name, ignoreCase, patterns.Select(pattern => new WildcardMatcher(matchBehaviour, pattern, ignoreCase)).Cast<IStringMatcher>().ToArray())
{
Guard.NotNull(patterns, nameof(patterns));
Guard.NotNull(patterns);
}
/// <summary>
@@ -68,14 +65,11 @@ public class RequestMessageCookieMatcher : IRequestMatcher
/// <param name="name">The name.</param>
/// <param name="matchers">The matchers.</param>
/// <param name="ignoreCase">Ignore the case from the pattern.</param>
public RequestMessageCookieMatcher(MatchBehaviour matchBehaviour, [NotNull] string name, bool ignoreCase, [NotNull] params IStringMatcher[] matchers)
public RequestMessageCookieMatcher(MatchBehaviour matchBehaviour, string name, bool ignoreCase, params IStringMatcher[] matchers)
{
Guard.NotNull(name, nameof(name));
Guard.NotNull(matchers, nameof(matchers));
_matchBehaviour = matchBehaviour;
Name = name;
Matchers = matchers;
Name = Guard.NotNull(name);
Matchers = Guard.NotNull(matchers);
_ignoreCase = ignoreCase;
}
@@ -83,11 +77,12 @@ public class RequestMessageCookieMatcher : IRequestMatcher
/// Initializes a new instance of the <see cref="RequestMessageCookieMatcher"/> class.
/// </summary>
/// <param name="funcs">The funcs.</param>
public RequestMessageCookieMatcher([NotNull] params Func<IDictionary<string, string>, bool>[] funcs)
public RequestMessageCookieMatcher(params Func<IDictionary<string, string>, bool>[] funcs)
{
Guard.NotNull(funcs, nameof(funcs));
Guard.NotNull(funcs);
Funcs = funcs;
Name = string.Empty; // Not used when Func, but set to a non-null valid value.
}
/// <inheritdoc />

View File

@@ -11,7 +11,10 @@ namespace WireMock.Matchers.Request;
/// </summary>
public class RequestMessageParamMatcher : IRequestMatcher
{
private readonly MatchBehaviour _matchBehaviour;
/// <summary>
/// MatchBehaviour
/// </summary>
public MatchBehaviour MatchBehaviour { get; }
/// <summary>
/// The funcs
@@ -63,7 +66,7 @@ public class RequestMessageParamMatcher : IRequestMatcher
/// <param name="matchers">The matchers.</param>
public RequestMessageParamMatcher(MatchBehaviour matchBehaviour, string key, bool ignoreCase, IStringMatcher[]? matchers)
{
_matchBehaviour = matchBehaviour;
MatchBehaviour = matchBehaviour;
Key = Guard.NotNull(key);
IgnoreCase = ignoreCase;
Matchers = matchers;
@@ -81,7 +84,7 @@ public class RequestMessageParamMatcher : IRequestMatcher
/// <inheritdoc />
public double GetMatchingScore(IRequestMessage requestMessage, IRequestMatchResult requestMatchResult)
{
double score = MatchBehaviourHelper.Convert(_matchBehaviour, IsMatch(requestMessage));
double score = MatchBehaviourHelper.Convert(MatchBehaviour, IsMatch(requestMessage));
return requestMatchResult.AddScore(GetType(), score);
}

View File

@@ -161,7 +161,6 @@ namespace WireMock.Owin
_options.Logger.Error($"Providing a Response for Mapping '{result.Match?.Mapping?.Guid}' failed. HttpStatusCode set to 500. Exception: {ex}");
response = ResponseMessageBuilder.Create(ex.Message, 500);
}
finally
{
var log = new LogEntry

View File

@@ -1,16 +1,10 @@
using Stef.Validation;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Stef.Validation;
using WireMock.Constants;
using WireMock.Http;
using WireMock.Matchers;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Serialization;
using WireMock.Settings;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Proxy;
@@ -18,13 +12,16 @@ namespace WireMock.Proxy;
internal class ProxyHelper
{
private readonly WireMockServerSettings _settings;
private readonly ProxyMappingConverter _proxyMappingConverter;
public ProxyHelper(WireMockServerSettings settings)
{
_settings = Guard.NotNull(settings);
_proxyMappingConverter = new ProxyMappingConverter(settings, new GuidUtils());
}
public async Task<(IResponseMessage Message, IMapping? Mapping)> SendAsync(
IMapping? mapping,
ProxyAndRecordSettings proxyAndRecordSettings,
HttpClient client,
IRequestMessage requestMessage,
@@ -49,78 +46,13 @@ internal class ProxyHelper
var responseMessage = await HttpResponseMessageHelper.CreateAsync(httpResponseMessage, requiredUri, originalUri, deserializeJson, decompressGzipAndDeflate).ConfigureAwait(false);
IMapping? mapping = null;
IMapping? newMapping = null;
if (HttpStatusRangeParser.IsMatch(proxyAndRecordSettings.SaveMappingForStatusCodePattern, responseMessage.StatusCode) &&
(proxyAndRecordSettings.SaveMapping || proxyAndRecordSettings.SaveMappingToFile))
{
mapping = ToMapping(proxyAndRecordSettings, requestMessage, responseMessage);
newMapping = _proxyMappingConverter.ToMapping(mapping, proxyAndRecordSettings, requestMessage, responseMessage);
}
return (responseMessage, mapping);
}
private IMapping ToMapping(ProxyAndRecordSettings proxyAndRecordSettings, IRequestMessage requestMessage, ResponseMessage responseMessage)
{
var excludedHeaders = proxyAndRecordSettings.ExcludedHeaders ?? new string[] { };
var excludedCookies = proxyAndRecordSettings.ExcludedCookies ?? new string[] { };
var request = Request.Create();
request.WithPath(requestMessage.Path);
request.UsingMethod(requestMessage.Method);
requestMessage.Query?.Loop((key, value) => request.WithParam(key, false, value.ToArray()));
requestMessage.Cookies?.Loop((key, value) =>
{
if (!excludedCookies.Contains(key, StringComparer.OrdinalIgnoreCase))
{
request.WithCookie(key, value);
}
});
var allExcludedHeaders = new List<string>(excludedHeaders) { "Cookie" };
requestMessage.Headers?.Loop((key, value) =>
{
if (!allExcludedHeaders.Contains(key, StringComparer.OrdinalIgnoreCase))
{
request.WithHeader(key, value.ToArray());
}
});
bool throwExceptionWhenMatcherFails = _settings.ThrowExceptionWhenMatcherFails == true;
switch (requestMessage.BodyData?.DetectedBodyType)
{
case BodyType.Json:
request.WithBody(new JsonMatcher(MatchBehaviour.AcceptOnMatch, requestMessage.BodyData.BodyAsJson!, true, throwExceptionWhenMatcherFails));
break;
case BodyType.String:
request.WithBody(new ExactMatcher(MatchBehaviour.AcceptOnMatch, throwExceptionWhenMatcherFails, MatchOperator.Or, requestMessage.BodyData.BodyAsString));
break;
case BodyType.Bytes:
request.WithBody(new ExactObjectMatcher(MatchBehaviour.AcceptOnMatch, requestMessage.BodyData.BodyAsBytes, throwExceptionWhenMatcherFails));
break;
}
var response = Response.Create(responseMessage);
return new Mapping
(
guid: Guid.NewGuid(),
title: $"Proxy Mapping for {requestMessage.Method} {requestMessage.Path}",
description: string.Empty,
path: null,
settings: _settings,
request,
response,
priority: WireMockConstants.ProxyPriority, // This was 0
scenario: null,
executionConditionState: null,
nextState: null,
stateTimes: null,
webhooks: null,
useWebhooksFireAndForget: null,
timeSettings: null
);
return (responseMessage, newMapping);
}
}

View File

@@ -20,7 +20,7 @@ namespace WireMock.RequestBuilders
/// <inheritdoc cref="IParamsRequestBuilder.WithParam(string, bool, MatchBehaviour)"/>
public IRequestBuilder WithParam(string key, bool ignoreCase, MatchBehaviour matchBehaviour = MatchBehaviour.AcceptOnMatch)
{
Guard.NotNull(key, nameof(key));
Guard.NotNull(key);
_requestMatchers.Add(new RequestMessageParamMatcher(matchBehaviour, key, ignoreCase));
return this;

View File

@@ -271,6 +271,7 @@ public partial class Response : IResponseBuilder
var proxyHelper = new ProxyHelper(settings);
return await proxyHelper.SendAsync(
mapping,
ProxyAndRecordSettings,
_httpClientForProxy,
requestMessage,

View File

@@ -20,7 +20,7 @@ internal class MappingConverter
public MappingConverter(MatcherMapper mapper)
{
_mapper = Guard.NotNull(mapper, nameof(mapper));
_mapper = Guard.NotNull(mapper);
}
public MappingModel ToMappingModel(IMapping mapping)

View File

@@ -0,0 +1,181 @@
using Stef.Validation;
using System;
using System.Collections.Generic;
using System.Linq;
using WireMock.Constants;
using WireMock.Matchers;
using WireMock.Matchers.Request;
using WireMock.RequestBuilders;
using WireMock.ResponseBuilders;
using WireMock.Settings;
using WireMock.Types;
using WireMock.Util;
namespace WireMock.Serialization;
internal class ProxyMappingConverter
{
private readonly WireMockServerSettings _settings;
private readonly IGuidUtils _guidUtils;
public ProxyMappingConverter(WireMockServerSettings settings, IGuidUtils guidUtils)
{
_settings = Guard.NotNull(settings);
_guidUtils = Guard.NotNull(guidUtils);
}
public IMapping ToMapping(IMapping? mapping, ProxyAndRecordSettings proxyAndRecordSettings, IRequestMessage requestMessage, ResponseMessage responseMessage)
{
var request = (Request?)mapping?.RequestMatcher;
var clientIPMatcher = request?.GetRequestMessageMatcher<RequestMessageClientIPMatcher>();
var pathMatcher = request?.GetRequestMessageMatcher<RequestMessagePathMatcher>();
var headerMatchers = request?.GetRequestMessageMatchers<RequestMessageHeaderMatcher>();
var cookieMatchers = request?.GetRequestMessageMatchers<RequestMessageCookieMatcher>();
var paramMatchers = request?.GetRequestMessageMatchers<RequestMessageParamMatcher>();
var methodMatcher = request?.GetRequestMessageMatcher<RequestMessageMethodMatcher>();
var bodyMatcher = request?.GetRequestMessageMatcher<RequestMessageBodyMatcher>();
var useDefinedRequestMatchers = proxyAndRecordSettings.UseDefinedRequestMatchers;
var excludedHeaders = new List<string>(proxyAndRecordSettings.ExcludedHeaders ?? new string[] { }) { "Cookie" };
var excludedCookies = proxyAndRecordSettings.ExcludedCookies ?? new string[] { };
var newRequest = Request.Create();
// ClientIP
if (useDefinedRequestMatchers && clientIPMatcher?.Matchers is not null)
{
newRequest.WithClientIP(clientIPMatcher.MatchOperator, clientIPMatcher.Matchers.ToArray());
}
// Path
if (useDefinedRequestMatchers && pathMatcher?.Matchers is not null)
{
newRequest.WithPath(pathMatcher.MatchOperator, pathMatcher.Matchers.ToArray());
}
else
{
newRequest.WithPath(requestMessage.Path);
}
// Method
if (useDefinedRequestMatchers && methodMatcher is not null)
{
newRequest.UsingMethod(methodMatcher.Methods);
}
else
{
newRequest.UsingMethod(requestMessage.Method);
}
// QueryParams
if (useDefinedRequestMatchers && paramMatchers is not null)
{
foreach (var paramMatcher in paramMatchers)
{
newRequest.WithParam(paramMatcher.Key, paramMatcher.MatchBehaviour, paramMatcher.Matchers!.ToArray());
}
}
else
{
requestMessage.Query?.Loop((key, value) => newRequest.WithParam(key, false, value.ToArray()));
}
// Cookies
if (useDefinedRequestMatchers && cookieMatchers is not null)
{
foreach (var cookieMatcher in cookieMatchers.Where(hm => hm.Matchers is not null))
{
if (!excludedCookies.Contains(cookieMatcher.Name, StringComparer.OrdinalIgnoreCase))
{
newRequest.WithCookie(cookieMatcher.Name, cookieMatcher.Matchers!);
}
}
}
else
{
requestMessage.Cookies?.Loop((key, value) =>
{
if (!excludedCookies.Contains(key, StringComparer.OrdinalIgnoreCase))
{
newRequest.WithCookie(key, value);
}
});
}
// Headers
if (useDefinedRequestMatchers && headerMatchers is not null)
{
foreach (var headerMatcher in headerMatchers.Where(hm => hm.Matchers is not null))
{
if (!excludedHeaders.Contains(headerMatcher.Name, StringComparer.OrdinalIgnoreCase))
{
newRequest.WithHeader(headerMatcher.Name, headerMatcher.Matchers!);
}
}
}
else
{
requestMessage.Headers?.Loop((key, value) =>
{
if (!excludedHeaders.Contains(key, StringComparer.OrdinalIgnoreCase))
{
newRequest.WithHeader(key, value.ToArray());
}
});
}
// Body
bool throwExceptionWhenMatcherFails = _settings.ThrowExceptionWhenMatcherFails == true;
if (useDefinedRequestMatchers && bodyMatcher?.Matchers is not null)
{
newRequest.WithBody(bodyMatcher.Matchers);
}
else
{
switch (requestMessage.BodyData?.DetectedBodyType)
{
case BodyType.Json:
newRequest.WithBody(new JsonMatcher(MatchBehaviour.AcceptOnMatch, requestMessage.BodyData.BodyAsJson!, true, throwExceptionWhenMatcherFails));
break;
case BodyType.String:
newRequest.WithBody(new ExactMatcher(MatchBehaviour.AcceptOnMatch, throwExceptionWhenMatcherFails, MatchOperator.Or, requestMessage.BodyData.BodyAsString));
break;
case BodyType.Bytes:
newRequest.WithBody(new ExactObjectMatcher(MatchBehaviour.AcceptOnMatch, requestMessage.BodyData.BodyAsBytes, throwExceptionWhenMatcherFails));
break;
}
}
// Title
var title = useDefinedRequestMatchers && !string.IsNullOrEmpty(mapping?.Title) ?
mapping!.Title :
$"Proxy Mapping for {requestMessage.Method} {requestMessage.Path}";
// Description
var description = useDefinedRequestMatchers && !string.IsNullOrEmpty(mapping?.Description) ?
mapping!.Description :
$"Proxy Mapping for {requestMessage.Method} {requestMessage.Path}";
return new Mapping
(
guid: _guidUtils.NewGuid(),
title: title,
description: description,
path: null,
settings: _settings,
requestMatcher: newRequest,
provider: Response.Create(responseMessage),
priority: WireMockConstants.ProxyPriority, // This was 0
scenario: null,
executionConditionState: null,
nextState: null,
stateTimes: null,
webhooks: null,
useWebhooksFireAndForget: null,
timeSettings: null
);
}
}

View File

@@ -236,6 +236,7 @@ public partial class WireMockServer
var proxyHelper = new ProxyHelper(settings);
var (responseMessage, mapping) = await proxyHelper.SendAsync(
null,
_settings.ProxyAndRecordSettings!,
_httpClientForProxy!,
requestMessage,

View File

@@ -198,7 +198,7 @@ public partial class WireMockServer
{
foreach (var cookieModel in requestModel.Cookies.Where(c => c.Matchers != null))
{
requestBuilder = requestBuilder.WithCookie(
requestBuilder = requestBuilder.WithCookie(
cookieModel.Name,
cookieModel.IgnoreCase == true,
cookieModel.RejectOnMatch == true ? MatchBehaviour.RejectOnMatch : MatchBehaviour.AcceptOnMatch,

View File

@@ -49,4 +49,13 @@ public class ProxyAndRecordSettings : HttpClientSettings
/// </summary>
//[PublicAPI]
//public bool PreferProxyMapping { get; set; }
/// <summary>
/// When SaveMapping is set to <c>true</c>, this setting can be used to control the behavior of the generated request matchers for the new mapping.
/// - <c>false</c>, the default matchers will be used.
/// - <c>true</c>, the defined mappings in the request wil be used for the new mapping.
///
/// Default value is false.
/// </summary>
public bool UseDefinedRequestMatchers { get; set; }
}

View File

@@ -92,6 +92,7 @@ public static class WireMockServerSettingsParser
SaveMapping = parser.GetBoolValue("SaveMapping"),
SaveMappingForStatusCodePattern = parser.GetStringValue("SaveMappingForStatusCodePattern", "*"),
SaveMappingToFile = parser.GetBoolValue("SaveMappingToFile"),
UseDefinedRequestMatchers = parser.GetBoolValue(nameof(ProxyAndRecordSettings.UseDefinedRequestMatchers)),
Url = proxyUrl!
};

View File

@@ -0,0 +1,16 @@
using System;
namespace WireMock.Util;
internal interface IGuidUtils
{
Guid NewGuid();
}
internal class GuidUtils : IGuidUtils
{
public Guid NewGuid()
{
return Guid.NewGuid();
}
}