From f7b04f32347382d9a356ecddc8d529e58cdc512b Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 30 Sep 2022 11:25:11 +0200 Subject: [PATCH] Add UseDefinedRequestMatchers to ProxyAndRecordSettings (#821) * . * UseDefinedRequestMatchers * ok * . * ClientIP * t * fix ut * . * cf * cf2 --- resources/WireMock.Net-Logo.png | Bin 13131 -> 13825 bytes .../Admin/Mappings/CookieModel.cs | 52 ++--- .../Settings/ProxyAndRecordSettingsModel.cs | 9 + .../Request/RequestMessageCookieMatcher.cs | 35 ++-- .../Request/RequestMessageParamMatcher.cs | 9 +- src/WireMock.Net/Owin/WireMockMiddleware.cs | 1 - src/WireMock.Net/Proxy/ProxyHelper.cs | 84 +------- .../RequestBuilders/Request.WithParam.cs | 2 +- src/WireMock.Net/ResponseBuilders/Response.cs | 1 + .../Serialization/MappingConverter.cs | 2 +- .../Serialization/ProxyMappingConverter.cs | 181 ++++++++++++++++++ .../Server/WireMockServer.Admin.cs | 1 + .../Server/WireMockServer.ConvertMapping.cs | 2 +- .../Settings/ProxyAndRecordSettings.cs | 9 + .../Settings/WireMockServerSettingsParser.cs | 1 + src/WireMock.Net/Util/GuidUtils.cs | 16 ++ .../ProxyMappingConverterTests.cs | 70 +++++++ .../Serialization/files/proxy.json | 72 +++++++ .../WireMock.Net.Tests.csproj | 4 + .../WireMockServer.Proxy.cs | 66 ++++++- 20 files changed, 486 insertions(+), 131 deletions(-) create mode 100644 src/WireMock.Net/Serialization/ProxyMappingConverter.cs create mode 100644 src/WireMock.Net/Util/GuidUtils.cs create mode 100644 test/WireMock.Net.Tests/Serialization/ProxyMappingConverterTests.cs create mode 100644 test/WireMock.Net.Tests/Serialization/files/proxy.json diff --git a/resources/WireMock.Net-Logo.png b/resources/WireMock.Net-Logo.png index 7325bd1305f8cece886390e6682c72f87e8640e9..6e2bcf39deb8d9c37bf01c3e13ba39516c090847 100644 GIT binary patch literal 13825 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 Dnm-hJ literal 13131 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 diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/CookieModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/CookieModel.cs index 5b88bb42..e38c60f1 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/CookieModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/CookieModel.cs @@ -1,31 +1,39 @@ using System.Collections.Generic; -namespace WireMock.Admin.Mappings +namespace WireMock.Admin.Mappings; + +/// +/// Cookie Model +/// +[FluentBuilder.AutoGenerateBuilder] +public class CookieModel { /// - /// Cookie Model + /// Gets or sets the name. /// - [FluentBuilder.AutoGenerateBuilder] - public class CookieModel - { - /// - /// Gets or sets the name. - /// - public string Name { get; set; } = null!; + public string Name { get; set; } = null!; - /// - /// Gets or sets the matchers. - /// - public IList? Matchers { get; set; } + /// + /// Gets or sets the matchers. + /// + public IList? Matchers { get; set; } - /// - /// Gets or sets the ignore case. - /// - public bool? IgnoreCase { get; set; } + /// + /// Gets or sets the ignore case. + /// + public bool? IgnoreCase { get; set; } - /// - /// Reject on match. - /// - public bool? RejectOnMatch { get; set; } - } + /// + /// Reject on match. + /// + public bool? RejectOnMatch { get; set; } + + /// + /// The Operator to use when matchers are defined. [Optional] + /// - null = Same as "or". + /// - "or" = Only one pattern should match. + /// - "and" = All patterns should match. + /// - "average" = The average value from all patterns. + /// + public string? MatchOperator { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Admin/Settings/ProxyAndRecordSettingsModel.cs b/src/WireMock.Net.Abstractions/Admin/Settings/ProxyAndRecordSettingsModel.cs index 96ca4181..36d4b03e 100644 --- a/src/WireMock.Net.Abstractions/Admin/Settings/ProxyAndRecordSettingsModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Settings/ProxyAndRecordSettingsModel.cs @@ -54,5 +54,14 @@ namespace WireMock.Admin.Settings /// Prefer the Proxy Mapping over the saved Mapping (in case SaveMapping is set to true). /// // public bool PreferProxyMapping { get; set; } + + /// + /// When SaveMapping is set to true, this setting can be used to control the behavior of the generated request matchers for the new mapping. + /// - false, the default matchers will be used. + /// - true, the defined mappings in the request wil be used for the new mapping. + /// + /// Default value is false. + /// + public bool UseDefinedRequestMatchers { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageCookieMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageCookieMatcher.cs index f4af9a44..576927de 100644 --- a/src/WireMock.Net/Matchers/Request/RequestMessageCookieMatcher.cs +++ b/src/WireMock.Net/Matchers/Request/RequestMessageCookieMatcher.cs @@ -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; /// /// The functions /// - public Func, bool>[] Funcs { get; } + public Func, bool>[]? Funcs { get; } /// /// The name @@ -28,7 +28,7 @@ public class RequestMessageCookieMatcher : IRequestMatcher /// /// The matchers. /// - public IStringMatcher[] Matchers { get; } + public IStringMatcher[]? Matchers { get; } /// /// Initializes a new instance of the class. @@ -37,15 +37,12 @@ public class RequestMessageCookieMatcher : IRequestMatcher /// The pattern. /// Ignore the case from the pattern. /// The match behaviour. - 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) }; } /// @@ -55,10 +52,10 @@ public class RequestMessageCookieMatcher : IRequestMatcher /// The name. /// The patterns. /// Ignore the case from the pattern. - 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().ToArray()) { - Guard.NotNull(patterns, nameof(patterns)); + Guard.NotNull(patterns); } /// @@ -68,14 +65,11 @@ public class RequestMessageCookieMatcher : IRequestMatcher /// The name. /// The matchers. /// Ignore the case from the pattern. - 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 class. /// /// The funcs. - public RequestMessageCookieMatcher([NotNull] params Func, bool>[] funcs) + public RequestMessageCookieMatcher(params Func, 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. } /// diff --git a/src/WireMock.Net/Matchers/Request/RequestMessageParamMatcher.cs b/src/WireMock.Net/Matchers/Request/RequestMessageParamMatcher.cs index dc4e34f8..8ac113d7 100644 --- a/src/WireMock.Net/Matchers/Request/RequestMessageParamMatcher.cs +++ b/src/WireMock.Net/Matchers/Request/RequestMessageParamMatcher.cs @@ -11,7 +11,10 @@ namespace WireMock.Matchers.Request; /// public class RequestMessageParamMatcher : IRequestMatcher { - private readonly MatchBehaviour _matchBehaviour; + /// + /// MatchBehaviour + /// + public MatchBehaviour MatchBehaviour { get; } /// /// The funcs @@ -63,7 +66,7 @@ public class RequestMessageParamMatcher : IRequestMatcher /// The matchers. 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 /// 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); } diff --git a/src/WireMock.Net/Owin/WireMockMiddleware.cs b/src/WireMock.Net/Owin/WireMockMiddleware.cs index e0e088bf..23445942 100644 --- a/src/WireMock.Net/Owin/WireMockMiddleware.cs +++ b/src/WireMock.Net/Owin/WireMockMiddleware.cs @@ -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 diff --git a/src/WireMock.Net/Proxy/ProxyHelper.cs b/src/WireMock.Net/Proxy/ProxyHelper.cs index 600d628a..6d6178a4 100644 --- a/src/WireMock.Net/Proxy/ProxyHelper.cs +++ b/src/WireMock.Net/Proxy/ProxyHelper.cs @@ -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(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); } } \ No newline at end of file diff --git a/src/WireMock.Net/RequestBuilders/Request.WithParam.cs b/src/WireMock.Net/RequestBuilders/Request.WithParam.cs index 0b300f22..06a925e6 100644 --- a/src/WireMock.Net/RequestBuilders/Request.WithParam.cs +++ b/src/WireMock.Net/RequestBuilders/Request.WithParam.cs @@ -20,7 +20,7 @@ namespace WireMock.RequestBuilders /// 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; diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 194e1e69..536feb87 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -271,6 +271,7 @@ public partial class Response : IResponseBuilder var proxyHelper = new ProxyHelper(settings); return await proxyHelper.SendAsync( + mapping, ProxyAndRecordSettings, _httpClientForProxy, requestMessage, diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index 96dcf870..5f2460a6 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -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) diff --git a/src/WireMock.Net/Serialization/ProxyMappingConverter.cs b/src/WireMock.Net/Serialization/ProxyMappingConverter.cs new file mode 100644 index 00000000..67446ad5 --- /dev/null +++ b/src/WireMock.Net/Serialization/ProxyMappingConverter.cs @@ -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(); + var pathMatcher = request?.GetRequestMessageMatcher(); + var headerMatchers = request?.GetRequestMessageMatchers(); + var cookieMatchers = request?.GetRequestMessageMatchers(); + var paramMatchers = request?.GetRequestMessageMatchers(); + var methodMatcher = request?.GetRequestMessageMatcher(); + var bodyMatcher = request?.GetRequestMessageMatcher(); + + var useDefinedRequestMatchers = proxyAndRecordSettings.UseDefinedRequestMatchers; + + var excludedHeaders = new List(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 + ); + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Server/WireMockServer.Admin.cs b/src/WireMock.Net/Server/WireMockServer.Admin.cs index 52a58d3f..2609a554 100644 --- a/src/WireMock.Net/Server/WireMockServer.Admin.cs +++ b/src/WireMock.Net/Server/WireMockServer.Admin.cs @@ -236,6 +236,7 @@ public partial class WireMockServer var proxyHelper = new ProxyHelper(settings); var (responseMessage, mapping) = await proxyHelper.SendAsync( + null, _settings.ProxyAndRecordSettings!, _httpClientForProxy!, requestMessage, diff --git a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs index 3c732ff6..1368b06d 100644 --- a/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs +++ b/src/WireMock.Net/Server/WireMockServer.ConvertMapping.cs @@ -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, diff --git a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs index 851eba80..3ae30763 100644 --- a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs +++ b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs @@ -49,4 +49,13 @@ public class ProxyAndRecordSettings : HttpClientSettings /// //[PublicAPI] //public bool PreferProxyMapping { get; set; } + + /// + /// When SaveMapping is set to true, this setting can be used to control the behavior of the generated request matchers for the new mapping. + /// - false, the default matchers will be used. + /// - true, the defined mappings in the request wil be used for the new mapping. + /// + /// Default value is false. + /// + public bool UseDefinedRequestMatchers { get; set; } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs b/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs index 2caca070..32fecedb 100644 --- a/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs +++ b/src/WireMock.Net/Settings/WireMockServerSettingsParser.cs @@ -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! }; diff --git a/src/WireMock.Net/Util/GuidUtils.cs b/src/WireMock.Net/Util/GuidUtils.cs new file mode 100644 index 00000000..53892407 --- /dev/null +++ b/src/WireMock.Net/Util/GuidUtils.cs @@ -0,0 +1,16 @@ +using System; + +namespace WireMock.Util; + +internal interface IGuidUtils +{ + Guid NewGuid(); +} + +internal class GuidUtils : IGuidUtils +{ + public Guid NewGuid() + { + return Guid.NewGuid(); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/ProxyMappingConverterTests.cs b/test/WireMock.Net.Tests/Serialization/ProxyMappingConverterTests.cs new file mode 100644 index 00000000..228d63b3 --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/ProxyMappingConverterTests.cs @@ -0,0 +1,70 @@ +using System; +using Moq; +using Newtonsoft.Json; +using System.IO; +using FluentAssertions; +using WireMock.Matchers; +using WireMock.RequestBuilders; +using WireMock.Serialization; +using WireMock.Settings; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests.Serialization; + +public class ProxyMappingConverterTests +{ + private readonly WireMockServerSettings _settings = new(); + + private readonly MappingConverter _mappingConverter; + + private readonly ProxyMappingConverter _sut; + + public ProxyMappingConverterTests() + { + var guidUtilsMock = new Mock(); + guidUtilsMock.Setup(g => g.NewGuid()).Returns(Guid.Parse("ff55ac0a-fea9-4d7b-be74-5e483a2c1305")); + + _mappingConverter = new MappingConverter(new MatcherMapper(_settings)); + + _sut = new ProxyMappingConverter(_settings, guidUtilsMock.Object); + } + + [Fact] + public void ToMapping_UseDefinedRequestMatchers_True() + { + // Arrange + var proxyAndRecordSettings = new ProxyAndRecordSettings + { + UseDefinedRequestMatchers = true + }; + + var request = Request.Create() + .UsingPost() + .WithPath("x") + .WithParam("p1", "p1-v") + .WithParam("p2", "p2-v") + .WithHeader("Content-Type", new ContentTypeMatcher("text/plain")) + .WithCookie("c", "x") + .WithBody(new RegexMatcher("Auth(); + mappingMock.SetupGet(m => m.RequestMatcher).Returns(request); + mappingMock.SetupGet(m => m.Title).Returns("my title"); + mappingMock.SetupGet(m => m.Description).Returns("my description"); + + var requestMessageMock = new Mock(); + + var responseMessage = new ResponseMessage(); + + // Act + var proxyMapping = _sut.ToMapping(mappingMock.Object, proxyAndRecordSettings, requestMessageMock.Object, responseMessage); + + // Assert + var model = _mappingConverter.ToMappingModel(proxyMapping); + var json = JsonConvert.SerializeObject(model, JsonSerializationConstants.JsonSerializerSettingsDefault); + var expected = File.ReadAllText(Path.Combine("../../../", "Serialization", "files", "proxy.json")); + + json.Should().Be(expected); + } +} \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/files/proxy.json b/test/WireMock.Net.Tests/Serialization/files/proxy.json new file mode 100644 index 00000000..a045bc7a --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/files/proxy.json @@ -0,0 +1,72 @@ +{ + "Guid": "ff55ac0a-fea9-4d7b-be74-5e483a2c1305", + "Title": "my title", + "Description": "my description", + "Priority": -2000000, + "Request": { + "Path": { + "Matchers": [ + { + "Name": "WildcardMatcher", + "Pattern": "x", + "IgnoreCase": false + } + ] + }, + "Methods": [ + "POST" + ], + "Headers": [ + { + "Name": "Content-Type", + "Matchers": [ + { + "Name": "ContentTypeMatcher", + "Pattern": "text/plain", + "IgnoreCase": false + } + ] + } + ], + "Cookies": [ + { + "Name": "c", + "Matchers": [ + { + "Name": "WildcardMatcher", + "Pattern": "x", + "IgnoreCase": true + } + ] + } + ], + "Params": [ + { + "Name": "p1", + "Matchers": [ + { + "Name": "ExactMatcher", + "Pattern": "p1-v" + } + ] + }, + { + "Name": "p2", + "Matchers": [ + { + "Name": "ExactMatcher", + "Pattern": "p2-v" + } + ] + } + ], + "Body": { + "Matcher": { + "Name": "RegexMatcher", + "Pattern": "Auth PreserveNewest + + PreserveNewest + PreserveNewest @@ -105,6 +108,7 @@ + \ No newline at end of file diff --git a/test/WireMock.Net.Tests/WireMockServer.Proxy.cs b/test/WireMock.Net.Tests/WireMockServer.Proxy.cs index def80c60..aa2a03a7 100644 --- a/test/WireMock.Net.Tests/WireMockServer.Proxy.cs +++ b/test/WireMock.Net.Tests/WireMockServer.Proxy.cs @@ -1,3 +1,6 @@ +using FluentAssertions; +using Moq; +using NFluent; using System; using System.Linq; using System.Net; @@ -5,11 +8,9 @@ using System.Net.Http; using System.Net.Http.Headers; using System.Text; using System.Threading.Tasks; -using FluentAssertions; -using Moq; -using NFluent; -using WireMock.Admin.Mappings; +using WireMock.Constants; using WireMock.Handlers; +using WireMock.Matchers; using WireMock.Matchers.Request; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; @@ -119,6 +120,61 @@ public class WireMockServerProxyTests server.Mappings.Should().HaveCount(28); } + [Fact] + public async Task WireMockServer_Proxy_With_SaveMappingToFile_Is_True_ShouldSaveMappingToFile() + { + // Assign + string path = $"/prx_{Guid.NewGuid()}"; + var title = "IndexFile"; + var description = "IndexFile_Test"; + var stringBody = "value"; + var serverForProxyForwarding = WireMockServer.Start(); + var fileSystemHandlerMock = new Mock(); + fileSystemHandlerMock.Setup(f => f.GetMappingFolder()).Returns("m"); + + var settings = new WireMockServerSettings + { + ProxyAndRecordSettings = new ProxyAndRecordSettings + { + Url = serverForProxyForwarding.Urls[0], + SaveMapping = false, + SaveMappingToFile = true + }, + FileSystemHandler = fileSystemHandlerMock.Object + }; + + var server = WireMockServer.Start(settings); + server.Given(Request.Create() + .WithPath("/*") + .WithBody(new RegexMatcher(stringBody))) + .WithTitle(title) + .WithDescription(description) + .AtPriority(WireMockConstants.ProxyPriority) + .RespondWith(Response.Create().WithProxy(new ProxyAndRecordSettings + { + Url = serverForProxyForwarding.Urls[0], + SaveMapping = false, + SaveMappingToFile = true, + UseDefinedRequestMatchers = true, + })); + + // Act + var requestMessage = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri($"{server.Urls[0]}{path}"), + Content = new StringContent(stringBody) + }; + var httpClientHandler = new HttpClientHandler { AllowAutoRedirect = false }; + await new HttpClient(httpClientHandler).SendAsync(requestMessage).ConfigureAwait(false); + + // Assert + server.Mappings.Should().HaveCount(2); + + // Verify + fileSystemHandlerMock.Verify(f => f.WriteMappingFile($"m{System.IO.Path.DirectorySeparatorChar}{title}.json", It.IsRegex(stringBody)), Times.Once); + } + [Fact] public async Task WireMockServer_Proxy_With_SaveMapping_Is_False_And_SaveMappingToFile_Is_True_ShouldSaveMappingToFile() { @@ -735,8 +791,6 @@ public class WireMockServerProxyTests content.Should().NotBeEmpty(); server.LogEntries.Should().HaveCount(1); - var status = ((StatusModel)server.LogEntries.First().ResponseMessage.BodyData.BodyAsJson).Status; - server.Stop(); } } \ No newline at end of file