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