diff --git a/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs b/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs index ed512adaf..e060dc35f 100644 --- a/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs +++ b/examples/WireMock.Net.StandAlone.NETCoreApp/Program.cs @@ -1,40 +1,40 @@ -using System; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Threading; -using log4net; -using log4net.Config; -using log4net.Repository; -using WireMock.RequestBuilders; -using WireMock.ResponseBuilders; -using WireMock.Server; -using WireMock.Settings; +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Threading; +using log4net; +using log4net.Config; +using log4net.Repository; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using WireMock.Settings; using WireMock.Util; - -namespace WireMock.Net.StandAlone.NETCoreApp -{ - static class Program - { - private static readonly ILoggerRepository LogRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); - // private static readonly ILog Log = LogManager.GetLogger(typeof(Program)); - - private static int sleepTime = 30000; - private static WireMockServer _server; - - static void Main(string[] args) - { - XmlConfigurator.Configure(LogRepository, new FileInfo("log4net.config")); - + +namespace WireMock.Net.StandAlone.NETCoreApp +{ + static class Program + { + private static readonly ILoggerRepository LogRepository = LogManager.GetRepository(Assembly.GetEntryAssembly()); + // private static readonly ILog Log = LogManager.GetLogger(typeof(Program)); + + private static int sleepTime = 30000; + private static WireMockServer _server; + + static void Main(string[] args) + { + XmlConfigurator.Configure(LogRepository, new FileInfo("log4net.config")); + if (!WireMockServerSettingsParser.TryParseArguments(args, out var settings, new WireMockLog4NetLogger())) { return; } - settings.Logger.Debug("WireMock.Net server arguments [{0}]", string.Join(", ", args.Select(a => $"'{a}'"))); - - _server = WireMockServer.Start(settings); - + settings.Logger.Debug("WireMock.Net server arguments [{0}]", string.Join(", ", args.Select(a => $"'{a}'"))); + + _server = WireMockServer.Start(settings); + _server.Given(Request.Create().WithPath("/api/sap") .UsingPost() .WithBody((IBodyData xmlData) => { @@ -43,37 +43,37 @@ static void Main(string[] args) })) .RespondWith(Response.Create().WithStatusCode(System.Net.HttpStatusCode.OK)); - _server - .Given(Request.Create() - .UsingAnyMethod()) - .RespondWith(Response.Create() - .WithTransformer() - .WithBody("{{Random Type=\"Integer\" Min=100 Max=999999}} {{DateTime.Now}} {{DateTime.Now \"yyyy-MMM\"}} {{String.Format (DateTime.Now) \"MMM-dd\"}}")); - - Console.WriteLine($"{DateTime.UtcNow} Press Ctrl+C to shut down"); - - Console.CancelKeyPress += (s, e) => - { - Stop("CancelKeyPress"); - }; - - System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += ctx => - { - Stop("AssemblyLoadContext.Default.Unloading"); - }; - - while (true) - { - Console.WriteLine($"{DateTime.UtcNow} WireMock.Net server running : {_server.IsStarted}"); - Thread.Sleep(sleepTime); - } - } - - private static void Stop(string why) - { - Console.WriteLine($"{DateTime.UtcNow} WireMock.Net server stopping because '{why}'"); - _server.Stop(); - Console.WriteLine($"{DateTime.UtcNow} WireMock.Net server stopped"); - } - } + _server + .Given(Request.Create() + .UsingAnyMethod()) + .RespondWith(Response.Create() + .WithTransformer() + .WithBody("{{Random Type=\"Integer\" Min=100 Max=999999}} {{DateTime.Now}} {{DateTime.Now \"yyyy-MMM\"}} {{String.Format (DateTime.Now) \"MMM-dd\"}}")); + + Console.WriteLine($"{DateTime.UtcNow} Press Ctrl+C to shut down"); + + Console.CancelKeyPress += (s, e) => + { + Stop("CancelKeyPress"); + }; + + System.Runtime.Loader.AssemblyLoadContext.Default.Unloading += ctx => + { + Stop("AssemblyLoadContext.Default.Unloading"); + }; + + while (true) + { + Console.WriteLine($"{DateTime.UtcNow} WireMock.Net server running : {_server.IsStarted}"); + Thread.Sleep(sleepTime); + } + } + + private static void Stop(string why) + { + Console.WriteLine($"{DateTime.UtcNow} WireMock.Net server stopping because '{why}'"); + _server.Stop(); + Console.WriteLine($"{DateTime.UtcNow} WireMock.Net server stopped"); + } + } } \ No newline at end of file diff --git a/examples/WireMock.Net.StandAlone.NETCoreApp/Properties/launchSettings.json b/examples/WireMock.Net.StandAlone.NETCoreApp/Properties/launchSettings.json index 89c5b6b82..1adeba280 100644 --- a/examples/WireMock.Net.StandAlone.NETCoreApp/Properties/launchSettings.json +++ b/examples/WireMock.Net.StandAlone.NETCoreApp/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "WireMock.Net.StandAlone.NETCoreApp": { "commandName": "Project", - "commandLineArgs": "--Urls https://localhost:10080 --WireMockLogger WireMockConsoleLogger" + "commandLineArgs": "--Urls http://localhost:9091 --WireMockLogger WireMockConsoleLogger" } } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs index f5eb8afe8..b1d5274fd 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/MappingModel.cs @@ -52,5 +52,10 @@ public class MappingModel /// Saves this mapping as a static mapping file. /// public bool? SaveToFile { get; set; } + + /// + /// The Webhook. + /// + public WebhookModel Webhook { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs index 28035a8d6..6aab6dc51 100644 --- a/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/ResponseModel.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using WireMock.Types; namespace WireMock.Admin.Mappings { diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/WebhookModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/WebhookModel.cs new file mode 100644 index 000000000..ab6a51bca --- /dev/null +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/WebhookModel.cs @@ -0,0 +1,13 @@ +namespace WireMock.Admin.Mappings +{ + /// + /// The Webhook + /// + public class WebhookModel + { + /// + /// The Webhook Request. + /// + public WebhookRequestModel Request { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Admin/Mappings/WebhookRequestModel.cs b/src/WireMock.Net.Abstractions/Admin/Mappings/WebhookRequestModel.cs new file mode 100644 index 000000000..05e0f4e38 --- /dev/null +++ b/src/WireMock.Net.Abstractions/Admin/Mappings/WebhookRequestModel.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; + +namespace WireMock.Admin.Mappings +{ + /// + /// RequestModel + /// + public class WebhookRequestModel + { + /// + /// Gets or sets the Url. + /// + public string Url { get; set; } + + /// + /// The methods + /// + public string Method { get; set; } + + /// + /// Gets or sets the headers. + /// + public IDictionary Headers { get; set; } + + /// + /// Gets or sets the body. + /// + public string Body { get; set; } + + /// + /// Gets or sets the body (as JSON object). + /// + public object BodyAsJson { get; set; } + + /// + /// Use ResponseMessage Transformer. + /// + public bool? UseTransformer { get; set; } + + /// + /// Gets the type of the transformer. + /// + public string TransformerType { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Util/IBodyData.cs b/src/WireMock.Net.Abstractions/Models/IBodyData.cs similarity index 100% rename from src/WireMock.Net.Abstractions/Util/IBodyData.cs rename to src/WireMock.Net.Abstractions/Models/IBodyData.cs diff --git a/src/WireMock.Net.Abstractions/Models/IWebhook.cs b/src/WireMock.Net.Abstractions/Models/IWebhook.cs new file mode 100644 index 000000000..67174d9eb --- /dev/null +++ b/src/WireMock.Net.Abstractions/Models/IWebhook.cs @@ -0,0 +1,13 @@ +namespace WireMock.Models +{ + /// + /// IWebhook + /// + public interface IWebhook + { + /// + /// Request + /// + IWebhookRequest Request { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net.Abstractions/Models/IWebhookRequest.cs b/src/WireMock.Net.Abstractions/Models/IWebhookRequest.cs new file mode 100644 index 000000000..bfd4f07fd --- /dev/null +++ b/src/WireMock.Net.Abstractions/Models/IWebhookRequest.cs @@ -0,0 +1,42 @@ +using System.Collections.Generic; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Models +{ + /// + /// IWebhookRequest + /// + public interface IWebhookRequest + { + /// + /// The Webhook Url. + /// + string Url { get; set; } + + /// + /// The method to use. + /// + string Method { get; set; } + + /// + /// The Headers to send. + /// + IDictionary> Headers { get; } + + /// + /// The body to send. + /// + IBodyData BodyData { get; set; } + + /// + /// Use Transformer. + /// + bool? UseTransformer { get; set; } + + /// + /// The transformer type. + /// + TransformerType TransformerType { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Http/HttpClientBuilder.cs b/src/WireMock.Net/Http/HttpClientBuilder.cs index 00b2dea22..67dbebf14 100644 --- a/src/WireMock.Net/Http/HttpClientBuilder.cs +++ b/src/WireMock.Net/Http/HttpClientBuilder.cs @@ -7,7 +7,7 @@ namespace WireMock.Http { internal static class HttpClientBuilder { - public static HttpClient Build(IProxyAndRecordSettings settings) + public static HttpClient Build(IHttpClientSettings settings) { #if NETSTANDARD || NETCOREAPP3_1 || NET5_0 var handler = new HttpClientHandler diff --git a/src/WireMock.Net/Http/WebhookSender.cs b/src/WireMock.Net/Http/WebhookSender.cs new file mode 100644 index 000000000..bcd065983 --- /dev/null +++ b/src/WireMock.Net/Http/WebhookSender.cs @@ -0,0 +1,85 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using JetBrains.Annotations; +using WireMock.Models; +using WireMock.Settings; +using WireMock.Transformers; +using WireMock.Transformers.Handlebars; +using WireMock.Transformers.Scriban; +using WireMock.Types; +using WireMock.Util; +using WireMock.Validation; + +namespace WireMock.Http +{ + internal class WebhookSender + { + private const string ClientIp = "::1"; + + private readonly IWireMockServerSettings _settings; + + public WebhookSender(IWireMockServerSettings settings) + { + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + } + + public Task SendAsync([NotNull] HttpClient client, [NotNull] IWebhookRequest request, [NotNull] RequestMessage originalRequestMessage, [NotNull] ResponseMessage originalResponseMessage) + { + Check.NotNull(client, nameof(client)); + Check.NotNull(request, nameof(request)); + Check.NotNull(originalRequestMessage, nameof(originalRequestMessage)); + Check.NotNull(originalResponseMessage, nameof(originalResponseMessage)); + + IBodyData bodyData; + IDictionary> headers; + if (request.UseTransformer == true) + { + ITransformer responseMessageTransformer; + switch (request.TransformerType) + { + case TransformerType.Handlebars: + var factoryHandlebars = new HandlebarsContextFactory(_settings.FileSystemHandler, _settings.HandlebarsRegistrationCallback); + responseMessageTransformer = new Transformer(factoryHandlebars); + break; + + case TransformerType.Scriban: + case TransformerType.ScribanDotLiquid: + var factoryDotLiquid = new ScribanContextFactory(_settings.FileSystemHandler, request.TransformerType); + responseMessageTransformer = new Transformer(factoryDotLiquid); + break; + + default: + throw new NotImplementedException($"TransformerType '{request.TransformerType}' is not supported."); + } + + (bodyData, headers) = responseMessageTransformer.Transform(originalRequestMessage, originalResponseMessage, request.BodyData, request.Headers); + } + else + { + bodyData = request.BodyData; + headers = request.Headers; + } + + // Create RequestMessage + var requestMessage = new RequestMessage( + new UrlDetails(request.Url), + request.Method, + ClientIp, + bodyData, + headers?.ToDictionary(x => x.Key, x => x.Value.ToArray()) + ) + { + DateTime = DateTime.UtcNow + }; + + // Create HttpRequestMessage + var httpRequestMessage = HttpRequestMessageHelper.Create(requestMessage, request.Url); + + // Call the URL + return client.SendAsync(httpRequestMessage); + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/IMapping.cs b/src/WireMock.Net/IMapping.cs index 6cdcf0e27..f7816a1e8 100644 --- a/src/WireMock.Net/IMapping.cs +++ b/src/WireMock.Net/IMapping.cs @@ -2,6 +2,7 @@ using System; using System.Threading.Tasks; using WireMock.Matchers.Request; +using WireMock.Models; using WireMock.ResponseProviders; using WireMock.Settings; @@ -93,6 +94,11 @@ public interface IMapping /// bool LogMapping { get; } + /// + /// The Webhook. + /// + IWebhook Webhook { get; } + /// /// ProvideResponseAsync /// diff --git a/src/WireMock.Net/Mapping.cs b/src/WireMock.Net/Mapping.cs index 9952f4ef6..031c027a3 100644 --- a/src/WireMock.Net/Mapping.cs +++ b/src/WireMock.Net/Mapping.cs @@ -1,7 +1,9 @@ using System; using System.Threading.Tasks; using JetBrains.Annotations; +using WireMock.Admin.Mappings; using WireMock.Matchers.Request; +using WireMock.Models; using WireMock.ResponseProviders; using WireMock.Settings; @@ -54,6 +56,9 @@ public class Mapping : IMapping /// public bool LogMapping => !(Provider is DynamicResponseProvider || Provider is DynamicAsyncResponseProvider); + /// + public IWebhook Webhook { get; } + /// /// Initializes a new instance of the class. /// @@ -68,6 +73,7 @@ public class Mapping : IMapping /// 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] + /// The Webhook. [Optional] public Mapping( Guid guid, [CanBeNull] string title, @@ -79,7 +85,8 @@ public Mapping( [CanBeNull] string scenario, [CanBeNull] string executionConditionState, [CanBeNull] string nextState, - [CanBeNull] int? stateTimes) + [CanBeNull] int? stateTimes, + [CanBeNull] IWebhook webhook) { Guid = guid; Title = title; @@ -92,6 +99,7 @@ public Mapping( ExecutionConditionState = executionConditionState; NextState = nextState; StateTimes = stateTimes; + Webhook = webhook; } /// diff --git a/src/WireMock.Net/Util/BodyData.cs b/src/WireMock.Net/Models/BodyData.cs similarity index 96% rename from src/WireMock.Net/Util/BodyData.cs rename to src/WireMock.Net/Models/BodyData.cs index 1e7d54e00..494465e50 100644 --- a/src/WireMock.Net/Util/BodyData.cs +++ b/src/WireMock.Net/Models/BodyData.cs @@ -6,7 +6,7 @@ namespace WireMock.Util /// /// BodyData /// - public class BodyData : IBodyData + internal class BodyData : IBodyData { /// public Encoding Encoding { get; set; } diff --git a/src/WireMock.Net/Models/Webhook.cs b/src/WireMock.Net/Models/Webhook.cs new file mode 100644 index 000000000..b5583d24a --- /dev/null +++ b/src/WireMock.Net/Models/Webhook.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; +using JetBrains.Annotations; +using WireMock.Types; + +namespace WireMock.Models +{ + /// + /// Webhook + /// + public class Webhook : IWebhook + { + /// + public IWebhookRequest Request { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Models/WebhookRequest.cs b/src/WireMock.Net/Models/WebhookRequest.cs new file mode 100644 index 000000000..077d84faa --- /dev/null +++ b/src/WireMock.Net/Models/WebhookRequest.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Models +{ + /// + /// WebhookRequest + /// + public class WebhookRequest : IWebhookRequest + { + /// + public string Url { get; set; } + + /// + public string Method { get; set; } + + /// + public IDictionary> Headers { get; set; } + + /// + public IBodyData BodyData { get; set; } + + /// + public bool? UseTransformer { get; set; } + + /// + public TransformerType TransformerType { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs b/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs index 49f8b5389..79a485dcc 100644 --- a/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs +++ b/src/WireMock.Net/Owin/Mappers/OwinRequestMapper.cs @@ -53,7 +53,7 @@ public async Task MapAsync(IRequest request, IWireMockMiddleware } } - BodyData body = null; + IBodyData body = null; if (request.Body != null && BodyParser.ShouldParseBody(method, options.AllowBodyForAllHttpMethods == true)) { var bodyParserSettings = new BodyParserSettings diff --git a/src/WireMock.Net/Owin/WireMockMiddleware.cs b/src/WireMock.Net/Owin/WireMockMiddleware.cs index 274bdd0df..e5b9eb925 100644 --- a/src/WireMock.Net/Owin/WireMockMiddleware.cs +++ b/src/WireMock.Net/Owin/WireMockMiddleware.cs @@ -9,6 +9,7 @@ using WireMock.Types; using WireMock.Validation; using WireMock.ResponseBuilders; +using WireMock.Settings; #if !USE_ASPNETCORE using Microsoft.Owin; using IContext = Microsoft.Owin.IOwinContext; @@ -153,6 +154,11 @@ private async Task InvokeInternal(IContext ctx) { UpdateScenarioState(targetMapping); } + + if (!targetMapping.IsAdminInterface && targetMapping.Webhook != null) + { + await SendToWebhookAsync(targetMapping, request, response).ConfigureAwait(false); + } } catch (Exception ex) { @@ -184,6 +190,21 @@ private async Task InvokeInternal(IContext ctx) await CompletedTask; } + private async Task SendToWebhookAsync(IMapping mapping, RequestMessage request, ResponseMessage response) + { + var httpClientForWebhook = HttpClientBuilder.Build(mapping.Settings.WebhookSettings ?? new WebhookSettings()); + var webhookSender = new WebhookSender(mapping.Settings); + + try + { + await webhookSender.SendAsync(httpClientForWebhook, mapping.Webhook.Request, request, response).ConfigureAwait(false); + } + catch (Exception ex) + { + _options.Logger.Error($"Sending message to Webhook Mapping '{mapping.Guid}' failed. Exception: {ex}"); + } + } + private void UpdateScenarioState(IMapping mapping) { var scenario = _options.Scenarios[mapping.Scenario]; diff --git a/src/WireMock.Net/Proxy/ProxyHelper.cs b/src/WireMock.Net/Proxy/ProxyHelper.cs index f88852408..96d12519f 100644 --- a/src/WireMock.Net/Proxy/ProxyHelper.cs +++ b/src/WireMock.Net/Proxy/ProxyHelper.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Net.Http; using System.Threading.Tasks; -using HandlebarsDotNet.Helpers.Validation; using JetBrains.Annotations; using WireMock.Http; using WireMock.Matchers; @@ -106,7 +105,7 @@ private IMapping ToMapping(IProxyAndRecordSettings proxyAndRecordSettings, Reque var response = Response.Create(responseMessage); - return new Mapping(Guid.NewGuid(), string.Empty, null, _settings, request, response, 0, null, null, null, null); + return new Mapping(Guid.NewGuid(), string.Empty, null, _settings, request, response, 0, null, null, null, null, null); } } } \ No newline at end of file diff --git a/src/WireMock.Net/RequestMessage.cs b/src/WireMock.Net/RequestMessage.cs index 85edef65b..fe3be8ea6 100644 --- a/src/WireMock.Net/RequestMessage.cs +++ b/src/WireMock.Net/RequestMessage.cs @@ -101,7 +101,7 @@ public class RequestMessage : IRequestMessage /// The BodyData. /// The headers. /// The cookies. - public RequestMessage([NotNull] UrlDetails urlDetails, [NotNull] string method, [NotNull] string clientIP, [CanBeNull] BodyData bodyData = null, [CanBeNull] IDictionary headers = null, [CanBeNull] IDictionary cookies = null) + public RequestMessage([NotNull] UrlDetails urlDetails, [NotNull] string method, [NotNull] string clientIP, [CanBeNull] IBodyData bodyData = null, [CanBeNull] IDictionary headers = null, [CanBeNull] IDictionary cookies = null) { Check.NotNull(urlDetails, nameof(urlDetails)); Check.NotNull(method, nameof(method)); diff --git a/src/WireMock.Net/Serialization/MappingConverter.cs b/src/WireMock.Net/Serialization/MappingConverter.cs index 844885d2d..cc08c1614 100644 --- a/src/WireMock.Net/Serialization/MappingConverter.cs +++ b/src/WireMock.Net/Serialization/MappingConverter.cs @@ -84,7 +84,8 @@ public MappingModel ToMappingModel(IMapping mapping) Response = new ResponseModel { Delay = (int?)response.Delay?.TotalMilliseconds - } + }, + Webhook = WebhookMapper.Map(mapping.Webhook) }; if (bodyMatcher?.Matchers != null) diff --git a/src/WireMock.Net/Serialization/WebhookMapper.cs b/src/WireMock.Net/Serialization/WebhookMapper.cs new file mode 100644 index 000000000..08f19f144 --- /dev/null +++ b/src/WireMock.Net/Serialization/WebhookMapper.cs @@ -0,0 +1,103 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using WireMock.Admin.Mappings; +using WireMock.Http; +using WireMock.Models; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Serialization +{ + internal static class WebhookMapper + { + public static IWebhook Map(WebhookModel model) + { + var webhook = new Webhook + { + Request = new WebhookRequest + { + Url = model.Request.Url, + Method = model.Request.Method, + Headers = model.Request.Headers?.ToDictionary(x => x.Key, x => new WireMockList(x.Value)) ?? new Dictionary>() + } + }; + + if (model.Request.UseTransformer == true) + { + webhook.Request.UseTransformer = true; + if (!Enum.TryParse(model.Request.TransformerType, out var transformerType)) + { + transformerType = TransformerType.Handlebars; + } + webhook.Request.TransformerType = transformerType; + } + + IEnumerable contentTypeHeader = null; + if (webhook.Request.Headers.Any(header => string.Equals(header.Key, HttpKnownHeaderNames.ContentType, StringComparison.OrdinalIgnoreCase))) + { + contentTypeHeader = webhook.Request.Headers.First(header => string.Equals(header.Key, HttpKnownHeaderNames.ContentType, StringComparison.OrdinalIgnoreCase)).Value; + } + + if (model.Request.Body != null) + { + webhook.Request.BodyData = new BodyData + { + BodyAsString = model.Request.Body, + DetectedBodyType = BodyType.String, + DetectedBodyTypeFromContentType = BodyParser.DetectBodyTypeFromContentType(contentTypeHeader?.FirstOrDefault()) + }; + } + else if (model.Request.BodyAsJson != null) + { + webhook.Request.BodyData = new BodyData + { + BodyAsJson = model.Request.BodyAsJson, + DetectedBodyType = BodyType.Json, + DetectedBodyTypeFromContentType = BodyParser.DetectBodyTypeFromContentType(contentTypeHeader?.FirstOrDefault()) + }; + } + + return webhook; + } + + public static WebhookModel Map(IWebhook webhook) + { + if (webhook?.Request == null) + { + return null; + } + + var model = new WebhookModel + { + Request = new WebhookRequestModel + { + Url = webhook.Request.Url, + Method = webhook.Request.Method, + Headers = webhook.Request.Headers?.ToDictionary(x => x.Key, x => x.Value.ToString()), + UseTransformer = webhook.Request.UseTransformer, + TransformerType = webhook.Request.UseTransformer == true ? webhook.Request.TransformerType.ToString() : null + } + }; + + if (webhook.Request.BodyData != null) + { + switch (webhook.Request.BodyData.DetectedBodyType) + { + case BodyType.String: + model.Request.Body = webhook.Request.BodyData.BodyAsString; + break; + + case BodyType.Json: + model.Request.BodyAsJson = webhook.Request.BodyData.BodyAsJson; + break; + + default: + break; + } + } + + return model; + } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Server/IRespondWithAProvider.cs b/src/WireMock.Net/Server/IRespondWithAProvider.cs index 50ea594db..560b3562c 100644 --- a/src/WireMock.Net/Server/IRespondWithAProvider.cs +++ b/src/WireMock.Net/Server/IRespondWithAProvider.cs @@ -1,5 +1,9 @@ using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using WireMock.Models; using WireMock.ResponseProviders; +using WireMock.Types; namespace WireMock.Server { @@ -97,5 +101,50 @@ public interface IRespondWithAProvider /// The number of times this match should be matched before the state will be changed to the specified one. Default value is 1. /// The . IRespondWithAProvider WillSetStateTo(int state, int? times = 1); + + /// + /// Add a Webbook to call after the response has been generated. + /// + /// The Webhook + /// The . + IRespondWithAProvider WithWebhook(IWebhook webhook); + + /// + /// Add a Webbook to call after the response has been generated. + /// + /// The Webhook Url + /// The method to use. [optional] + /// The Headers to send. [optional] + /// The body (as string) to send. [optional] + /// Use Transformer. [optional] + /// The transformer type. [optional] + /// The . + IRespondWithAProvider WithWebhook( + [NotNull] string url, + [CanBeNull] string method = "post", + [CanBeNull] IDictionary> headers = null, + [CanBeNull] string body = null, + bool useTransformer = true, + TransformerType transformerType = TransformerType.Handlebars + ); + + /// + /// Add a Webbook to call after the response has been generated. + /// + /// The Webhook Url + /// The method to use. [optional] + /// The Headers to send. [optional] + /// The body (as json) to send. [optional] + /// Use Transformer. [optional] + /// The transformer type. [optional] + /// The . + IRespondWithAProvider WithWebhook( + [NotNull] string url, + [CanBeNull] string method = "post", + [CanBeNull] IDictionary> headers = null, + [CanBeNull] object body = null, + bool useTransformer = true, + TransformerType transformerType = TransformerType.Handlebars + ); } } \ No newline at end of file diff --git a/src/WireMock.Net/Server/RespondWithAProvider.cs b/src/WireMock.Net/Server/RespondWithAProvider.cs index b2c303d64..59837f8b7 100644 --- a/src/WireMock.Net/Server/RespondWithAProvider.cs +++ b/src/WireMock.Net/Server/RespondWithAProvider.cs @@ -1,10 +1,15 @@ // 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 System; +using System.Collections.Generic; +using JetBrains.Annotations; using WireMock.Matchers.Request; +using WireMock.Models; using WireMock.ResponseProviders; using WireMock.Settings; - +using WireMock.Types; +using WireMock.Util; + namespace WireMock.Server { /// @@ -26,6 +31,8 @@ internal class RespondWithAProvider : IRespondWithAProvider public Guid Guid { get; private set; } = Guid.NewGuid(); + public IWebhook Webhook { get; private set; } + /// /// Initializes a new instance of the class. /// @@ -47,7 +54,7 @@ public RespondWithAProvider(RegistrationCallback registrationCallback, IRequestM /// The provider. public void RespondWith(IResponseProvider provider) { - _registrationCallback(new Mapping(Guid, _title, _path, _settings, _requestMatcher, provider, _priority, _scenario, _executionConditionState, _nextState, _timesInSameState), _saveToFile); + _registrationCallback(new Mapping(Guid, _title, _path, _settings, _requestMatcher, provider, _priority, _scenario, _executionConditionState, _nextState, _timesInSameState, Webhook), _saveToFile); } /// @@ -140,5 +147,81 @@ public IRespondWithAProvider WillSetStateTo(int state, int? times = 1) { return WillSetStateTo(state.ToString(), times); } + + /// + public IRespondWithAProvider WithWebhook(IWebhook webhook) + { + Webhook = webhook; + + return this; + } + + /// + public IRespondWithAProvider WithWebhook( + [NotNull] string url, + [CanBeNull] string method = "post", + [CanBeNull] IDictionary> headers = null, + [CanBeNull] string body = null, + bool useTransformer = true, + TransformerType transformerType = TransformerType.Handlebars) + { + Webhook = InitWebhook(url, method, headers, useTransformer, transformerType); + + if (body != null) + { + Webhook.Request.BodyData = new BodyData + { + BodyAsString = body, + DetectedBodyType = BodyType.String, + DetectedBodyTypeFromContentType = BodyType.String + }; + } + + return this; + } + + /// + public IRespondWithAProvider WithWebhook( + [NotNull] string url, + [CanBeNull] string method = "post", + [CanBeNull] IDictionary> headers = null, + [CanBeNull] object body = null, + bool useTransformer = true, + TransformerType transformerType = TransformerType.Handlebars) + { + Webhook = InitWebhook(url, method, headers, useTransformer, transformerType); + + if (body != null) + { + Webhook.Request.BodyData = new BodyData + { + BodyAsJson = body, + DetectedBodyType = BodyType.Json, + DetectedBodyTypeFromContentType = BodyType.Json + }; + } + + return this; + } + + private IWebhook InitWebhook( + string url, + string method, + IDictionary> headers, + bool useTransformer, + TransformerType transformerType) + { + return new Webhook + { + Request = new WebhookRequest + { + Url = url, + Method = method ?? "post", + Headers = headers, + UseTransformer = useTransformer, + TransformerType = transformerType + } + }; + } } } \ 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 3c9a82b51..9087a4ed8 100644 --- a/src/WireMock.Net/Server/WireMockServer.Admin.cs +++ b/src/WireMock.Net/Server/WireMockServer.Admin.cs @@ -470,6 +470,11 @@ private ResponseMessage MappingsPost(RequestMessage requestMessage) respondProvider = respondProvider.WillSetStateTo(mappingModel.SetStateTo); } + if (mappingModel.Webhook?.Request != null) + { + respondProvider = respondProvider.WithWebhook(WebhookMapper.Map(mappingModel.Webhook)); + } + respondProvider.RespondWith(responseBuilder); return respondProvider.Guid; diff --git a/src/WireMock.Net/Settings/HttpClientSettings.cs b/src/WireMock.Net/Settings/HttpClientSettings.cs new file mode 100644 index 000000000..ab9e20500 --- /dev/null +++ b/src/WireMock.Net/Settings/HttpClientSettings.cs @@ -0,0 +1,17 @@ +namespace WireMock.Settings +{ + /// + /// HttpClientSettings + /// + public class HttpClientSettings : IHttpClientSettings + { + /// + public string ClientX509Certificate2ThumbprintOrSubjectName { get; set; } + + /// + public IWebProxySettings WebProxySettings { get; set; } + + /// + public bool? AllowAutoRedirect { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Settings/IHttpClientSettings.cs b/src/WireMock.Net/Settings/IHttpClientSettings.cs new file mode 100644 index 000000000..82a9d9c2f --- /dev/null +++ b/src/WireMock.Net/Settings/IHttpClientSettings.cs @@ -0,0 +1,24 @@ +namespace WireMock.Settings +{ + /// + /// IHttpClientSettings + /// + public interface IHttpClientSettings + { + /// + /// The clientCertificate thumbprint or subject name fragment to use. + /// Example thumbprint : "D2DBF135A8D06ACCD0E1FAD9BFB28678DF7A9818". Example subject name: "www.google.com"" + /// + string ClientX509Certificate2ThumbprintOrSubjectName { get; set; } + + /// + /// Defines the WebProxySettings. + /// + IWebProxySettings WebProxySettings { get; set; } + + /// + /// Proxy requests should follow redirection (30x). + /// + bool? AllowAutoRedirect { get; set; } + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Settings/IProxyAndRecordSettings.cs b/src/WireMock.Net/Settings/IProxyAndRecordSettings.cs index 2426cf2c7..2e10128e4 100644 --- a/src/WireMock.Net/Settings/IProxyAndRecordSettings.cs +++ b/src/WireMock.Net/Settings/IProxyAndRecordSettings.cs @@ -5,7 +5,7 @@ namespace WireMock.Settings /// /// IProxyAndRecordSettings /// - public interface IProxyAndRecordSettings + public interface IProxyAndRecordSettings : IHttpClientSettings { /// /// The URL to proxy. @@ -29,12 +29,6 @@ public interface IProxyAndRecordSettings /// bool SaveMappingToFile { get; set; } - /// - /// The clientCertificate thumbprint or subject name fragment to use. - /// Example thumbprint : "D2DBF135A8D06ACCD0E1FAD9BFB28678DF7A9818". Example subject name: "www.google.com"" - /// - string ClientX509Certificate2ThumbprintOrSubjectName { get; set; } - /// /// Defines a list from headers which will be excluded from the saved mappings. /// @@ -44,15 +38,5 @@ public interface IProxyAndRecordSettings /// Defines a list of cookies which will be excluded from the saved mappings. /// string[] ExcludedCookies { get; set; } - - /// - /// Defines the WebProxySettings. - /// - IWebProxySettings WebProxySettings { get; set; } - - /// - /// Proxy requests should follow redirection (30x). - /// - bool? AllowAutoRedirect { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/IWebhookSettings.cs b/src/WireMock.Net/Settings/IWebhookSettings.cs new file mode 100644 index 000000000..ea4a5777c --- /dev/null +++ b/src/WireMock.Net/Settings/IWebhookSettings.cs @@ -0,0 +1,9 @@ +namespace WireMock.Settings +{ + /// + /// IWebhookSettings + /// + public interface IWebhookSettings : IHttpClientSettings + { + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Settings/IWireMockServerSettings.cs b/src/WireMock.Net/Settings/IWireMockServerSettings.cs index ac511676d..d70e0411e 100644 --- a/src/WireMock.Net/Settings/IWireMockServerSettings.cs +++ b/src/WireMock.Net/Settings/IWireMockServerSettings.cs @@ -186,5 +186,11 @@ public interface IWireMockServerSettings /// [PublicAPI] bool CustomCertificateDefined { get; } + + /// + /// Defines the global IWebhookSettingsto use + /// + [PublicAPI] + IWebhookSettings WebhookSettings { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs index cc797b3e1..1e3f57f93 100644 --- a/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs +++ b/src/WireMock.Net/Settings/ProxyAndRecordSettings.cs @@ -5,7 +5,7 @@ namespace WireMock.Settings /// /// ProxyAndRecordSettings /// - public class ProxyAndRecordSettings : IProxyAndRecordSettings + public class ProxyAndRecordSettings : HttpClientSettings, IProxyAndRecordSettings { /// /// The URL to proxy. @@ -32,13 +32,6 @@ public class ProxyAndRecordSettings : IProxyAndRecordSettings [PublicAPI] public string SaveMappingForStatusCodePattern { get; set; } = "*"; - /// - /// The clientCertificate thumbprint or subject name fragment to use. - /// Example thumbprint : "D2DBF135A8D06ACCD0E1FAD9BFB28678DF7A9818". Example subject name: "www.google.com"" - /// - [PublicAPI] - public string ClientX509Certificate2ThumbprintOrSubjectName { get; set; } - /// [PublicAPI] public string[] ExcludedHeaders { get; set; } @@ -46,13 +39,5 @@ public class ProxyAndRecordSettings : IProxyAndRecordSettings /// [PublicAPI] public string[] ExcludedCookies { get; set; } - - /// - [PublicAPI] - public IWebProxySettings WebProxySettings { get; set; } - - /// - [PublicAPI] - public bool? AllowAutoRedirect { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WebhookSettings.cs b/src/WireMock.Net/Settings/WebhookSettings.cs new file mode 100644 index 000000000..f0689d91e --- /dev/null +++ b/src/WireMock.Net/Settings/WebhookSettings.cs @@ -0,0 +1,9 @@ +namespace WireMock.Settings +{ + /// + /// WebhookSettings + /// + public class WebhookSettings : HttpClientSettings, IWebhookSettings + { + } +} \ No newline at end of file diff --git a/src/WireMock.Net/Settings/WireMockServerSettings.cs b/src/WireMock.Net/Settings/WireMockServerSettings.cs index 1f655c4cf..1074a64cd 100644 --- a/src/WireMock.Net/Settings/WireMockServerSettings.cs +++ b/src/WireMock.Net/Settings/WireMockServerSettings.cs @@ -129,5 +129,9 @@ public class WireMockServerSettings : IWireMockServerSettings /// [PublicAPI] public bool CustomCertificateDefined => CertificateSettings?.IsDefined == true; + + /// + [PublicAPI] + public IWebhookSettings WebhookSettings { get; set; } } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/ITransformer.cs b/src/WireMock.Net/Transformers/ITransformer.cs index e6e4d5408..ebcd3336a 100644 --- a/src/WireMock.Net/Transformers/ITransformer.cs +++ b/src/WireMock.Net/Transformers/ITransformer.cs @@ -1,7 +1,13 @@ -namespace WireMock.Transformers +using System.Collections.Generic; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Transformers { interface ITransformer { ResponseMessage Transform(RequestMessage requestMessage, ResponseMessage original, bool useTransformerForBodyAsFile); + + (IBodyData BodyData, IDictionary> Headers) Transform(RequestMessage originalRequestMessage, ResponseMessage originalResponseMessage, IBodyData bodyData, IDictionary> headers); } } \ No newline at end of file diff --git a/src/WireMock.Net/Transformers/Transformer.cs b/src/WireMock.Net/Transformers/Transformer.cs index 0acedc2d2..901590f2d 100644 --- a/src/WireMock.Net/Transformers/Transformer.cs +++ b/src/WireMock.Net/Transformers/Transformer.cs @@ -1,227 +1,265 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using JetBrains.Annotations; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using WireMock.Types; -using WireMock.Util; -using WireMock.Validation; - -namespace WireMock.Transformers.Handlebars -{ - internal class Transformer : ITransformer - { - private readonly ITransformerContextFactory _factory; - - public Transformer([NotNull] ITransformerContextFactory factory) - { - Check.NotNull(factory, nameof(factory)); - - _factory = factory; - } - - public ResponseMessage Transform(RequestMessage requestMessage, ResponseMessage original, bool useTransformerForBodyAsFile) - { - var handlebarsContext = _factory.Create(); - - var responseMessage = new ResponseMessage(); - - var model = new { request = requestMessage }; - - switch (original.BodyData?.DetectedBodyType) - { - case BodyType.Json: - TransformBodyAsJson(handlebarsContext, model, original, responseMessage); - break; - - case BodyType.File: - TransformBodyAsFile(handlebarsContext, model, original, responseMessage, useTransformerForBodyAsFile); - break; - - case BodyType.String: - responseMessage.BodyOriginal = original.BodyData.BodyAsString; - TransformBodyAsString(handlebarsContext, model, original, responseMessage); - break; - } - - responseMessage.FaultType = original.FaultType; - responseMessage.FaultPercentage = original.FaultPercentage; - - // Headers - var newHeaders = new Dictionary>(); - foreach (var header in original.Headers) - { - var headerKey = handlebarsContext.ParseAndRender(header.Key, model); - var templateHeaderValues = header.Value - .Select(text => handlebarsContext.ParseAndRender(text, model)) - .ToArray(); - - newHeaders.Add(headerKey, new WireMockList(templateHeaderValues)); - } - - responseMessage.Headers = newHeaders; - - switch (original.StatusCode) - { - case int statusCodeAsInteger: - responseMessage.StatusCode = statusCodeAsInteger; - break; - - case string statusCodeAsString: - responseMessage.StatusCode = handlebarsContext.ParseAndRender(statusCodeAsString, model); - break; - } - - return responseMessage; - } - - private static void TransformBodyAsJson(ITransformerContext handlebarsContext, object model, ResponseMessage original, ResponseMessage responseMessage) - { - JToken jToken; - switch (original.BodyData.BodyAsJson) - { - case JObject bodyAsJObject: - jToken = bodyAsJObject.DeepClone(); - WalkNode(handlebarsContext, jToken, model); - break; - - case Array bodyAsArray: - jToken = JArray.FromObject(bodyAsArray); - WalkNode(handlebarsContext, jToken, model); - break; - - case string bodyAsString: - jToken = ReplaceSingleNode(handlebarsContext, bodyAsString, model); - break; - - default: - jToken = JObject.FromObject(original.BodyData.BodyAsJson); - WalkNode(handlebarsContext, jToken, model); - break; - } - - responseMessage.BodyData = new BodyData - { - Encoding = original.BodyData.Encoding, - DetectedBodyType = original.BodyData.DetectedBodyType, - DetectedBodyTypeFromContentType = original.BodyData.DetectedBodyTypeFromContentType, - BodyAsJson = jToken - }; - } - - private static JToken ReplaceSingleNode(ITransformerContext handlebarsContext, string stringValue, object model) - { - string transformedString = handlebarsContext.ParseAndRender(stringValue, model) as string; - - if (!string.Equals(stringValue, transformedString)) - { - const string property = "_"; - JObject dummy = JObject.Parse($"{{ \"{property}\": null }}"); - JToken node = dummy[property]; - - ReplaceNodeValue(node, transformedString); - - return dummy[property]; - } - - return stringValue; - } - - private static void WalkNode(ITransformerContext handlebarsContext, JToken node, object model) - { - if (node.Type == JTokenType.Object) - { - // In case of Object, loop all children. Do a ToArray() to avoid `Collection was modified` exceptions. - foreach (JProperty child in node.Children().ToArray()) - { - WalkNode(handlebarsContext, child.Value, model); - } - } - else if (node.Type == JTokenType.Array) - { - // In case of Array, loop all items. Do a ToArray() to avoid `Collection was modified` exceptions. - foreach (JToken child in node.Children().ToArray()) - { - WalkNode(handlebarsContext, child, model); - } - } - else if (node.Type == JTokenType.String) - { - // In case of string, try to transform the value. - string stringValue = node.Value(); - if (string.IsNullOrEmpty(stringValue)) - { - return; - } - - string transformedString = handlebarsContext.ParseAndRender(stringValue, model); - if (!string.Equals(stringValue, transformedString)) - { - ReplaceNodeValue(node, transformedString); - } - } - } - - private static void ReplaceNodeValue(JToken node, string stringValue) - { - if (bool.TryParse(stringValue, out bool valueAsBoolean)) - { - node.Replace(valueAsBoolean); - return; - } - - JToken value; - try - { - // Try to convert this string into a JsonObject - value = JToken.Parse(stringValue); - } - catch (JsonException) - { - // Ignore JsonException and just keep string value and convert to JToken - value = stringValue; - } - - node.Replace(value); - } - - private static void TransformBodyAsString(ITransformerContext handlebarsContext, object model, ResponseMessage original, ResponseMessage responseMessage) - { - responseMessage.BodyData = new BodyData - { - Encoding = original.BodyData.Encoding, - DetectedBodyType = original.BodyData.DetectedBodyType, - DetectedBodyTypeFromContentType = original.BodyData.DetectedBodyTypeFromContentType, - BodyAsString = handlebarsContext.ParseAndRender(original.BodyData.BodyAsString, model) - }; - } - - private void TransformBodyAsFile(ITransformerContext handlebarsContext, object model, ResponseMessage original, ResponseMessage responseMessage, bool useTransformerForBodyAsFile) - { - string transformedBodyAsFilename = handlebarsContext.ParseAndRender(original.BodyData.BodyAsFile, model); - - if (!useTransformerForBodyAsFile) - { - responseMessage.BodyData = new BodyData - { - DetectedBodyType = original.BodyData.DetectedBodyType, - DetectedBodyTypeFromContentType = original.BodyData.DetectedBodyTypeFromContentType, - BodyAsFile = transformedBodyAsFilename - }; - } - else - { - string text = handlebarsContext.FileSystemHandler.ReadResponseBodyAsString(transformedBodyAsFilename); - - responseMessage.BodyData = new BodyData - { - DetectedBodyType = BodyType.String, - DetectedBodyTypeFromContentType = original.BodyData.DetectedBodyTypeFromContentType, - BodyAsString = handlebarsContext.ParseAndRender(text, model), - BodyAsFile = transformedBodyAsFilename - }; - } - } - } +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using WireMock.Types; +using WireMock.Util; + +namespace WireMock.Transformers.Handlebars +{ + internal class Transformer : ITransformer + { + private readonly ITransformerContextFactory _factory; + + public Transformer([NotNull] ITransformerContextFactory factory) + { + _factory = factory ?? throw new ArgumentNullException(nameof(factory)); + } + + public (IBodyData BodyData, IDictionary> Headers) Transform(RequestMessage originalRequestMessage, ResponseMessage originalResponseMessage, IBodyData bodyData, IDictionary> headers) + { + var transformerContext = _factory.Create(); + + var model = new + { + request = originalRequestMessage, + response = originalResponseMessage + }; + + IBodyData newBodyData = null; + if (bodyData?.DetectedBodyType != null) + { + newBodyData = TransformBodyData(transformerContext, model, bodyData, false); + } + + return (newBodyData, TransformHeaders(transformerContext, model, headers)); + } + + public ResponseMessage Transform(RequestMessage requestMessage, ResponseMessage original, bool useTransformerForBodyAsFile) + { + var transformerContext = _factory.Create(); + + var responseMessage = new ResponseMessage(); + + var model = new + { + request = requestMessage + }; + + if (original.BodyData?.DetectedBodyType != null) + { + responseMessage.BodyData = TransformBodyData(transformerContext, model, original.BodyData, useTransformerForBodyAsFile); + + if (original.BodyData.DetectedBodyType == BodyType.String) + { + responseMessage.BodyOriginal = original.BodyData.BodyAsString; + } + } + + responseMessage.FaultType = original.FaultType; + responseMessage.FaultPercentage = original.FaultPercentage; + + responseMessage.Headers = TransformHeaders(transformerContext, model, original.Headers); + + switch (original.StatusCode) + { + case int statusCodeAsInteger: + responseMessage.StatusCode = statusCodeAsInteger; + break; + + case string statusCodeAsString: + responseMessage.StatusCode = transformerContext.ParseAndRender(statusCodeAsString, model); + break; + } + + return responseMessage; + } + + private static IBodyData TransformBodyData(ITransformerContext transformerContext, object model, IBodyData original, bool useTransformerForBodyAsFile) + { + switch (original?.DetectedBodyType) + { + case BodyType.Json: + return TransformBodyAsJson(transformerContext, model, original); + + case BodyType.File: + return TransformBodyAsFile(transformerContext, model, original, useTransformerForBodyAsFile); + + case BodyType.String: + return TransformBodyAsString(transformerContext, model, original); + + default: + return null; + } + } + + private static IDictionary> TransformHeaders(ITransformerContext transformerContext, object model, IDictionary> original) + { + if (original == null) + { + return new Dictionary>(); + } + + var newHeaders = new Dictionary>(); + foreach (var header in original) + { + var headerKey = transformerContext.ParseAndRender(header.Key, model); + var templateHeaderValues = header.Value.Select(text => transformerContext.ParseAndRender(text, model)).ToArray(); + + newHeaders.Add(headerKey, new WireMockList(templateHeaderValues)); + } + + return newHeaders; + } + + private static IBodyData TransformBodyAsJson(ITransformerContext handlebarsContext, object model, IBodyData original) + { + JToken jToken; + switch (original.BodyAsJson) + { + case JObject bodyAsJObject: + jToken = bodyAsJObject.DeepClone(); + WalkNode(handlebarsContext, jToken, model); + break; + + case Array bodyAsArray: + jToken = JArray.FromObject(bodyAsArray); + WalkNode(handlebarsContext, jToken, model); + break; + + case string bodyAsString: + jToken = ReplaceSingleNode(handlebarsContext, bodyAsString, model); + break; + + default: + jToken = JObject.FromObject(original.BodyAsJson); + WalkNode(handlebarsContext, jToken, model); + break; + } + + return new BodyData + { + Encoding = original.Encoding, + DetectedBodyType = original.DetectedBodyType, + DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, + BodyAsJson = jToken + }; + } + + private static JToken ReplaceSingleNode(ITransformerContext handlebarsContext, string stringValue, object model) + { + string transformedString = handlebarsContext.ParseAndRender(stringValue, model); + + if (!string.Equals(stringValue, transformedString)) + { + const string property = "_"; + JObject dummy = JObject.Parse($"{{ \"{property}\": null }}"); + JToken node = dummy[property]; + + ReplaceNodeValue(node, transformedString); + + return dummy[property]; + } + + return stringValue; + } + + private static void WalkNode(ITransformerContext handlebarsContext, JToken node, object model) + { + if (node.Type == JTokenType.Object) + { + // In case of Object, loop all children. Do a ToArray() to avoid `Collection was modified` exceptions. + foreach (JProperty child in node.Children().ToArray()) + { + WalkNode(handlebarsContext, child.Value, model); + } + } + else if (node.Type == JTokenType.Array) + { + // In case of Array, loop all items. Do a ToArray() to avoid `Collection was modified` exceptions. + foreach (JToken child in node.Children().ToArray()) + { + WalkNode(handlebarsContext, child, model); + } + } + else if (node.Type == JTokenType.String) + { + // In case of string, try to transform the value. + string stringValue = node.Value(); + if (string.IsNullOrEmpty(stringValue)) + { + return; + } + + string transformedString = handlebarsContext.ParseAndRender(stringValue, model); + if (!string.Equals(stringValue, transformedString)) + { + ReplaceNodeValue(node, transformedString); + } + } + } + + private static void ReplaceNodeValue(JToken node, string stringValue) + { + if (bool.TryParse(stringValue, out bool valueAsBoolean)) + { + node.Replace(valueAsBoolean); + return; + } + + JToken value; + try + { + // Try to convert this string into a JsonObject + value = JToken.Parse(stringValue); + } + catch (JsonException) + { + // Ignore JsonException and just keep string value and convert to JToken + value = stringValue; + } + + node.Replace(value); + } + + private static IBodyData TransformBodyAsString(ITransformerContext handlebarsContext, object model, IBodyData original) + { + return new BodyData + { + Encoding = original.Encoding, + DetectedBodyType = original.DetectedBodyType, + DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, + BodyAsString = handlebarsContext.ParseAndRender(original.BodyAsString, model) + }; + } + + private static IBodyData TransformBodyAsFile(ITransformerContext handlebarsContext, object model, IBodyData original, bool useTransformerForBodyAsFile) + { + string transformedBodyAsFilename = handlebarsContext.ParseAndRender(original.BodyAsFile, model); + + if (!useTransformerForBodyAsFile) + { + return new BodyData + { + DetectedBodyType = original.DetectedBodyType, + DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, + BodyAsFile = transformedBodyAsFilename + }; + } + else + { + string text = handlebarsContext.FileSystemHandler.ReadResponseBodyAsString(transformedBodyAsFilename); + + return new BodyData + { + DetectedBodyType = BodyType.String, + DetectedBodyTypeFromContentType = original.DetectedBodyTypeFromContentType, + BodyAsString = handlebarsContext.ParseAndRender(text, model), + BodyAsFile = transformedBodyAsFilename + }; + } + } + } } \ No newline at end of file diff --git a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs index 319870325..c839c6f29 100644 --- a/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs +++ b/test/WireMock.Net.Tests/Serialization/MappingConverterTests.cs @@ -1,9 +1,13 @@ using System; +using System.Collections.Generic; using FluentAssertions; +using WireMock.Models; using WireMock.RequestBuilders; using WireMock.ResponseBuilders; using WireMock.Serialization; using WireMock.Settings; +using WireMock.Types; +using WireMock.Util; using Xunit; namespace WireMock.Net.Tests.Serialization @@ -25,7 +29,26 @@ public void ToMappingModel() // Assign var request = Request.Create(); var response = Response.Create(); - var mapping = new Mapping(Guid.NewGuid(), "", null, _settings, request, response, 0, null, null, null, null); + var webhook = new Webhook + { + Request = new WebhookRequest + { + Url = "https://test.com", + Headers = new Dictionary> + { + { "Single", new WireMockList("x") }, + { "Multi", new WireMockList("a", "b") } + }, + Method = "post", + BodyData = new BodyData + { + BodyAsString = "b", + DetectedBodyType = BodyType.String, + DetectedBodyTypeFromContentType = BodyType.String + } + } + }; + var mapping = new Mapping(Guid.NewGuid(), "", null, _settings, request, response, 0, null, null, null, null, webhook); // Act var model = _sut.ToMappingModel(mapping); @@ -33,9 +56,16 @@ public void ToMappingModel() // Assert model.Should().NotBeNull(); model.Priority.Should().BeNull(); + model.Response.BodyAsJsonIndented.Should().BeNull(); model.Response.UseTransformer.Should().BeNull(); model.Response.Headers.Should().BeNull(); + + model.Webhook.Request.Method.Should().Be("post"); + model.Webhook.Request.Url.Should().Be("https://test.com"); + model.Webhook.Request.Headers.Should().HaveCount(2); + model.Webhook.Request.Body.Should().Be("b"); + model.Webhook.Request.BodyAsJson.Should().BeNull(); } [Fact] @@ -44,7 +74,7 @@ public void ToMappingModel_WithPriority_ReturnsPriority() // Assign var request = Request.Create(); var response = Response.Create().WithBodyAsJson(new { x = "x" }).WithTransformer(); - var mapping = new Mapping(Guid.NewGuid(), "", null, _settings, request, response, 42, null, null, null, null); + var mapping = new Mapping(Guid.NewGuid(), "", null, _settings, request, response, 42, null, null, null, null, null); // Act var model = _sut.ToMappingModel(mapping); diff --git a/test/WireMock.Net.Tests/Serialization/WebhookMapperTests.cs b/test/WireMock.Net.Tests/Serialization/WebhookMapperTests.cs new file mode 100644 index 000000000..dc6df4e13 --- /dev/null +++ b/test/WireMock.Net.Tests/Serialization/WebhookMapperTests.cs @@ -0,0 +1,101 @@ +using System.Collections.Generic; +using FluentAssertions; +using WireMock.Admin.Mappings; +using WireMock.Serialization; +using WireMock.Types; +using Xunit; + +namespace WireMock.Net.Tests.Serialization +{ + public class WebhookMapperTests + { + [Fact] + public void WebhookMapper_Map_Model_BodyAsString_And_UseTransformerIsFalse() + { + // Assign + var model = new WebhookModel + { + Request = new WebhookRequestModel + { + Url = "https://localhost", + Method = "get", + Headers = new Dictionary + { + { "x", "y" } + }, + Body = "test", + UseTransformer = false + } + }; + + var result = WebhookMapper.Map(model); + + result.Request.Url.Should().Be("https://localhost"); + result.Request.Method.Should().Be("get"); + result.Request.Headers.Should().HaveCount(1); + result.Request.BodyData.BodyAsJson.Should().BeNull(); + result.Request.BodyData.BodyAsString.Should().Be("test"); + result.Request.BodyData.DetectedBodyType.Should().Be(BodyType.String); + result.Request.UseTransformer.Should().BeNull(); + } + + [Fact] + public void WebhookMapper_Map_Model_BodyAsString_And_UseTransformerIsTrue() + { + // Assign + var model = new WebhookModel + { + Request = new WebhookRequestModel + { + Url = "https://localhost", + Method = "get", + Headers = new Dictionary + { + { "x", "y" } + }, + Body = "test", + UseTransformer = true + } + }; + + var result = WebhookMapper.Map(model); + + result.Request.Url.Should().Be("https://localhost"); + result.Request.Method.Should().Be("get"); + result.Request.Headers.Should().HaveCount(1); + result.Request.BodyData.BodyAsJson.Should().BeNull(); + result.Request.BodyData.BodyAsString.Should().Be("test"); + result.Request.BodyData.DetectedBodyType.Should().Be(BodyType.String); + result.Request.UseTransformer.Should().BeTrue(); + result.Request.TransformerType.Should().Be(TransformerType.Handlebars); + } + + [Fact] + public void WebhookMapper_Map_Model_BodyAsJson() + { + // Assign + var model = new WebhookModel + { + Request = new WebhookRequestModel + { + Url = "https://localhost", + Method = "get", + Headers = new Dictionary + { + { "x", "y" } + }, + BodyAsJson = new { n = 12345 } + } + }; + + var result = WebhookMapper.Map(model); + + result.Request.Url.Should().Be("https://localhost"); + result.Request.Method.Should().Be("get"); + result.Request.Headers.Should().HaveCount(1); + result.Request.BodyData.BodyAsString.Should().BeNull(); + result.Request.BodyData.BodyAsJson.Should().NotBeNull(); + result.Request.BodyData.DetectedBodyType.Should().Be(BodyType.Json); + } + } +} diff --git a/test/WireMock.Net.Tests/WireMockServer.WebhookTests.cs b/test/WireMock.Net.Tests/WireMockServer.WebhookTests.cs new file mode 100644 index 000000000..7e6ae43ef --- /dev/null +++ b/test/WireMock.Net.Tests/WireMockServer.WebhookTests.cs @@ -0,0 +1,133 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading.Tasks; +using FluentAssertions; +using WireMock.Models; +using WireMock.RequestBuilders; +using WireMock.ResponseBuilders; +using WireMock.Server; +using WireMock.Types; +using WireMock.Util; +using Xunit; + +namespace WireMock.Net.Tests +{ + public class WireMockServerWebhookTests + { + [Fact] + public async Task WireMockServer_WithWebhook_Should_Send_Message_To_Webhook() + { + // Assign + var serverReceivingTheWebhook = WireMockServer.Start(); + serverReceivingTheWebhook.Given(Request.Create().UsingPost()).RespondWith(Response.Create().WithStatusCode(200)); + + // Act + var server = WireMockServer.Start(); + server.Given(Request.Create().UsingPost()) + .WithWebhook(new Webhook + { + Request = new WebhookRequest + { + Url = serverReceivingTheWebhook.Urls[0], + Method = "post", + BodyData = new BodyData + { + BodyAsString = "abc", + DetectedBodyType = BodyType.String, + DetectedBodyTypeFromContentType = BodyType.String + } + } + }) + .RespondWith(Response.Create().WithBody("a-response")); + + var request = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri($"{server.Urls[0]}/TST"), + Content = new StringContent("test") + }; + + // Assert + var response = await new HttpClient().SendAsync(request); + string content = await response.Content.ReadAsStringAsync(); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + content.Should().Be("a-response"); + + serverReceivingTheWebhook.LogEntries.Should().HaveCount(1); + + server.Dispose(); + serverReceivingTheWebhook.Dispose(); + } + + [Fact] + public async Task WireMockServer_WithWebhookArgs_Should_Send_StringMessage_To_Webhook() + { + // Assign + var serverReceivingTheWebhook = WireMockServer.Start(); + serverReceivingTheWebhook.Given(Request.Create().UsingPost()).RespondWith(Response.Create().WithStatusCode(200)); + + // Act + var server = WireMockServer.Start(); + server.Given(Request.Create().UsingPost()) + .WithWebhook(serverReceivingTheWebhook.Urls[0], "post", null, "OK !", true, TransformerType.Handlebars) + .RespondWith(Response.Create().WithBody("a-response")); + + var request = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri($"{server.Urls[0]}/TST"), + Content = new StringContent("test") + }; + + // Assert + var response = await new HttpClient().SendAsync(request); + string content = await response.Content.ReadAsStringAsync(); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + content.Should().Be("a-response"); + + serverReceivingTheWebhook.LogEntries.Should().HaveCount(1); + serverReceivingTheWebhook.LogEntries.First().RequestMessage.Body.Should().Be("OK !"); + + server.Dispose(); + serverReceivingTheWebhook.Dispose(); + } + + [Fact] + public async Task WireMockServer_WithWebhookArgs_Should_Send_JsonMessage_To_Webhook() + { + // Assign + var serverReceivingTheWebhook = WireMockServer.Start(); + serverReceivingTheWebhook.Given(Request.Create().UsingPost()).RespondWith(Response.Create().WithStatusCode(200)); + + // Act + var server = WireMockServer.Start(); + server.Given(Request.Create().UsingPost()) + .WithWebhook(serverReceivingTheWebhook.Urls[0], "post", null, new { Status = "OK" }, true, TransformerType.Handlebars) + .RespondWith(Response.Create().WithBody("a-response")); + + var request = new HttpRequestMessage + { + Method = HttpMethod.Post, + RequestUri = new Uri($"{server.Urls[0]}/TST"), + Content = new StringContent("test") + }; + + // Assert + var response = await new HttpClient().SendAsync(request); + string content = await response.Content.ReadAsStringAsync(); + + response.StatusCode.Should().Be(HttpStatusCode.OK); + content.Should().Be("a-response"); + + serverReceivingTheWebhook.LogEntries.Should().HaveCount(1); + serverReceivingTheWebhook.LogEntries.First().RequestMessage.Body.Should().Be("{\"Status\":\"OK\"}"); + + server.Dispose(); + serverReceivingTheWebhook.Dispose(); + } + } +} \ No newline at end of file