From 35565f6aa88dbb3eaf55e29b9a53de45e90bcd29 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Tue, 8 Dec 2020 08:21:00 +0100 Subject: [PATCH] WithProxy(...) also use all proxy settings (#550) --- .../Program.cs | 28 +- ...reMock.Net.Console.Proxy.NETCoreApp.csproj | 2 +- ...tpClientHelper.cs => HttpClientBuilder.cs} | 34 +- src/WireMock.Net/IMapping.cs | 2 +- src/WireMock.Net/Mapping.cs | 17 +- src/WireMock.Net/Owin/WireMockMiddleware.cs | 17 + src/WireMock.Net/Proxy/ProxyHelper.cs | 111 +++++ .../ResponseBuilders/Response.WithProxy.cs | 12 +- src/WireMock.Net/ResponseBuilders/Response.cs | 19 +- .../ResponseProviders/IResponseProvider.cs | 2 +- .../JsonSerializationConstants.cs | 19 + .../Serialization/MappingConverter.cs | 6 +- .../Serialization/MappingToFileSaver.cs | 49 +++ .../Server/WireMockServer.Admin.cs | 123 +----- src/WireMock.Net/Server/WireMockServer.cs | 4 +- .../Settings/ProxyAndRecordSettings.cs | 4 +- .../Owin/WireMockMiddlewareTests.cs | 389 +++++++++++------- 17 files changed, 524 insertions(+), 314 deletions(-) rename src/WireMock.Net/Http/{HttpClientHelper.cs => HttpClientBuilder.cs} (69%) create mode 100644 src/WireMock.Net/Proxy/ProxyHelper.cs create mode 100644 src/WireMock.Net/Serialization/JsonSerializationConstants.cs create mode 100644 src/WireMock.Net/Serialization/MappingToFileSaver.cs diff --git a/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs b/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs index 547266bc..604b6615 100644 --- a/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs +++ b/examples/WireMock.Net.Console.Record.NETCoreApp/Program.cs @@ -1,4 +1,6 @@ using Newtonsoft.Json; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; using WireMock.Server; using WireMock.Settings; @@ -13,16 +15,26 @@ namespace WireMock.Net.Console.Proxy.NETCoreApp Urls = new[] { "http://localhost:9091/", "https://localhost:9443/" }, StartAdminInterface = true, ReadStaticMappings = false, - ProxyAndRecordSettings = new ProxyAndRecordSettings - { - Url = "https://www.google.com", - //ClientX509Certificate2ThumbprintOrSubjectName = "www.yourclientcertname.com OR yourcertificatethumbprint (only if the service you're proxying to requires it)", - SaveMapping = true, - SaveMappingToFile = false, - ExcludedHeaders = new [] { "dnt", "Content-Length" } - } + //ProxyAndRecordSettings = new ProxyAndRecordSettings + //{ + // Url = "https://www.google.com", + // //ClientX509Certificate2ThumbprintOrSubjectName = "www.yourclientcertname.com OR yourcertificatethumbprint (only if the service you're proxying to requires it)", + // SaveMapping = true, + // SaveMappingToFile = false, + // ExcludedHeaders = new [] { "dnt", "Content-Length" } + //} }); + server + .Given(Request.Create().UsingGet()) + .RespondWith(Response.Create() + .WithProxy(new ProxyAndRecordSettings + { + Url = "http://postman-echo.com/post", + SaveMapping = true, + SaveMappingToFile = true + })); + System.Console.WriteLine("Press any key to stop the server"); System.Console.ReadKey(); server.Stop(); diff --git a/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Proxy.NETCoreApp.csproj b/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Proxy.NETCoreApp.csproj index c0c3c194..1526ba1f 100644 --- a/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Proxy.NETCoreApp.csproj +++ b/examples/WireMock.Net.Console.Record.NETCoreApp/WireMock.Net.Console.Proxy.NETCoreApp.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp1.1 + net5.0 ../../WireMock.Net-Logo.ico diff --git a/src/WireMock.Net/Http/HttpClientHelper.cs b/src/WireMock.Net/Http/HttpClientBuilder.cs similarity index 69% rename from src/WireMock.Net/Http/HttpClientHelper.cs rename to src/WireMock.Net/Http/HttpClientBuilder.cs index 76247d61..00b2dea2 100644 --- a/src/WireMock.Net/Http/HttpClientHelper.cs +++ b/src/WireMock.Net/Http/HttpClientBuilder.cs @@ -1,17 +1,13 @@ -using JetBrains.Annotations; -using System; -using System.Net; +using System.Net; using System.Net.Http; -using System.Threading.Tasks; using WireMock.HttpsCertificate; using WireMock.Settings; -using WireMock.Validation; namespace WireMock.Http { - internal static class HttpClientHelper + internal static class HttpClientBuilder { - public static HttpClient CreateHttpClient(IProxyAndRecordSettings settings) + public static HttpClient Build(IProxyAndRecordSettings settings) { #if NETSTANDARD || NETCOREAPP3_1 || NET5_0 var handler = new HttpClientHandler @@ -57,32 +53,14 @@ namespace WireMock.Http { handler.Proxy.Credentials = new NetworkCredential(settings.WebProxySettings.UserName, settings.WebProxySettings.Password); } - } - + } + #if !NETSTANDARD1_3 ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12 | SecurityProtocolType.Tls11 | SecurityProtocolType.Tls; ServicePointManager.ServerCertificateValidationCallback = (message, cert, chain, errors) => true; #endif - + return new HttpClient(handler); } - - public static async Task SendAsync([NotNull] HttpClient client, [NotNull] RequestMessage requestMessage, string url, bool deserializeJson, bool decompressGzipAndDeflate) - { - Check.NotNull(client, nameof(client)); - Check.NotNull(requestMessage, nameof(requestMessage)); - - var originalUri = new Uri(requestMessage.Url); - var requiredUri = new Uri(url); - - // Create HttpRequestMessage - var httpRequestMessage = HttpRequestMessageHelper.Create(requestMessage, url); - - // Call the URL - var httpResponseMessage = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseContentRead); - - // Create ResponseMessage - return await HttpResponseMessageHelper.CreateAsync(httpResponseMessage, requiredUri, originalUri, deserializeJson, decompressGzipAndDeflate); - } } } \ No newline at end of file diff --git a/src/WireMock.Net/IMapping.cs b/src/WireMock.Net/IMapping.cs index a28bd526..6cdcf0e2 100644 --- a/src/WireMock.Net/IMapping.cs +++ b/src/WireMock.Net/IMapping.cs @@ -71,7 +71,7 @@ namespace WireMock /// The WireMockServerSettings. /// IWireMockServerSettings Settings { get; } - + /// /// Is State started ? /// diff --git a/src/WireMock.Net/Mapping.cs b/src/WireMock.Net/Mapping.cs index 6c215be0..9952f4ef 100644 --- a/src/WireMock.Net/Mapping.cs +++ b/src/WireMock.Net/Mapping.cs @@ -68,9 +68,18 @@ namespace WireMock /// State in which the current mapping can occur. [Optional] /// The next state which will occur after the current mapping execution. [Optional] /// Only when the current state is executed this number, the next state which will occur. [Optional] - public Mapping(Guid guid, [CanBeNull] string title, [CanBeNull] string path, - [NotNull] IWireMockServerSettings settings, [NotNull] IRequestMatcher requestMatcher, [NotNull] IResponseProvider provider, - int priority, [CanBeNull] string scenario, [CanBeNull] string executionConditionState, [CanBeNull] string nextState, [CanBeNull] int? stateTimes) + public Mapping( + Guid guid, + [CanBeNull] string title, + [CanBeNull] string path, + [NotNull] IWireMockServerSettings settings, + [NotNull] IRequestMatcher requestMatcher, + [NotNull] IResponseProvider provider, + int priority, + [CanBeNull] string scenario, + [CanBeNull] string executionConditionState, + [CanBeNull] string nextState, + [CanBeNull] int? stateTimes) { Guid = guid; Title = title; @@ -82,7 +91,7 @@ namespace WireMock Scenario = scenario; ExecutionConditionState = executionConditionState; NextState = nextState; - StateTimes = stateTimes; + StateTimes = stateTimes; } /// diff --git a/src/WireMock.Net/Owin/WireMockMiddleware.cs b/src/WireMock.Net/Owin/WireMockMiddleware.cs index 5047306c..e6f9cac1 100644 --- a/src/WireMock.Net/Owin/WireMockMiddleware.cs +++ b/src/WireMock.Net/Owin/WireMockMiddleware.cs @@ -8,6 +8,7 @@ using WireMock.Owin.Mappers; using WireMock.Serialization; using WireMock.Types; using WireMock.Validation; +using WireMock.ResponseBuilders; #if !USE_ASPNETCORE using Microsoft.Owin; using IContext = Microsoft.Owin.IOwinContext; @@ -129,6 +130,22 @@ namespace WireMock.Owin response = await targetMapping.ProvideResponseAsync(request); + var responseBuilder = targetMapping.Provider as Response; + + if (responseBuilder?.ProxyAndRecordSettings?.SaveMapping == true || targetMapping?.Settings?.ProxyAndRecordSettings?.SaveMapping == true) + { + _options.Mappings.TryAdd(targetMapping.Guid, targetMapping); + } + + if (responseBuilder?.ProxyAndRecordSettings?.SaveMappingToFile == true || targetMapping?.Settings?.ProxyAndRecordSettings?.SaveMappingToFile == true) + { + var matcherMapper = new MatcherMapper(targetMapping.Settings); + var mappingConverter = new MappingConverter(matcherMapper); + var mappingToFileSaver = new MappingToFileSaver(targetMapping.Settings, mappingConverter); + + mappingToFileSaver.SaveMappingToFile(targetMapping); + } + if (targetMapping.Scenario != null) { UpdateScenarioState(targetMapping); diff --git a/src/WireMock.Net/Proxy/ProxyHelper.cs b/src/WireMock.Net/Proxy/ProxyHelper.cs new file mode 100644 index 00000000..d4410f21 --- /dev/null +++ b/src/WireMock.Net/Proxy/ProxyHelper.cs @@ -0,0 +1,111 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using HandlebarsDotNet.Helpers.Validation; +using JetBrains.Annotations; +using WireMock.Http; +using WireMock.Matchers; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Settings; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Proxy +{ + internal class ProxyHelper + { + private readonly IWireMockServerSettings _settings; + + public ProxyHelper([NotNull] IWireMockServerSettings settings) + { + Guard.NotNull(settings, nameof(settings)); + _settings = settings; + } + + public async Task<(ResponseMessage ResponseMessage, IMapping Mapping)> SendAsync( + [NotNull] IProxyAndRecordSettings proxyAndRecordSettings, + [NotNull] HttpClient client, + [NotNull] RequestMessage requestMessage, + [NotNull] string url) + { + Guard.NotNull(client, nameof(client)); + Guard.NotNull(requestMessage, nameof(requestMessage)); + Guard.NotNull(url, nameof(url)); + + var originalUri = new Uri(requestMessage.Url); + var requiredUri = new Uri(url); + + // Create HttpRequestMessage + var httpRequestMessage = HttpRequestMessageHelper.Create(requestMessage, url); + + // Call the URL + var httpResponseMessage = await client.SendAsync(httpRequestMessage, HttpCompletionOption.ResponseContentRead); + + // Create ResponseMessage + bool deserializeJson = !_settings.DisableJsonBodyParsing.GetValueOrDefault(false); + bool decompressGzipAndDeflate = !_settings.DisableRequestBodyDecompressing.GetValueOrDefault(false); + + var responseMessage = await HttpResponseMessageHelper.CreateAsync(httpResponseMessage, requiredUri, originalUri, deserializeJson, decompressGzipAndDeflate); + + IMapping mapping = null; + if (HttpStatusRangeParser.IsMatch(proxyAndRecordSettings.SaveMappingForStatusCodePattern, responseMessage.StatusCode) && + (proxyAndRecordSettings.SaveMapping || proxyAndRecordSettings.SaveMappingToFile)) + { + mapping = ToMapping(proxyAndRecordSettings, requestMessage, responseMessage); + } + + return (responseMessage, mapping); + } + + private IMapping ToMapping(IProxyAndRecordSettings proxyAndRecordSettings, RequestMessage requestMessage, ResponseMessage responseMessage) + { + string[] excludedHeaders = proxyAndRecordSettings.ExcludedHeaders ?? new string[] { }; + string[] 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, 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.NewGuid(), string.Empty, null, _settings, request, response, 0, null, null, null, null); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/ResponseBuilders/Response.WithProxy.cs b/src/WireMock.Net/ResponseBuilders/Response.WithProxy.cs index b43880cb..526364e0 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.WithProxy.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.WithProxy.cs @@ -9,15 +9,10 @@ namespace WireMock.ResponseBuilders { private HttpClient _httpClientForProxy; - /// - /// The Proxy URL to use. - /// - public string ProxyUrl { get; private set; } - /// /// The WebProxy settings. /// - public IWebProxySettings WebProxySettings { get; private set; } + public IProxyAndRecordSettings ProxyAndRecordSettings { get; private set; } /// public IResponseBuilder WithProxy(string proxyUrl, string clientX509Certificate2ThumbprintOrSubjectName = null) @@ -38,10 +33,9 @@ namespace WireMock.ResponseBuilders { Check.NotNull(settings, nameof(settings)); - ProxyUrl = settings.Url; - WebProxySettings = settings.WebProxySettings; + ProxyAndRecordSettings = settings; - _httpClientForProxy = HttpClientHelper.CreateHttpClient(settings); + _httpClientForProxy = HttpClientBuilder.Build(settings); return this; } } diff --git a/src/WireMock.Net/ResponseBuilders/Response.cs b/src/WireMock.Net/ResponseBuilders/Response.cs index 061552b8..b999bd55 100644 --- a/src/WireMock.Net/ResponseBuilders/Response.cs +++ b/src/WireMock.Net/ResponseBuilders/Response.cs @@ -7,7 +7,7 @@ using System.Net; using System.Text; using System.Threading.Tasks; using JetBrains.Annotations; -using WireMock.Http; +using WireMock.Proxy; using WireMock.ResponseProviders; using WireMock.Settings; using WireMock.Transformers; @@ -312,7 +312,7 @@ namespace WireMock.ResponseBuilders await Task.Delay(Delay.Value); } - if (ProxyUrl != null && _httpClientForProxy != null) + if (ProxyAndRecordSettings != null && _httpClientForProxy != null) { string RemoveFirstOccurrence(string source, string find) { @@ -323,16 +323,19 @@ namespace WireMock.ResponseBuilders var requestUri = new Uri(requestMessage.Url); // Build the proxy url and skip duplicates - string extra = RemoveFirstOccurrence(requestUri.LocalPath.TrimEnd('/'), new Uri(ProxyUrl).LocalPath.TrimEnd('/')); - requestMessage.ProxyUrl = ProxyUrl + extra + requestUri.Query; + string extra = RemoveFirstOccurrence(requestUri.LocalPath.TrimEnd('/'), new Uri(ProxyAndRecordSettings.Url).LocalPath.TrimEnd('/')); + requestMessage.ProxyUrl = ProxyAndRecordSettings.Url + extra + requestUri.Query; - return await HttpClientHelper.SendAsync( + var proxyHelper = new ProxyHelper(settings); + + var (proxyResponseMessage, mapping) = await proxyHelper.SendAsync( + ProxyAndRecordSettings, _httpClientForProxy, requestMessage, - requestMessage.ProxyUrl, - !settings.DisableJsonBodyParsing.GetValueOrDefault(false), - !settings.DisableRequestBodyDecompressing.GetValueOrDefault(false) + requestMessage.ProxyUrl ); + + return proxyResponseMessage; } ResponseMessage responseMessage; diff --git a/src/WireMock.Net/ResponseProviders/IResponseProvider.cs b/src/WireMock.Net/ResponseProviders/IResponseProvider.cs index a3813b4c..6d33443b 100644 --- a/src/WireMock.Net/ResponseProviders/IResponseProvider.cs +++ b/src/WireMock.Net/ResponseProviders/IResponseProvider.cs @@ -1,7 +1,7 @@ // This source file is based on mock4net by Alexandre Victoor which is licensed under the Apache 2.0 License. // For more details see 'mock4net/LICENSE.txt' and 'mock4net/readme.md' in this project root. -using JetBrains.Annotations; using System.Threading.Tasks; +using JetBrains.Annotations; using WireMock.Settings; namespace WireMock.ResponseProviders diff --git a/src/WireMock.Net/Serialization/JsonSerializationConstants.cs b/src/WireMock.Net/Serialization/JsonSerializationConstants.cs new file mode 100644 index 00000000..b251a340 --- /dev/null +++ b/src/WireMock.Net/Serialization/JsonSerializationConstants.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace WireMock.Serialization +{ + internal static class JsonSerializationConstants + { + public static readonly JsonSerializerSettings JsonSerializerSettingsDefault = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Ignore + }; + + public static readonly JsonSerializerSettings JsonSerializerSettingsIncludeNullValues = new JsonSerializerSettings + { + Formatting = Formatting.Indented, + NullValueHandling = NullValueHandling.Include + }; + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index 90cbf966..6e4167f9 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -101,7 +101,7 @@ namespace WireMock.Serialization } } - if (!string.IsNullOrEmpty(response.ProxyUrl)) + if (response.ProxyAndRecordSettings != null) { mappingModel.Response.StatusCode = null; mappingModel.Response.Headers = null; @@ -115,9 +115,9 @@ namespace WireMock.Serialization mappingModel.Response.UseTransformer = null; mappingModel.Response.UseTransformerForBodyAsFile = null; mappingModel.Response.BodyEncoding = null; - mappingModel.Response.ProxyUrl = response.ProxyUrl; + mappingModel.Response.ProxyUrl = response.ProxyAndRecordSettings.Url; mappingModel.Response.Fault = null; - mappingModel.Response.WebProxy = MapWebProxy(response.WebProxySettings); + mappingModel.Response.WebProxy = MapWebProxy(response.ProxyAndRecordSettings.WebProxySettings); } else { diff --git a/src/WireMock.Net/Serialization/MappingToFileSaver.cs b/src/WireMock.Net/Serialization/MappingToFileSaver.cs new file mode 100644 index 00000000..6b6581fc --- /dev/null +++ b/src/WireMock.Net/Serialization/MappingToFileSaver.cs @@ -0,0 +1,49 @@ +using System.IO; +using System.Linq; +using Newtonsoft.Json; +using WireMock.Settings; +using WireMock.Validation; + +namespace WireMock.Serialization +{ + internal class MappingToFileSaver + { + private readonly IWireMockServerSettings _settings; + private readonly MappingConverter _mappingConverter; + + public MappingToFileSaver(IWireMockServerSettings settings, MappingConverter mappingConverter) + { + Check.NotNull(settings, nameof(settings)); + + _settings = settings; + _mappingConverter = mappingConverter; + } + + public void SaveMappingToFile(IMapping mapping, string folder = null) + { + if (folder == null) + { + folder = _settings.FileSystemHandler.GetMappingFolder(); + } + + if (!_settings.FileSystemHandler.FolderExists(folder)) + { + _settings.FileSystemHandler.CreateFolder(folder); + } + + var model = _mappingConverter.ToMappingModel(mapping); + string filename = (!string.IsNullOrEmpty(mapping.Title) ? SanitizeFileName(mapping.Title) : mapping.Guid.ToString()) + ".json"; + + string path = Path.Combine(folder, filename); + + _settings.Logger.Info("Saving Mapping file {0}", filename); + + _settings.FileSystemHandler.WriteMappingFile(path, JsonConvert.SerializeObject(model, JsonSerializationConstants.JsonSerializerSettingsDefault)); + } + + private static string SanitizeFileName(string name, char replaceChar = '_') + { + return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, replaceChar)); + } + } +} \ 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 d0bd7d73..80b91e01 100644 --- a/src/WireMock.Net/Server/WireMockServer.Admin.cs +++ b/src/WireMock.Net/Server/WireMockServer.Admin.cs @@ -16,6 +16,7 @@ using WireMock.Http; using WireMock.Logging; using WireMock.Matchers; using WireMock.Matchers.Request; +using WireMock.Proxy; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.ResponseProviders; @@ -47,18 +48,6 @@ namespace WireMock.Server private readonly RegexMatcher _adminMappingsGuidPathMatcher = new RegexMatcher(@"^\/__admin\/mappings\/([0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12})$"); private readonly RegexMatcher _adminRequestsGuidPathMatcher = new RegexMatcher(@"^\/__admin\/requests\/([0-9A-Fa-f]{8}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{4}[-][0-9A-Fa-f]{12})$"); - private readonly JsonSerializerSettings _jsonSerializerSettings = new JsonSerializerSettings - { - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Ignore - }; - - private readonly JsonSerializerSettings _settingsIncludeNullValues = new JsonSerializerSettings - { - Formatting = Formatting.Indented, - NullValueHandling = NullValueHandling.Include - }; - #region InitAdmin private void InitAdmin() { @@ -119,7 +108,7 @@ namespace WireMock.Server { foreach (var mapping in Mappings.Where(m => !m.IsAdminInterface)) { - SaveMappingToFile(mapping, folder); + _mappingToFileSaver.SaveMappingToFile(mapping, folder); } } @@ -246,7 +235,7 @@ namespace WireMock.Server private void InitProxyAndRecord(IWireMockServerSettings settings) { - _httpClientForProxy = HttpClientHelper.CreateHttpClient(settings.ProxyAndRecordSettings); + _httpClientForProxy = HttpClientBuilder.Build(settings.ProxyAndRecordSettings); var respondProvider = Given(Request.Create().WithPath("/*").UsingAnyMethod()); if (settings.StartAdminInterface == true) @@ -263,82 +252,27 @@ namespace WireMock.Server var proxyUri = new Uri(settings.ProxyAndRecordSettings.Url); var proxyUriWithRequestPathAndQuery = new Uri(proxyUri, requestUri.PathAndQuery); - var responseMessage = await HttpClientHelper.SendAsync( + var proxyHelper = new ProxyHelper(settings); + + var (responseMessage, mapping) = await proxyHelper.SendAsync( + _settings.ProxyAndRecordSettings, _httpClientForProxy, requestMessage, - proxyUriWithRequestPathAndQuery.AbsoluteUri, - !settings.DisableJsonBodyParsing.GetValueOrDefault(false), - !settings.DisableRequestBodyDecompressing.GetValueOrDefault(false) + proxyUriWithRequestPathAndQuery.AbsoluteUri ); - if (HttpStatusRangeParser.IsMatch(settings.ProxyAndRecordSettings.SaveMappingForStatusCodePattern, responseMessage.StatusCode) && - (settings.ProxyAndRecordSettings.SaveMapping || settings.ProxyAndRecordSettings.SaveMappingToFile)) + if (settings.ProxyAndRecordSettings.SaveMapping) { - var mapping = ToMapping( - requestMessage, - responseMessage, - settings.ProxyAndRecordSettings.ExcludedHeaders ?? new string[] { }, - settings.ProxyAndRecordSettings.ExcludedCookies ?? new string[] { } - ); + _options.Mappings.TryAdd(mapping.Guid, mapping); + } - if (settings.ProxyAndRecordSettings.SaveMapping) - { - _options.Mappings.TryAdd(mapping.Guid, mapping); - } - - if (settings.ProxyAndRecordSettings.SaveMappingToFile) - { - SaveMappingToFile(mapping); - } + if (settings.ProxyAndRecordSettings.SaveMappingToFile) + { + _mappingToFileSaver.SaveMappingToFile(mapping); } return responseMessage; } - - private IMapping ToMapping(RequestMessage requestMessage, ResponseMessage responseMessage, string[] excludedHeaders, string[] excludedCookies) - { - 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, 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.NewGuid(), string.Empty, null, _settings, request, response, 0, null, null, null, null); - } #endregion #region Settings @@ -446,33 +380,6 @@ namespace WireMock.Server return ResponseMessageBuilder.Create("Mappings saved to disk"); } - private void SaveMappingToFile(IMapping mapping, string folder = null) - { - if (folder == null) - { - folder = _settings.FileSystemHandler.GetMappingFolder(); - } - - if (!_settings.FileSystemHandler.FolderExists(folder)) - { - _settings.FileSystemHandler.CreateFolder(folder); - } - - var model = _mappingConverter.ToMappingModel(mapping); - string filename = (!string.IsNullOrEmpty(mapping.Title) ? SanitizeFileName(mapping.Title) : mapping.Guid.ToString()) + ".json"; - - string path = Path.Combine(folder, filename); - - _settings.Logger.Info("Saving Mapping file {0}", filename); - - _settings.FileSystemHandler.WriteMappingFile(path, JsonConvert.SerializeObject(model, _jsonSerializerSettings)); - } - - private static string SanitizeFileName(string name, char replaceChar = '_') - { - return Path.GetInvalidFileNameChars().Aggregate(name, (current, c) => current.Replace(c, replaceChar)); - } - private IEnumerable ToMappingModels() { return Mappings.Where(m => !m.IsAdminInterface).Select(_mappingConverter.ToMappingModel); @@ -945,7 +852,7 @@ namespace WireMock.Server BodyData = new BodyData { DetectedBodyType = BodyType.String, - BodyAsString = JsonConvert.SerializeObject(result, keepNullValues ? _settingsIncludeNullValues : _jsonSerializerSettings) + BodyAsString = JsonConvert.SerializeObject(result, keepNullValues ? JsonSerializationConstants.JsonSerializerSettingsIncludeNullValues : JsonSerializationConstants.JsonSerializerSettingsDefault) }, StatusCode = (int)HttpStatusCode.OK, Headers = new Dictionary> { { HttpKnownHeaderNames.ContentType, new WireMockList(ContentTypeJson) } } diff --git a/src/WireMock.Net/Server/WireMockServer.cs b/src/WireMock.Net/Server/WireMockServer.cs index 02c51568..d5b0faa9 100644 --- a/src/WireMock.Net/Server/WireMockServer.cs +++ b/src/WireMock.Net/Server/WireMockServer.cs @@ -35,6 +35,7 @@ namespace WireMock.Server private readonly IWireMockMiddlewareOptions _options = new WireMockMiddlewareOptions(); private readonly MappingConverter _mappingConverter; private readonly MatcherMapper _matcherMapper; + private readonly MappingToFileSaver _mappingToFileSaver; /// [PublicAPI] @@ -238,6 +239,7 @@ namespace WireMock.Server _matcherMapper = new MatcherMapper(_settings); _mappingConverter = new MappingConverter(_matcherMapper); + _mappingToFileSaver = new MappingToFileSaver(_settings, _mappingConverter); #if USE_ASPNETCORE _httpServer = new AspNetCoreSelfHost(_options, urlOptions); @@ -491,7 +493,7 @@ namespace WireMock.Server if (saveToFile) { - SaveMappingToFile(mapping); + _mappingToFileSaver.SaveMappingToFile(mapping); } } } diff --git a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs index 04d492f8..cc797b3e 100644 --- a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs +++ b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs @@ -17,13 +17,13 @@ namespace WireMock.Settings /// Save the mapping for each request/response to the internal Mappings. /// [PublicAPI] - public bool SaveMapping { get; set; } = true; + public bool SaveMapping { get; set; } /// /// Save the mapping for each request/response also to a file. (Note that SaveMapping must also be set to true.) /// [PublicAPI] - public bool SaveMappingToFile { get; set; } = true; + public bool SaveMappingToFile { get; set; } /// /// Only save request/response to the internal Mappings if the status code is included in this pattern. (Note that SaveMapping must also be set to true.) diff --git a/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs b/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs index 0fcc0130..f4035a5f 100644 --- a/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs +++ b/test/WireMock.Net.Tests/Owin/WireMockMiddlewareTests.cs @@ -1,141 +1,250 @@ -using System; -using System.Collections.Concurrent; -using System.Linq.Expressions; -using System.Threading.Tasks; -using Moq; -using Xunit; -using WireMock.Models; -using WireMock.Owin; -using WireMock.Owin.Mappers; -using WireMock.Util; -using WireMock.Logging; -using WireMock.Matchers; -using System.Collections.Generic; -using WireMock.Admin.Mappings; -using WireMock.Admin.Requests; -#if NET452 -using Microsoft.Owin; -using IContext = Microsoft.Owin.IOwinContext; -using IRequest = Microsoft.Owin.IOwinRequest; -using IResponse = Microsoft.Owin.IOwinResponse; -#else -using Microsoft.AspNetCore.Http; -using IContext = Microsoft.AspNetCore.Http.HttpContext; -using IRequest = Microsoft.AspNetCore.Http.HttpRequest; -using IResponse = Microsoft.AspNetCore.Http.HttpResponse; -#endif - -namespace WireMock.Net.Tests.Owin -{ - public class WireMockMiddlewareTests - { - private readonly WireMockMiddleware _sut; - - private readonly Mock _optionsMock; - private readonly Mock _requestMapperMock; - private readonly Mock _responseMapperMock; - private readonly Mock _matcherMock; - private readonly Mock _mappingMock; - private readonly Mock _contextMock; - - public WireMockMiddlewareTests() - { - _optionsMock = new Mock(); - _optionsMock.SetupAllProperties(); - _optionsMock.Setup(o => o.Mappings).Returns(new ConcurrentDictionary()); - _optionsMock.Setup(o => o.LogEntries).Returns(new ConcurrentObservableCollection()); - _optionsMock.Setup(o => o.Scenarios).Returns(new ConcurrentDictionary()); - _optionsMock.Setup(o => o.Logger.Warn(It.IsAny(), It.IsAny())); - _optionsMock.Setup(o => o.Logger.Error(It.IsAny(), It.IsAny())); - _optionsMock.Setup(o => o.Logger.DebugRequestResponse(It.IsAny(), It.IsAny())); - - _requestMapperMock = new Mock(); - _requestMapperMock.SetupAllProperties(); - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); - _requestMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).ReturnsAsync(request); - - _responseMapperMock = new Mock(); - _responseMapperMock.SetupAllProperties(); - _responseMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); - - _matcherMock = new Mock(); - _matcherMock.SetupAllProperties(); - _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((new MappingMatcherResult(), new MappingMatcherResult())); - - _contextMock = new Mock(); - - _mappingMock = new Mock(); - - _sut = new WireMockMiddleware(null, _optionsMock.Object, _requestMapperMock.Object, _responseMapperMock.Object, _matcherMock.Object); - } - - [Fact] - public async void WireMockMiddleware_Invoke_NoMatch() - { - // Act - await _sut.Invoke(_contextMock.Object); - - // Assert and Verify - _optionsMock.Verify(o => o.Logger.Warn(It.IsAny(), It.IsAny()), Times.Once); - - Expression> match = r => (int)r.StatusCode == 404 && ((StatusModel)r.BodyData.BodyAsJson).Status == "No matching mapping found"; - _responseMapperMock.Verify(m => m.MapAsync(It.Is(match), It.IsAny()), Times.Once); - } - - [Fact] - public async void WireMockMiddleware_Invoke_IsAdminInterface_EmptyHeaders_401() - { - // Assign - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1", null, new Dictionary()); - _requestMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).ReturnsAsync(request); - - _optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher()); - _mappingMock.SetupGet(m => m.IsAdminInterface).Returns(true); - - var result = new MappingMatcherResult { Mapping = _mappingMock.Object }; - _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((result, result)); - - // Act - await _sut.Invoke(_contextMock.Object); - - // Assert and Verify - _optionsMock.Verify(o => o.Logger.Error(It.IsAny(), It.IsAny()), Times.Once); - - Expression> match = r => (int)r.StatusCode == 401; - _responseMapperMock.Verify(m => m.MapAsync(It.Is(match), It.IsAny()), Times.Once); - } - - [Fact] - public async void WireMockMiddleware_Invoke_IsAdminInterface_MissingHeader_401() - { - // Assign - var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1", null, new Dictionary { { "h", new[] { "x" } } }); - _requestMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).ReturnsAsync(request); - - _optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher()); - _mappingMock.SetupGet(m => m.IsAdminInterface).Returns(true); - - var result = new MappingMatcherResult { Mapping = _mappingMock.Object }; - _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((result, result)); - - // Act - await _sut.Invoke(_contextMock.Object); - - // Assert and Verify - _optionsMock.Verify(o => o.Logger.Error(It.IsAny(), It.IsAny()), Times.Once); - - Expression> match = r => (int)r.StatusCode == 401; - _responseMapperMock.Verify(m => m.MapAsync(It.Is(match), It.IsAny()), Times.Once); - } - - [Fact] - public async void WireMockMiddleware_Invoke_RequestLogExpirationDurationIsDefined() - { - // Assign - _optionsMock.SetupGet(o => o.RequestLogExpirationDuration).Returns(1); - - // Act - await _sut.Invoke(_contextMock.Object); - } - } +using System; +using System.Collections.Concurrent; +using System.Linq.Expressions; +using System.Threading.Tasks; +using Moq; +using Xunit; +using WireMock.Models; +using WireMock.Owin; +using WireMock.Owin.Mappers; +using WireMock.Util; +using WireMock.Logging; +using WireMock.Matchers; +using System.Collections.Generic; +using WireMock.Admin.Mappings; +using WireMock.Admin.Requests; +using WireMock.ResponseProviders; +using WireMock.Settings; +using FluentAssertions; +using WireMock.Handlers; +using WireMock.ResponseBuilders; +using WireMock.RequestBuilders; +#if NET452 +using Microsoft.Owin; +using IContext = Microsoft.Owin.IOwinContext; +using IRequest = Microsoft.Owin.IOwinRequest; +using IResponse = Microsoft.Owin.IOwinResponse; +#else +using Microsoft.AspNetCore.Http; +using IContext = Microsoft.AspNetCore.Http.HttpContext; +using IRequest = Microsoft.AspNetCore.Http.HttpRequest; +using IResponse = Microsoft.AspNetCore.Http.HttpResponse; +#endif + +namespace WireMock.Net.Tests.Owin +{ + public class WireMockMiddlewareTests + { + private readonly WireMockMiddleware _sut; + + private readonly Mock _optionsMock; + private readonly Mock _requestMapperMock; + private readonly Mock _responseMapperMock; + private readonly Mock _matcherMock; + private readonly Mock _mappingMock; + private readonly Mock _contextMock; + + private readonly ConcurrentDictionary _mappings = new ConcurrentDictionary(); + + public WireMockMiddlewareTests() + { + _optionsMock = new Mock(); + _optionsMock.SetupAllProperties(); + _optionsMock.Setup(o => o.Mappings).Returns(_mappings); + _optionsMock.Setup(o => o.LogEntries).Returns(new ConcurrentObservableCollection()); + _optionsMock.Setup(o => o.Scenarios).Returns(new ConcurrentDictionary()); + _optionsMock.Setup(o => o.Logger.Warn(It.IsAny(), It.IsAny())); + _optionsMock.Setup(o => o.Logger.Error(It.IsAny(), It.IsAny())); + _optionsMock.Setup(o => o.Logger.DebugRequestResponse(It.IsAny(), It.IsAny())); + + _requestMapperMock = new Mock(); + _requestMapperMock.SetupAllProperties(); + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1"); + _requestMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).ReturnsAsync(request); + + _responseMapperMock = new Mock(); + _responseMapperMock.SetupAllProperties(); + _responseMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).Returns(Task.FromResult(true)); + + _matcherMock = new Mock(); + _matcherMock.SetupAllProperties(); + _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((new MappingMatcherResult(), new MappingMatcherResult())); + + _contextMock = new Mock(); + + _mappingMock = new Mock(); + + _sut = new WireMockMiddleware(null, _optionsMock.Object, _requestMapperMock.Object, _responseMapperMock.Object, _matcherMock.Object); + } + + [Fact] + public async void WireMockMiddleware_Invoke_NoMatch() + { + // Act + await _sut.Invoke(_contextMock.Object); + + // Assert and Verify + _optionsMock.Verify(o => o.Logger.Warn(It.IsAny(), It.IsAny()), Times.Once); + + Expression> match = r => (int)r.StatusCode == 404 && ((StatusModel)r.BodyData.BodyAsJson).Status == "No matching mapping found"; + _responseMapperMock.Verify(m => m.MapAsync(It.Is(match), It.IsAny()), Times.Once); + } + + [Fact] + public async void WireMockMiddleware_Invoke_IsAdminInterface_EmptyHeaders_401() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1", null, new Dictionary()); + _requestMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).ReturnsAsync(request); + + _optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher()); + _mappingMock.SetupGet(m => m.IsAdminInterface).Returns(true); + + var result = new MappingMatcherResult { Mapping = _mappingMock.Object }; + _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((result, result)); + + // Act + await _sut.Invoke(_contextMock.Object); + + // Assert and Verify + _optionsMock.Verify(o => o.Logger.Error(It.IsAny(), It.IsAny()), Times.Once); + + Expression> match = r => (int)r.StatusCode == 401; + _responseMapperMock.Verify(m => m.MapAsync(It.Is(match), It.IsAny()), Times.Once); + } + + [Fact] + public async void WireMockMiddleware_Invoke_IsAdminInterface_MissingHeader_401() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1", null, new Dictionary { { "h", new[] { "x" } } }); + _requestMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).ReturnsAsync(request); + + _optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher()); + _mappingMock.SetupGet(m => m.IsAdminInterface).Returns(true); + + var result = new MappingMatcherResult { Mapping = _mappingMock.Object }; + _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((result, result)); + + // Act + await _sut.Invoke(_contextMock.Object); + + // Assert and Verify + _optionsMock.Verify(o => o.Logger.Error(It.IsAny(), It.IsAny()), Times.Once); + + Expression> match = r => (int)r.StatusCode == 401; + _responseMapperMock.Verify(m => m.MapAsync(It.Is(match), It.IsAny()), Times.Once); + } + + [Fact] + public async void WireMockMiddleware_Invoke_RequestLogExpirationDurationIsDefined() + { + // Assign + _optionsMock.SetupGet(o => o.RequestLogExpirationDuration).Returns(1); + + // Act + await _sut.Invoke(_contextMock.Object); + } + + [Fact] + public async void WireMockMiddleware_Invoke_Mapping_Has_ProxyAndRecordSettings_And_SaveMapping_Is_True() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1", null, new Dictionary()); + _requestMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).ReturnsAsync(request); + + _optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher()); + + var fileSystemHandlerMock = new Mock(); + fileSystemHandlerMock.Setup(f => f.GetMappingFolder()).Returns("m"); + + var logger = new Mock(); + + var proxyAndRecordSettings = new ProxyAndRecordSettings + { + SaveMapping = true, + SaveMappingToFile = true + }; + + var settings = new WireMockServerSettings + { + FileSystemHandler = fileSystemHandlerMock.Object, + Logger = logger.Object + }; + + var responseBuilder = Response.Create().WithProxy(proxyAndRecordSettings); + + _mappingMock.SetupGet(m => m.Provider).Returns(responseBuilder); + _mappingMock.SetupGet(m => m.Settings).Returns(settings); + + _mappingMock.Setup(m => m.ProvideResponseAsync(It.IsAny())).ReturnsAsync(new ResponseMessage()); + + var requestBuilder = Request.Create().UsingAnyMethod(); + _mappingMock.SetupGet(m => m.RequestMatcher).Returns(requestBuilder); + + var result = new MappingMatcherResult { Mapping = _mappingMock.Object }; + _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((result, result)); + + // Act + await _sut.Invoke(_contextMock.Object); + + // Assert and Verify + fileSystemHandlerMock.Verify(f => f.WriteMappingFile(It.IsAny(), It.IsAny()), Times.Once); + + _mappings.Count.Should().Be(1); + } + + [Fact] + public async void WireMockMiddleware_Invoke_Mapping_Has_ProxyAndRecordSettings_And_SaveMapping_Is_False_But_WireMockServerSettings_SaveMapping_Is_True() + { + // Assign + var request = new RequestMessage(new UrlDetails("http://localhost/foo"), "GET", "::1", null, new Dictionary()); + _requestMapperMock.Setup(m => m.MapAsync(It.IsAny(), It.IsAny())).ReturnsAsync(request); + + _optionsMock.SetupGet(o => o.AuthorizationMatcher).Returns(new ExactMatcher()); + + var fileSystemHandlerMock = new Mock(); + fileSystemHandlerMock.Setup(f => f.GetMappingFolder()).Returns("m"); + + var logger = new Mock(); + + var proxyAndRecordSettings = new ProxyAndRecordSettings + { + SaveMapping = false, + SaveMappingToFile = false + }; + + var settings = new WireMockServerSettings + { + FileSystemHandler = fileSystemHandlerMock.Object, + Logger = logger.Object, + ProxyAndRecordSettings = new ProxyAndRecordSettings + { + SaveMapping = true, + SaveMappingToFile = true + } + }; + + var responseBuilder = Response.Create().WithProxy(proxyAndRecordSettings); + + _mappingMock.SetupGet(m => m.Provider).Returns(responseBuilder); + _mappingMock.SetupGet(m => m.Settings).Returns(settings); + + _mappingMock.Setup(m => m.ProvideResponseAsync(It.IsAny())).ReturnsAsync(new ResponseMessage()); + + var requestBuilder = Request.Create().UsingAnyMethod(); + _mappingMock.SetupGet(m => m.RequestMatcher).Returns(requestBuilder); + + var result = new MappingMatcherResult { Mapping = _mappingMock.Object }; + _matcherMock.Setup(m => m.FindBestMatch(It.IsAny())).Returns((result, result)); + + // Act + await _sut.Invoke(_contextMock.Object); + + // Assert and Verify + fileSystemHandlerMock.Verify(f => f.WriteMappingFile(It.IsAny(), It.IsAny()), Times.Once); + + _mappings.Count.Should().Be(1); + } + } } \ No newline at end of file