diff --git a/Deveel.Webhooks.sln b/Deveel.Webhooks.sln
index 7b1b590..9b541fb 100644
--- a/Deveel.Webhooks.sln
+++ b/Deveel.Webhooks.sln
@@ -77,6 +77,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Webhooks.Receiver.Te
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Deveel.Webhooks.DeliveryResultLogging.Tests", "test\Deveel.Webhooks.DeliveryResultLogging.Tests\Deveel.Webhooks.DeliveryResultLogging.Tests.csproj", "{DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0}"
EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "util", "util", "{6D7A2395-7D23-40E4-91EF-AF4EE5C4EFEB}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Deveel.Webhooks.TestHttpClient", "test\Deveel.Webhooks.TestHttpClient\Deveel.Webhooks.TestHttpClient.csproj", "{98547810-98BF-46E6-8C13-2CC2690D145E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -207,6 +211,10 @@ Global
{DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0}.Debug|Any CPU.Build.0 = Debug|Any CPU
{DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0}.Release|Any CPU.ActiveCfg = Release|Any CPU
{DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0}.Release|Any CPU.Build.0 = Release|Any CPU
+ {98547810-98BF-46E6-8C13-2CC2690D145E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {98547810-98BF-46E6-8C13-2CC2690D145E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {98547810-98BF-46E6-8C13-2CC2690D145E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {98547810-98BF-46E6-8C13-2CC2690D145E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -243,6 +251,8 @@ Global
{EBD3DB50-0E90-47C7-9DD8-FBBAC8696CE1} = {07F23FF6-2FE1-4072-BF37-9238E3750AA1}
{4942C858-277D-438D-B822-92055B8E8DF7} = {07F23FF6-2FE1-4072-BF37-9238E3750AA1}
{DDD9A4CB-AA5E-4C0B-87F3-CDC24F6A8DA0} = {07F23FF6-2FE1-4072-BF37-9238E3750AA1}
+ {6D7A2395-7D23-40E4-91EF-AF4EE5C4EFEB} = {07F23FF6-2FE1-4072-BF37-9238E3750AA1}
+ {98547810-98BF-46E6-8C13-2CC2690D145E} = {6D7A2395-7D23-40E4-91EF-AF4EE5C4EFEB}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {E682A9F5-43D7-4D4C-82EA-953545B8F4DE}
diff --git a/src/Deveel.Webhooks.Sender/Deveel.Webhooks.Sender.csproj b/src/Deveel.Webhooks.Sender/Deveel.Webhooks.Sender.csproj
index 0262d8f..1d7aec6 100644
--- a/src/Deveel.Webhooks.Sender/Deveel.Webhooks.Sender.csproj
+++ b/src/Deveel.Webhooks.Sender/Deveel.Webhooks.Sender.csproj
@@ -6,18 +6,14 @@
webhooks;webhook;events;event;sender;send;delivery;http
-
-
-
-
-
+
-
+
diff --git a/src/Deveel.Webhooks.Sender/Webhooks/LoggerExtensions.cs b/src/Deveel.Webhooks.Sender/Webhooks/LoggerExtensions.cs
index 7d3d310..a40276e 100644
--- a/src/Deveel.Webhooks.Sender/Webhooks/LoggerExtensions.cs
+++ b/src/Deveel.Webhooks.Sender/Webhooks/LoggerExtensions.cs
@@ -57,11 +57,27 @@ static partial class LoggerExtensions {
public static partial void TraceSendingWebhook(this ILogger logger, string destinationUrl);
[LoggerMessage(EventId = 120328, Level = LogLevel.Debug,
- Message = "The webhook has been sent to the receiver '{DestinationUrl}'")]
+ Message = "The webhook has been sent to the receiver '{DestinationUrl}'")]
public static partial void TraceSuccessfulDelivery(this ILogger logger, string destinationUrl);
[LoggerMessage(EventId = -120329, Level = LogLevel.Debug,
- Message = "The webhook has failed to be delivered to the receiver '{DestinationUrl}'")]
+ Message = "The webhook has failed to be delivered to the receiver '{DestinationUrl}'")]
public static partial void WarnDeliveryFailed(this ILogger logger, string destinationUrl);
+
+ [LoggerMessage(EventId = 120330, Level = LogLevel.Debug,
+ Message = "The webhook has been delivered to the receiver '{DestinationUrl}'")]
+ public static partial void TraceDeliveryFinished(this ILogger logger, string destinationUrl);
+
+ [LoggerMessage(EventId = 120331, Level = LogLevel.Debug,
+ Message = "Verifying the the receiver '{VerificationUrl}' for the delivery")]
+ public static partial void TraceVerifyingReceiver(this ILogger logger, string verificationUrl);
+
+ [LoggerMessage(EventId = 120332, Level = LogLevel.Debug,
+ Message = "The receiver '{VerificationUrl}' has been verified for the delivery")]
+ public static partial void TraceReceiverVerified(this ILogger logger, string verificationUrl);
+
+ [LoggerMessage(EventId = -120333, Level = LogLevel.Error,
+ Message = "The receiver '{VerificationUrl}' has failed to be verified for the delivery")]
+ public static partial void LogVerificationFailed(this ILogger logger, Exception error, string verificationUrl);
}
}
diff --git a/src/Deveel.Webhooks.Sender/Webhooks/SystemWebhookXmlSerializer.cs b/src/Deveel.Webhooks.Sender/Webhooks/SystemWebhookXmlSerializer.cs
index c5c681d..27a053e 100644
--- a/src/Deveel.Webhooks.Sender/Webhooks/SystemWebhookXmlSerializer.cs
+++ b/src/Deveel.Webhooks.Sender/Webhooks/SystemWebhookXmlSerializer.cs
@@ -39,8 +39,8 @@ public SystemWebhookXmlSerializer(XmlSerializerOptions? options = null) {
///
public async Task SerializeAsync(Stream utf8Stream, TWebhook webhook, CancellationToken cancellationToken = default) {
- if (utf8Stream == null)
- throw new ArgumentNullException(nameof(utf8Stream));
+ ArgumentNullException.ThrowIfNull(utf8Stream, nameof(utf8Stream));
+
if (!utf8Stream.CanWrite)
throw new ArgumentException("The stream is not writable", nameof(utf8Stream));
diff --git a/src/Deveel.Webhooks.Sender/Webhooks/UriExtensions.cs b/src/Deveel.Webhooks.Sender/Webhooks/UriExtensions.cs
index da102fd..3803be3 100644
--- a/src/Deveel.Webhooks.Sender/Webhooks/UriExtensions.cs
+++ b/src/Deveel.Webhooks.Sender/Webhooks/UriExtensions.cs
@@ -12,12 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
namespace Deveel.Webhooks {
///
/// Extends the class with additional
diff --git a/src/Deveel.Webhooks.Sender/Webhooks/WebhookDestination.cs b/src/Deveel.Webhooks.Sender/Webhooks/WebhookDestination.cs
index 8d95d26..8eb8f43 100644
--- a/src/Deveel.Webhooks.Sender/Webhooks/WebhookDestination.cs
+++ b/src/Deveel.Webhooks.Sender/Webhooks/WebhookDestination.cs
@@ -12,6 +12,11 @@
// See the License for the specific language governing permissions and
// limitations under the License.
+using System.Threading;
+
+using Polly;
+using Polly.Extensions.Http;
+
namespace Deveel.Webhooks {
// TODO: Find a better name for this class? The reason
// for not calling it 'WebhookReceiver' is to avoid
@@ -26,6 +31,8 @@ namespace Deveel.Webhooks {
/// including configuration options for the delivery and verification.
///
public sealed class WebhookDestination {
+ private string name;
+
///
/// Initializes a new instance of the class
///
@@ -39,13 +46,14 @@ public sealed class WebhookDestination {
/// Thrown when the is not an absolute URI.
///
public WebhookDestination(Uri url) {
- if (url == null)
- throw new ArgumentNullException(nameof(url));
+ ArgumentNullException.ThrowIfNull(url, nameof(url));
if (!url.IsAbsoluteUri)
throw new ArgumentException($"The '{nameof(url)}' must be an absolute URI.", nameof(url));
Url = url;
+
+ name = url.ToString();
}
///
@@ -77,6 +85,18 @@ private static Uri ParseUri(string url) {
///
public Uri Url { get; }
+ ///
+ /// Gets or sets the name of the destination,
+ /// to uniquely identify it in a sender context.
+ ///
+ public string Name {
+ get => name;
+ set {
+ ArgumentNullException.ThrowIfNull(value, nameof(Name));
+ name = value;
+ }
+ }
+
///
/// Gets or sets the options for the verification of the destination
///
@@ -214,6 +234,22 @@ public WebhookDestination WithVerification(Action
+ /// Sets the name of the webhook destination.
+ ///
+ ///
+ /// The name of the destination.
+ ///
+ ///
+ /// Returns this instance of the with
+ /// the name set.
+ ///
+ public WebhookDestination WithName(string name) {
+ ArgumentNullException.ThrowIfNull(name, nameof(name));
+ Name = name;
+ return this;
+ }
+
///
/// Merges the current settings with the default settings
/// from the configuration of a sender service.
@@ -231,6 +267,7 @@ public WebhookDestination WithVerification(Action
public WebhookDestination Merge(WebhookSenderOptions options) where TWebhook : class {
var result = new WebhookDestination(Url) {
+ Name = Name,
Sign = Sign,
Headers = new Dictionary(),
Format = Format ?? options.DefaultFormat,
@@ -265,5 +302,25 @@ public WebhookDestination Merge(WebhookSenderOptions options
return result;
}
+
+ internal IAsyncPolicy CreateRetryPolicy(WebhookRetryOptions? retry = null) {
+ // TODO: Validate that the sum of the retry delays is less than the timeout
+ var retryCountValue = (Retry?.MaxRetries ?? retry?.MaxRetries) ?? 0;
+ var sleepValue = (Retry?.MaxDelay ?? retry?.MaxDelay) ?? TimeSpan.FromMilliseconds(300);
+
+ // the retry policy
+ return Policy
+ .Handle()
+ .Or()
+ .Or()
+ .OrTransientHttpStatusCode()
+ .WaitAndRetryAsync(retryCountValue, attempt => sleepValue);
+ }
+
+ internal IAsyncPolicy CreateTimeoutPolicy(WebhookRetryOptions? retry = null) {
+ // TODO: Validate that the timeout is not less than the retry timeout
+ var timeoutValue = (Retry?.Timeout ?? retry?.Timeout) ?? System.Threading.Timeout.InfiniteTimeSpan;
+ return Policy.TimeoutAsync(timeoutValue);
+ }
}
}
diff --git a/src/Deveel.Webhooks.Sender/Webhooks/WebhookDestinationVerifier.cs b/src/Deveel.Webhooks.Sender/Webhooks/WebhookDestinationVerifier.cs
index c0016ce..6df64d9 100644
--- a/src/Deveel.Webhooks.Sender/Webhooks/WebhookDestinationVerifier.cs
+++ b/src/Deveel.Webhooks.Sender/Webhooks/WebhookDestinationVerifier.cs
@@ -18,6 +18,7 @@
using System.Text;
using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Polly;
@@ -28,8 +29,11 @@ namespace Deveel.Webhooks {
/// A default implementation of the ,
/// that pings a destination URL with some configured parameters to verify if it is reachable.
///
- public class WebhookDestinationVerifier : WebhookSenderClient, IWebhookDestinationVerifier, IDisposable
+ public class WebhookDestinationVerifier : IWebhookDestinationVerifier
where TWebhook : class {
+ private readonly ILogger logger;
+ private readonly IHttpClientFactory httpClientFactory;
+
///
/// Creates a new instance of the class
/// with the given options.
@@ -44,11 +48,11 @@ public class WebhookDestinationVerifier : WebhookSenderClient, IWebhoo
///
/// A logger to use for logging the operations of the verifier.
///
- public WebhookDestinationVerifier(IOptions> options,
- IHttpClientFactory? httpClientFactory = null,
+ public WebhookDestinationVerifier(IOptions> options,
+ IHttpClientFactory httpClientFactory,
ILogger>? logger = null)
- : this(options.Value, httpClientFactory, logger) {
- }
+ : this(options.Value, httpClientFactory, logger) {
+ }
///
/// Creates a new instance of the class
@@ -66,44 +70,18 @@ public WebhookDestinationVerifier(IOptions> optio
///
/// Thrown when the is null.
///
- protected WebhookDestinationVerifier(WebhookSenderOptions options,
- IHttpClientFactory? httpClientFactory = null,
- ILogger? logger = null)
- : base(httpClientFactory, logger) {
+ protected WebhookDestinationVerifier(WebhookSenderOptions options,
+ IHttpClientFactory httpClientFactory,
+ ILogger? logger = null) {
+ this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
+ this.logger = logger ?? NullLogger>.Instance;
SenderOptions = options ?? throw new ArgumentNullException(nameof(options));
- }
-
- ///
- /// Creates a new instance of the class
- /// using an optional HTTP client.
- ///
- ///
- /// The options to configure the verifier service.
- ///
- ///
- /// The optional HTTP client used to send the verification requests.
- ///
- ///
- /// A logger to use for logging the operations of the verifier.
- ///
- ///
- /// Thrown when the is null.
- ///
- protected WebhookDestinationVerifier(WebhookSenderOptions options, HttpClient httpClient, ILogger? logger)
- : base(httpClient, logger) {
- SenderOptions = options;
}
///
/// Gets the options used to configure the verifier service.
///
- protected WebhookSenderOptions SenderOptions { get; }
-
- ///
- protected override WebhookRetryOptions? Retry => SenderOptions.Retry;
-
- ///
- protected override TimeSpan? Timeout => SenderOptions.Timeout;
+ protected WebhookSenderOptions SenderOptions { get; }
///
/// Appends a challenge to query string of the
@@ -154,25 +132,25 @@ protected virtual string CreateChallenge() {
///
/// Returns an instance of that can be sent
///
- protected virtual HttpRequestMessage CreateRequest(Uri verificationUrl, string token, string? challenge) {
- var request = new HttpRequestMessage(new HttpMethod(SenderOptions.Verification.HttpMethod), verificationUrl);
+ protected virtual HttpRequestMessage CreateRequest(Uri verificationUrl, string token, string? challenge) {
+ var request = new HttpRequestMessage(new HttpMethod(SenderOptions.Verification.HttpMethod), verificationUrl);
- if (SenderOptions.Verification.TokenLocation == VerificationTokenLocation.QueryString) {
+ if (SenderOptions.Verification.TokenLocation == VerificationTokenLocation.QueryString) {
request.RequestUri = request.RequestUri!.AddQueryParameter(SenderOptions.Verification.TokenQueryParameter, token);
- } else {
- request.Headers.TryAddWithoutValidation(SenderOptions.Verification.TokenHeaderName, token);
- }
+ } else {
+ request.Headers.TryAddWithoutValidation(SenderOptions.Verification.TokenHeaderName, token);
+ }
- if ((SenderOptions.Verification.Challenge ?? false) &&
+ if ((SenderOptions.Verification.Challenge ?? false) &&
!String.IsNullOrWhiteSpace(challenge)) {
AddChallenge(request, challenge);
}
- return request;
- }
+ return request;
+ }
private async Task VerifyChallengeAsync(HttpResponseMessage response, string challenge, CancellationToken cancellationToken) {
- if (response.Content == null ||
+ if (response.Content == null ||
response.Content.Headers == null ||
response.Content.Headers.ContentType == null)
return new HttpResponseMessage(HttpStatusCode.BadRequest);
@@ -188,15 +166,15 @@ private async Task VerifyChallengeAsync(HttpResponseMessage
return new HttpResponseMessage(HttpStatusCode.OK);
}
- private async Task TryVerifyAsync(Uri destinationUrl, string verifyToken, string? challenge, CancellationToken cancellationToken) {
- var timeoutPolicy = CreateTryTimeoutPolicy(SenderOptions.Retry?.Timeout);
+ private async Task TryVerifyAsync(HttpClient httpClient, WebhookDestination destination, Uri destinationUrl, string verifyToken, string? challenge, CancellationToken cancellationToken) {
+ var timeoutPolicy = destination.CreateTimeoutPolicy(SenderOptions.Retry);
HttpResponseMessage? response = null;
try {
var request = CreateRequest(destinationUrl, verifyToken, challenge);
- response = await timeoutPolicy.ExecuteAsync(token => SendRequestAsync(request, token), cancellationToken);
+ response = await timeoutPolicy.ExecuteAsync(token => httpClient.SendAsync(request, token), cancellationToken);
// TODO: Check the response body for a specific value?
@@ -205,14 +183,14 @@ private async Task TryVerifyAsync(Uri destinationUrl, string ver
!String.IsNullOrWhiteSpace(challenge))
response = await VerifyChallengeAsync(response, challenge, cancellationToken);
- return response.StatusCode;
+ return response;
} catch (TimeoutRejectedException ex) {
throw new TimeoutException("A timeout has occurred", ex);
} catch (HttpRequestException) {
throw;
- } catch(TaskCanceledException) {
+ } catch (TaskCanceledException) {
throw;
- } catch(Exception ex) {
+ } catch (Exception ex) {
throw new WebhookVerificationException("An error occurred while verifying the destination", ex);
} finally {
response?.Dispose();
@@ -270,9 +248,11 @@ public virtual async Task VerifyDestinationAsync(
destination.Verification.Parameters == null)
throw new WebhookVerificationException("The destination is not configured to be verified");
- var timeoutPolicy = CreateTimeoutPolicy();
+ var httpClient = httpClientFactory.CreateClient(destination.Name);
+
+ var timeoutPolicy = Policy.TimeoutAsync(SenderOptions.Timeout ?? Timeout.InfiniteTimeSpan);
- var retryPolicy = CreateRetryPolicy(destination.Retry?.MaxRetries, destination.Retry?.MaxDelay);
+ var retryPolicy = destination.CreateRetryPolicy(SenderOptions.Retry);
var policy = timeoutPolicy.WrapAsync(retryPolicy);
var url = destination.Verification.VerificationUrl ?? destination.Url;
@@ -284,14 +264,19 @@ public virtual async Task VerifyDestinationAsync(
if (SenderOptions.Verification.Challenge ?? false)
challenge = CreateChallenge();
- var capture = await policy.ExecuteAndCaptureAsync(token => TryVerifyAsync(url, verifyToken!, challenge, token), cancellationToken);
+ logger.TraceVerifyingReceiver(url.ToString());
+
+ var capture = await policy.ExecuteAndCaptureAsync(token => TryVerifyAsync(httpClient, destination, url, verifyToken!, challenge, token), cancellationToken);
+
+ // var response = await TryVerifyAsync(httpClient, destination, url, verifyToken!, challenge, cancellationToken);
- // TODO: configure the expected status code
if (capture.Outcome == OutcomeType.Successful) {
- var httpStatusCode = (int)capture.Result;
+ var httpStatusCode = (int)capture.Result.StatusCode;
- if (httpStatusCode < 400)
+ if (httpStatusCode < 400) {
+ logger.TraceReceiverVerified(url.ToString());
return DestinationVerificationResult.Success(httpStatusCode);
+ }
return DestinationVerificationResult.Failed(httpStatusCode);
} else if (capture.FinalException is HttpRequestException error) {
@@ -301,11 +286,15 @@ public virtual async Task VerifyDestinationAsync(
} else {
throw new WebhookVerificationException("An error occurred while verifying the destination", capture.FinalException);
}
+ } catch (HttpRequestException ex) {
+ logger.LogVerificationFailed(ex, destination.Url.ToString());
+ return DestinationVerificationResult.Failed(ex.StatusCode == null ? 0 : (int)ex.StatusCode);
} catch (WebhookVerificationException) {
throw;
} catch (Exception ex) {
+ logger.LogVerificationFailed(ex, destination.Url.ToString());
throw new WebhookVerificationException("An error occurred while verifying the destination URL", ex);
}
- }
- }
-}
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Deveel.Webhooks.Sender/Webhooks/WebhookSender.cs b/src/Deveel.Webhooks.Sender/Webhooks/WebhookSender.cs
index ee72b01..dc11e8f 100644
--- a/src/Deveel.Webhooks.Sender/Webhooks/WebhookSender.cs
+++ b/src/Deveel.Webhooks.Sender/Webhooks/WebhookSender.cs
@@ -36,7 +36,10 @@ namespace Deveel.Webhooks {
/// overloads that accept an instance of , to ensure
/// a proper management of the instances.
///
- public class WebhookSender : WebhookSenderClient, IWebhookSender, IDisposable where TWebhook : class {
+ public class WebhookSender : IWebhookSender where TWebhook : class {
+ private readonly IHttpClientFactory httpClientFactory;
+ private readonly ILogger logger;
+
///
/// Constructs a new instance of the
///
@@ -52,7 +55,7 @@ public class WebhookSender : WebhookSenderClient, IWebhookSender
public WebhookSender(IOptions> options,
- IHttpClientFactory? httpClientFactory = null,
+ IHttpClientFactory httpClientFactory,
ILogger>? logger = null)
: this(options.Value, httpClientFactory, logger) {
}
@@ -75,43 +78,14 @@ public WebhookSender(IOptions> options,
/// are null
///
protected WebhookSender(WebhookSenderOptions options,
- IHttpClientFactory? httpClientFactory = null,
- ILogger>? logger = null)
- : base(httpClientFactory, logger ?? NullLogger>.Instance) {
- if (options is null) throw new ArgumentNullException(nameof(options));
+ IHttpClientFactory httpClientFactory,
+ ILogger>? logger = null) {
+ ArgumentNullException.ThrowIfNull(options, nameof(options));
- SenderOptions = options;
- }
-
- ///
- /// Constructs a sender that uses the given options and a HTTP client
- ///
- ///
- /// The instance of the options used to configure the sender.
- ///
- ///
- /// A used to send webhooks.
- ///
- ///
- /// A logger that is used to log messages during the sending of webhooks.
- ///
- ///
- /// Thown when the or the
- /// are null.
- ///
- ///
- /// The provided will not be disposed when
- /// the sender is disposed.
- ///
- protected WebhookSender(WebhookSenderOptions options, HttpClient httpClient, ILogger? logger)
- : base(httpClient, logger) {
- if (options is null)
- throw new ArgumentNullException(nameof(options));
-
- if (httpClient is null)
- throw new ArgumentNullException(nameof(httpClient));
+ this.httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
SenderOptions = options;
+ this.logger = logger ?? NullLogger>.Instance;
}
///
@@ -119,15 +93,6 @@ protected WebhookSender(WebhookSenderOptions options, HttpClient httpC
///
protected WebhookSenderOptions SenderOptions { get; }
- ///
- protected override WebhookRetryOptions? Retry => SenderOptions.Retry;
-
- ///
- protected override string? HttpClientName => SenderOptions.HttpClientName;
-
- ///
- protected override TimeSpan? Timeout => SenderOptions.Timeout;
-
///
/// Gets a service that is used to compute the signature of a webhook,
/// using the algorithm specified.
@@ -144,8 +109,6 @@ protected WebhookSender(WebhookSenderOptions options, HttpClient httpC
/// Thrown when the sender has been disposed
///
protected virtual IWebhookSigner? GetSigner(string algorithm) {
- ThrowIfDisposed();
-
if (SenderOptions.Signer != null &&
SenderOptions.Signer.Algorithms.Any(x => String.Equals(x, algorithm, StringComparison.OrdinalIgnoreCase)))
return SenderOptions.Signer;
@@ -181,7 +144,7 @@ protected WebhookSender(WebhookSenderOptions options, HttpClient httpC
if (string.IsNullOrWhiteSpace(webhookBody))
throw new ArgumentNullException(nameof(webhookBody));
- Logger.TraceCreatingSignature(algorithm);
+ logger.TraceCreatingSignature(algorithm);
string? signature = null;
@@ -192,7 +155,7 @@ protected WebhookSender(WebhookSenderOptions options, HttpClient httpC
signature = signer.SignWebhook(webhookBody, secret);
}
- Logger.TraceCreatingSignature(algorithm);
+ logger.TraceCreatingSignature(algorithm);
return signature;
}
@@ -219,11 +182,11 @@ protected virtual async Task SerializeToJsonAsync(TWebhook webhook, Canc
if (SenderOptions.JsonSerializer == null)
throw new NotSupportedException("No JSON serializer was set");
- Logger.TraceSerializing("json");
+ logger.TraceSerializing("json");
var result = await SenderOptions.JsonSerializer.SerializeToStringAsync(webhook, cancellationToken);
- Logger.TraceSerialized("json");
+ logger.TraceSerialized("json");
return result;
}
@@ -250,11 +213,11 @@ protected virtual async Task SerializeToXmlAsync(TWebhook webhook, Cance
if (SenderOptions.XmlSerializer == null)
throw new NotSupportedException("No XML serializer was set");
- Logger.TraceSerializing("xml");
+ logger.TraceSerializing("xml");
var result = await SenderOptions.XmlSerializer.SerializeToStringAsync(webhook, cancellationToken);
- Logger.TraceSerialized("xml");
+ logger.TraceSerialized("xml");
return result;
}
@@ -303,17 +266,17 @@ protected virtual void SignWebhookRequest(HttpRequestMessage request, string alg
if (String.IsNullOrWhiteSpace(SenderOptions.Signature.HeaderName))
throw new WebhookSenderException("The header name for the signature is not set");
- Logger.TraceSigningRequest(algorithm, WebhookSignatureLocation.Header, SenderOptions.Signature.HeaderName);
+ logger.TraceSigningRequest(algorithm, WebhookSignatureLocation.Header, SenderOptions.Signature.HeaderName);
// request.Headers.Add(configuration.DeliveryOptions.SignatureHeaderName, $"{provider.Algorithm}={signature}");
request.Headers.Add(SenderOptions.Signature.HeaderName, signature);
- Logger.TraceRequestSigned(algorithm, WebhookSignatureLocation.Header, SenderOptions.Signature.HeaderName);
+ logger.TraceRequestSigned(algorithm, WebhookSignatureLocation.Header, SenderOptions.Signature.HeaderName);
} else if (SenderOptions.Signature.Location == WebhookSignatureLocation.QueryString) {
if (String.IsNullOrWhiteSpace(SenderOptions.Signature.QueryParameter))
throw new WebhookSenderException("The query parameter for the signature is not set");
- Logger.TraceSigningRequest(algorithm, WebhookSignatureLocation.QueryString, SenderOptions.Signature.QueryParameter);
+ logger.TraceSigningRequest(algorithm, WebhookSignatureLocation.QueryString, SenderOptions.Signature.QueryParameter);
var uri = request.RequestUri!
.AddQueryParameter(SenderOptions.Signature.QueryParameter, signature);
@@ -324,7 +287,7 @@ protected virtual void SignWebhookRequest(HttpRequestMessage request, string alg
request.RequestUri = uri;
- Logger.TraceRequestSigned(algorithm, WebhookSignatureLocation.QueryString, SenderOptions.Signature.QueryParameter);
+ logger.TraceRequestSigned(algorithm, WebhookSignatureLocation.QueryString, SenderOptions.Signature.QueryParameter);
}
}
@@ -511,9 +474,8 @@ protected virtual Task OnAttemptCompletedAsync(WebhookDestination destination, T
return Task.CompletedTask;
}
- private async Task TrySendAsync(WebhookDestination destination, TWebhook webhook, WebhookDeliveryResult result, CancellationToken cancellationToken) {
+ private async Task TrySendAsync(HttpClient httpClient, WebhookDestination destination, TWebhook webhook, WebhookDeliveryResult result, CancellationToken cancellationToken) {
var attempt = result.StartAttempt();
- var timeoutPolicy = CreateTryTimeoutPolicy(destination.Retry?.Timeout);
HttpResponseMessage? response = null;
@@ -527,28 +489,32 @@ private async Task TrySendAsync(WebhookDestination destination, TWebhook webhook
AddTraceHeader(request, result.OperationId);
AddAttemptHeader(request, attempt.Number);
- Logger.TraceStartingAttempt(request.RequestUri!.GetLeftPart(UriPartial.Path));
+ logger.TraceStartingAttempt(request.RequestUri!.GetLeftPart(UriPartial.Path));
- response = await timeoutPolicy.ExecuteAsync(token => SendRequestAsync(request, token), cancellationToken);
+ var timeoutPolicy = destination.CreateTimeoutPolicy(SenderOptions.Retry);
+
+ response = await timeoutPolicy.ExecuteAsync(token => httpClient.SendAsync(request, token), cancellationToken);
attempt.Complete((int) response.StatusCode, response.ReasonPhrase);
response.EnsureSuccessStatusCode();
+
+ return response;
} catch (TaskCanceledException ex) {
- Logger.WarnTimeOut(ex);
+ logger.WarnTimeOut(ex);
attempt.TimeOut();
throw;
} catch (TimeoutException ex) {
- Logger.WarnTimeOut(ex);
+ logger.WarnTimeOut(ex);
attempt.TimeOut();
throw;
} catch (TimeoutRejectedException ex) {
- Logger.WarnTimeOut(ex);
+ logger.WarnTimeOut(ex);
attempt.TimeOut();
throw new TimeoutException("A timeout occurred while trying to get the response", ex);
} catch (HttpRequestException ex) {
- Logger.WarnRequestFailed(ex, request?.RequestUri!.GetLeftPart(UriPartial.Path)!, (int?)(response?.StatusCode ?? ex.StatusCode));
+ logger.WarnRequestFailed(ex, request?.RequestUri!.GetLeftPart(UriPartial.Path)!, (int?)(response?.StatusCode ?? ex.StatusCode));
if (response != null) {
attempt.Complete((int)response.StatusCode, response.ReasonPhrase);
@@ -558,12 +524,12 @@ private async Task TrySendAsync(WebhookDestination destination, TWebhook webhook
throw;
} catch (WebhookSenderException ex) {
- Logger.LogUnknownError(ex);
+ logger.LogUnknownError(ex);
attempt.LocalFail($"Local error: {ex.Message}");
throw;
} catch (Exception ex) {
- Logger.LogUnknownError(ex);
+ logger.LogUnknownError(ex);
attempt.LocalFail($"Local error: {ex.Message}");
throw new WebhookSenderException("Could not send the webhook", ex);
@@ -572,7 +538,7 @@ private async Task TrySendAsync(WebhookDestination destination, TWebhook webhook
attempt.LocalFail("Could not complete the request");
}
- Logger.TraceAttemptFinished(request?.RequestUri!.GetLeftPart(UriPartial.Path)!, (int?)attempt.ResponseCode);
+ logger.TraceAttemptFinished(request?.RequestUri!.GetLeftPart(UriPartial.Path)!, (int?)attempt.ResponseCode);
if (attempt.Failed) {
// TODO: log that the attempt failed
@@ -625,17 +591,19 @@ public virtual async Task> SendAsync(WebhookDest
try {
var destination = receiver.Merge(SenderOptions);
- Logger.TraceSendingWebhook(destination.Url.GetLeftPart(UriPartial.Path));
+ logger.TraceSendingWebhook(destination.Url.GetLeftPart(UriPartial.Path));
+
+ var httpClient = httpClientFactory.CreateClient(destination.Name);
var operationId = Guid.NewGuid().ToString("N");
var result = new WebhookDeliveryResult(operationId, destination, webhook);
- var timeoutPolicy = CreateTimeoutPolicy();
+ var retryPolicy = destination.CreateRetryPolicy(SenderOptions.Retry);
+ var timeoutPolicy = Policy.TimeoutAsync(SenderOptions.Timeout ?? Timeout.InfiniteTimeSpan);
- var retryPolicy = CreateRetryPolicy(destination.Retry?.MaxRetries, destination.Retry?.MaxDelay);
- var policy = timeoutPolicy.WrapAsync(retryPolicy);
+ var policy = Policy.WrapAsync(timeoutPolicy, retryPolicy);
- var captured = await policy.ExecuteAndCaptureAsync(token => TrySendAsync(receiver, webhook, result, token), cancellationToken);
+ var captured = await policy.ExecuteAndCaptureAsync(token => TrySendAsync(httpClient, receiver, webhook, result, token), cancellationToken);
// TODO: Should we handle the managed state? All the states are in the result object
@@ -645,9 +613,9 @@ public virtual async Task> SendAsync(WebhookDest
}
if (result.Successful) {
- Logger.TraceSuccessfulDelivery(destination.Url.GetLeftPart(UriPartial.Path));
+ logger.TraceSuccessfulDelivery(destination.Url.GetLeftPart(UriPartial.Path));
} else {
- Logger.WarnDeliveryFailed(destination.Url.GetLeftPart(UriPartial.Path));
+ logger.WarnDeliveryFailed(destination.Url.GetLeftPart(UriPartial.Path));
}
return result;
@@ -655,7 +623,7 @@ public virtual async Task> SendAsync(WebhookDest
throw;
} catch(Exception ex) {
- Logger.LogUnknownError(ex);
+ logger.LogUnknownError(ex);
throw new WebhookSenderException("An error occurred while sending the webhook", ex);
}
diff --git a/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderBuilder.cs b/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderBuilder.cs
index c1505c6..4d41de3 100644
--- a/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderBuilder.cs
+++ b/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderBuilder.cs
@@ -52,6 +52,8 @@ private void RegisterDefaultServices() {
Services.TryAddScoped>();
Services.AddScoped, WebhookDestinationVerifier>();
+
+ Services.AddHttpClient();
}
///
@@ -91,6 +93,5 @@ public WebhookSenderBuilder UseDestinationVerifier()
return this;
}
-
}
}
diff --git a/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderClient.cs b/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderClient.cs
deleted file mode 100644
index 8d3f9d7..0000000
--- a/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderClient.cs
+++ /dev/null
@@ -1,244 +0,0 @@
-// Copyright 2022-2023 Deveel
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Logging.Abstractions;
-
-using Polly;
-
-namespace Deveel.Webhooks {
- ///
- /// A base class for the clients that send HTTP requests to a destination.
- ///
- public abstract class WebhookSenderClient : IDisposable {
- private readonly IHttpClientFactory? httpClientFactory;
-
- private bool disposeClient;
- private HttpClient? httpClient;
-
- private bool disposed = false;
-
- ///
- /// Creates a new instance of the class
- /// that uses the given HTTP client.
- ///
- ///
- /// The HTTP client to use for sending the requests.
- ///
- ///
- /// A logger to use for logging the operations of the sender.
- ///
- protected WebhookSenderClient(HttpClient httpClient, ILogger? logger) {
- this.httpClient = httpClient;
- disposeClient = (httpClient == null);
-
- Logger = logger ?? NullLogger.Instance;
- }
-
- ///
- /// Creates a new instance of the class
- ///
- ///
- /// The factory of HTTP clients to use for sending the requests.
- ///
- ///
- /// A logger to use for logging the operations of the sender.
- ///
- protected WebhookSenderClient(IHttpClientFactory? httpClientFactory, ILogger? logger) {
- this.httpClientFactory = httpClientFactory;
- Logger = logger ?? NullLogger.Instance;
- }
-
-
- ///
- /// Gets the timeout for each request sent.
- ///
- protected virtual TimeSpan? Timeout { get; }
-
- ///
- /// Gets the retry options for each request sent.
- ///
- protected virtual WebhookRetryOptions? Retry { get; }
-
- ///
- /// Gets the logger to use for logging the operations of the sender.
- ///
- protected ILogger Logger { get; }
-
- ///
- /// Gets the name of the to be obtained
- /// from the .
- ///
- protected virtual string? HttpClientName { get; }
-
- ///
- /// Throws an exception if the sender has been disposed
- ///
- ///
- /// Thrown when the sender has been disposed
- ///
- protected void ThrowIfDisposed() {
- if (disposed)
- throw new ObjectDisposedException(GetType().Name);
- }
-
- ///
- /// Create a HTTP client to use for sending the webhook
- ///
- ///
- ///
- /// -
- /// When the sender was constructed with a ,
- /// this method will use it to create a new instance of .
- ///
- /// -
- /// When the sender was constructed with a , this method
- /// returns the same instance.
- ///
- /// -
- /// When neither a or a
- /// where provided, this method will create a new instance of
- /// that will be disposed when the sender is disposed.
- ///
- ///
- ///
- ///
- /// Returns an instance of to use for sending the webhook,
- /// that can be already existing (when explicitly specified) or a new one (from the factory).
- ///
- ///
- /// Thrown when the sender has been disposed
- ///
- protected HttpClient GetOrCreateClient() {
- ThrowIfDisposed();
-
- if (httpClientFactory != null) {
- if (String.IsNullOrWhiteSpace(HttpClientName))
- return httpClientFactory.CreateClient();
-
- return httpClientFactory.CreateClient(HttpClientName);
- }
-
- if (httpClient == null) {
- httpClient = new HttpClient();
- disposeClient = true;
- }
-
- return httpClient;
- }
-
- ///
- /// Creates a retry policy for the given number of retries and the sleep time
- ///
- ///
- /// The number of retries to perform
- ///
- ///
- /// The time to sleep between retries
- ///
- ///
- protected IAsyncPolicy CreateRetryPolicy(int? retryCount, TimeSpan? sleep) {
- // TODO: Validate that the sum of the retry delays is less than the timeout
- var retryCountValue = (retryCount ?? Retry?.MaxRetries) ?? 0;
- var sleepValue = (sleep ?? Retry?.MaxDelay) ?? TimeSpan.FromMilliseconds(300);
-
- // the retry policy
- return Policy
- .Handle()
- .Or()
- .Or()
- .WaitAndRetryAsync(retryCountValue, attempt => sleepValue);
- }
-
- ///
- /// Creates a timeout policy for a single try for the given time
- ///
- ///
- /// The type of the result of the policy execution
- ///
- ///
- /// The timeout to apply
- ///
- ///
- /// Returns a policy that will timeout the execution after the given time
- ///
- protected AsyncPolicy CreateTryTimeoutPolicy(TimeSpan? timeout) {
- // TODO: Validate that the timeout is not less than the retry timeout
- var timeoutValue = (timeout ?? Retry?.Timeout) ?? System.Threading.Timeout.InfiniteTimeSpan;
- return Policy.TimeoutAsync(timeoutValue);
- }
-
- ///
- /// Creates a timeout policy for the given time
- ///
- ///
- /// Returns a policy that will timeout the execution after the given time
- ///
- protected AsyncPolicy CreateTimeoutPolicy() {
- // TODO: Validate that the timeout is not less than the retry timeout
- var timeOut = Timeout ?? System.Threading.Timeout.InfiniteTimeSpan;
- return Policy.TimeoutAsync(timeOut);
- }
-
- ///
- /// Sends the request to the given request through the HTTP channel.
- ///
- ///
- /// The HTTP request to be sent.
- ///
- ///
- /// A cancellation token that can be used to cancel the operation.
- ///
- ///
- /// Returns a response message that was received from the remote destination.
- ///
- protected virtual Task SendRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
- // we don't dispose the client, because it might be a singleton...
- // 1. if the client is coming from the IHttpClientFactory, it will be disposed by the factory
- // 2. if the client was set at the constructor, it will be disposed by the caller
- // 3. if the client was created by the sender, it will be disposed when the sender is disposed
-
- var client = GetOrCreateClient();
- return client.SendAsync(request, cancellationToken);
- }
-
- ///
- /// Deisposes the sender.
- ///
- ///
- /// Whether the method is called from the method.
- ///
- protected virtual void Dispose(bool disposing) {
- if (!disposed) {
- if (disposing && disposeClient) {
- httpClient?.Dispose();
- }
-
- disposed = true;
- }
- }
-
- ///
- public void Dispose() {
- Dispose(true);
- GC.SuppressFinalize(this);
- }
- }
-}
diff --git a/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderOptions.cs b/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderOptions.cs
index 33f1413..b83c91d 100644
--- a/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderOptions.cs
+++ b/src/Deveel.Webhooks.Sender/Webhooks/WebhookSenderOptions.cs
@@ -17,13 +17,6 @@ namespace Deveel.Webhooks {
/// Provides configurations for the webhooks sender service.
///
public class WebhookSenderOptions where TWebhook : class {
- ///
- /// Gets or sets the name of the HTTP client registered in the
- /// factory pool and that will be used to send the webhooks.
- /// When this is not provided, the default HTTP client is used.
- ///
- public string? HttpClientName { get; set; }
-
///
/// Gets or sets the default headers to be sent with the webhook,
/// additionally to the ones specified in the webhook definition.
diff --git a/src/Deveel.Webhooks/Webhooks/WebhookSubscriptionExtensions.cs b/src/Deveel.Webhooks/Webhooks/WebhookSubscriptionExtensions.cs
index 0d5b123..788765b 100644
--- a/src/Deveel.Webhooks/Webhooks/WebhookSubscriptionExtensions.cs
+++ b/src/Deveel.Webhooks/Webhooks/WebhookSubscriptionExtensions.cs
@@ -12,12 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
namespace Deveel.Webhooks {
///
/// Extends the to provide some helper methods.
@@ -35,6 +29,7 @@ public static class WebhookSubscriptionExtensions {
///
public static WebhookDestination AsDestination(this IWebhookSubscription subscription) {
var destination = new WebhookDestination(subscription.DestinationUrl) {
+ Name = subscription.Name,
Headers = subscription.Headers?.ToDictionary(x => x.Key, x => x.Value)
};
diff --git a/test/Deveel.Webhooks.MongoDb.XUnit/Deveel.Webhooks.MongoDb.XUnit.csproj b/test/Deveel.Webhooks.MongoDb.XUnit/Deveel.Webhooks.MongoDb.XUnit.csproj
index 4178fdc..258ad71 100644
--- a/test/Deveel.Webhooks.MongoDb.XUnit/Deveel.Webhooks.MongoDb.XUnit.csproj
+++ b/test/Deveel.Webhooks.MongoDb.XUnit/Deveel.Webhooks.MongoDb.XUnit.csproj
@@ -17,5 +17,6 @@
+
diff --git a/test/Deveel.Webhooks.MongoDb.XUnit/Util/IHttpRequestCallback.cs b/test/Deveel.Webhooks.MongoDb.XUnit/Util/IHttpRequestCallback.cs
deleted file mode 100644
index a1b0649..0000000
--- a/test/Deveel.Webhooks.MongoDb.XUnit/Util/IHttpRequestCallback.cs
+++ /dev/null
@@ -1,19 +0,0 @@
-// Copyright 2022-2023 Deveel
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-namespace Deveel.Util {
- interface IHttpRequestCallback {
- Task RequestsAsync(HttpRequestMessage request, CancellationToken cancellationToken);
- }
-}
diff --git a/test/Deveel.Webhooks.MongoDb.XUnit/Util/ServiceCollectionExtensions.cs b/test/Deveel.Webhooks.MongoDb.XUnit/Util/ServiceCollectionExtensions.cs
deleted file mode 100644
index eaf4cfa..0000000
--- a/test/Deveel.Webhooks.MongoDb.XUnit/Util/ServiceCollectionExtensions.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright 2022-2023 Deveel
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-
-using Microsoft.Extensions.DependencyInjection;
-
-using RichardSzalay.MockHttp;
-
-namespace Deveel.Util {
- static class ServiceCollectionExtensions {
- public static IServiceCollection AddTestHttpClient(this IServiceCollection services, IHttpRequestCallback callback) {
- return services.AddSingleton(provider => {
- var factory = new MockHttpClientFactory();
- var handler = new MockHttpMessageHandler();
- handler.When("*")
- .Respond(request => callback.RequestsAsync(request, default));
-
- var client = handler.ToHttpClient();
- factory.AddClient("", client);
-
- return factory;
- });
- }
-
- public static IServiceCollection AddTestHttpClient(this IServiceCollection services, Func callback)
- => services.AddTestHttpClient(new TestHttpRequestCallback(callback));
-
- public static IServiceCollection AddTestHttpClient(this IServiceCollection services, Func> callback)
- => services.AddTestHttpClient(new TestHttpRequestAsyncCallback(callback));
-
- public static IServiceCollection AddTestHttpClient(this IServiceCollection services, Func> callback)
- => services.AddTestHttpClient(new TestHttpRequestAsyncCallback(callback));
-
-
- public static IServiceCollection AddTestHttpClient(this IServiceCollection services)
- => services.AddTestHttpClient(request => new HttpResponseMessage(HttpStatusCode.OK));
-
- class MockHttpClientFactory : IHttpClientFactory {
- public MockHttpClientFactory() {
-
- }
-
- public void AddClient(string name, HttpClient client) {
- clients.Add(name, client);
- }
-
- private readonly Dictionary clients = new Dictionary();
-
- public HttpClient CreateClient(string name) {
- if (!clients.TryGetValue(name, out var client))
- throw new Exception($"No client with name '{name}' was registered");
-
- return client;
- }
- }
- }
-}
diff --git a/test/Deveel.Webhooks.MongoDb.XUnit/Util/TestHttpRequestCallback.cs b/test/Deveel.Webhooks.MongoDb.XUnit/Util/TestHttpRequestCallback.cs
deleted file mode 100644
index 28a83a0..0000000
--- a/test/Deveel.Webhooks.MongoDb.XUnit/Util/TestHttpRequestCallback.cs
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright 2022-2023 Deveel
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Text;
-using System.Threading.Tasks;
-using System.Threading;
-
-namespace Deveel.Util {
- class TestHttpRequestCallback : IHttpRequestCallback {
- private readonly Func func;
-
- public TestHttpRequestCallback(Func func) {
- this.func = func;
- }
-
- public Task RequestsAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
- var response = func(request);
- return Task.FromResult(response);
- }
-
- }
-
- class TestHttpRequestAsyncCallback : IHttpRequestCallback {
- private readonly Func>? func;
- private readonly Func>? cancellableFunc;
-
- public TestHttpRequestAsyncCallback(Func> func) {
- cancellableFunc = func;
- }
-
- public TestHttpRequestAsyncCallback(Func> func) {
- this.func = func;
- }
-
- public Task RequestsAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
- if (func != null)
- return func(request);
-
- if (cancellableFunc != null)
- return cancellableFunc(request, cancellationToken);
-
- throw new InvalidOperationException();
- }
- }
-}
diff --git a/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoDbWebhookTestBase.cs b/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoDbWebhookTestBase.cs
index 050215c..dc8fce9 100644
--- a/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoDbWebhookTestBase.cs
+++ b/test/Deveel.Webhooks.MongoDb.XUnit/Webhooks/MongoDbWebhookTestBase.cs
@@ -14,8 +14,6 @@
using System.Net;
-using Deveel.Util;
-
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -60,7 +58,7 @@ public virtual async Task DisposeAsync() {
private IServiceProvider BuildServiceProvider(ITestOutputHelper outputHelper) {
var services = new ServiceCollection()
.AddWebhookSubscriptions(buidler => ConfigureWebhookService(buidler))
- .AddTestHttpClient(OnRequestAsync)
+ .AddHttpCallback(OnRequestAsync)
.AddLogging(logging => logging.AddXUnit(outputHelper));
ConfigureServices(services);
diff --git a/test/Deveel.Webhooks.Sender.XUnit/Deveel.Webhooks.Sender.XUnit.csproj b/test/Deveel.Webhooks.Sender.XUnit/Deveel.Webhooks.Sender.XUnit.csproj
index ea68c06..ec4a2a4 100644
--- a/test/Deveel.Webhooks.Sender.XUnit/Deveel.Webhooks.Sender.XUnit.csproj
+++ b/test/Deveel.Webhooks.Sender.XUnit/Deveel.Webhooks.Sender.XUnit.csproj
@@ -10,12 +10,13 @@
-
-
+
+
+
diff --git a/test/Deveel.Webhooks.Sender.XUnit/MockHttpClientFactory.cs b/test/Deveel.Webhooks.Sender.XUnit/MockHttpClientFactory.cs
deleted file mode 100644
index 5e1b56d..0000000
--- a/test/Deveel.Webhooks.Sender.XUnit/MockHttpClientFactory.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright 2022-2023 Deveel
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Deveel {
- class MockHttpClientFactory : IHttpClientFactory, IDisposable {
- private Dictionary clients = new Dictionary();
-
- public MockHttpClientFactory(string name, HttpClient client) {
- RegisterClient(name, client);
- }
-
- public MockHttpClientFactory() {
- }
-
- public HttpClient CreateClient(string name) {
- if (!clients.TryGetValue(name, out var client))
- throw new ArgumentException($"No client with name '{name}' was registered.");
-
- return client;
- }
-
- public void RegisterClient(string name, HttpClient client) {
- clients.Add(name, client);
- }
-
- public void Dispose() {
- foreach (var client in clients.Values) {
- client.Dispose();
- }
- }
- }
-}
diff --git a/test/Deveel.Webhooks.Sender.XUnit/Webhooks/WebhookSenderTests.cs b/test/Deveel.Webhooks.Sender.XUnit/Webhooks/WebhookSenderTests.cs
index c69c9b4..b189a3d 100644
--- a/test/Deveel.Webhooks.Sender.XUnit/Webhooks/WebhookSenderTests.cs
+++ b/test/Deveel.Webhooks.Sender.XUnit/Webhooks/WebhookSenderTests.cs
@@ -41,7 +41,7 @@ public WebhookSenderTests(ITestOutputHelper outputHelper) {
}
private IServiceProvider ConfigureServices(ITestOutputHelper outputHelper) {
- var retryTimeoutMs = 500;
+ var retryTimeoutMs = TimeSpan.FromSeconds(1).TotalMilliseconds;
Func> readContent = async request => {
TestWebhook? webhook;
@@ -124,10 +124,11 @@ private IServiceProvider ConfigureServices(ITestOutputHelper outputHelper) {
var services = new ServiceCollection()
- .AddSingleton(new MockHttpClientFactory("", mockHandler.ToHttpClient()))
.AddLogging(logging => logging.AddXUnit(outputHelper, options => options.Filter = (cat, level) => true)
.SetMinimumLevel(LogLevel.Trace));
+ services.AddTestHttpClientFacoty(mockHandler);
+
services.AddWebhookSender(options => {
options.DefaultHeaders = new Dictionary {
{"X-Test", "true"}
diff --git a/test/Deveel.Webhooks.TestHttpClient/Deveel.Webhooks.TestHttpClient.csproj b/test/Deveel.Webhooks.TestHttpClient/Deveel.Webhooks.TestHttpClient.csproj
new file mode 100644
index 0000000..bfbd79d
--- /dev/null
+++ b/test/Deveel.Webhooks.TestHttpClient/Deveel.Webhooks.TestHttpClient.csproj
@@ -0,0 +1,19 @@
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/test/Deveel.Webhooks.TestHttpClient/IHttpRequestCallback.cs b/test/Deveel.Webhooks.TestHttpClient/IHttpRequestCallback.cs
new file mode 100644
index 0000000..d851916
--- /dev/null
+++ b/test/Deveel.Webhooks.TestHttpClient/IHttpRequestCallback.cs
@@ -0,0 +1,5 @@
+namespace Deveel {
+ public interface IHttpRequestCallback {
+ Task HandleRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken);
+ }
+}
diff --git a/test/Deveel.Webhooks.TestHttpClient/ServiceCollectionExtensions.cs b/test/Deveel.Webhooks.TestHttpClient/ServiceCollectionExtensions.cs
new file mode 100644
index 0000000..58f7746
--- /dev/null
+++ b/test/Deveel.Webhooks.TestHttpClient/ServiceCollectionExtensions.cs
@@ -0,0 +1,105 @@
+using System.Net;
+
+using Microsoft.Extensions.DependencyInjection;
+
+using RichardSzalay.MockHttp;
+
+namespace Deveel {
+ public static class ServiceCollectionExtensions {
+ public static IServiceCollection AddTestHttpCallback(this IServiceCollection services, IHttpRequestCallback callback) {
+ var handler = new MockHttpMessageHandler();
+ handler.When("*")
+ .Respond(request => callback.HandleRequestAsync(request, default));
+
+ return services.AddTestHttpMessageHandlerFactory(handler)
+ .AddTestHttpClientFacoty(handler);
+ }
+
+ public static IServiceCollection AddTestHttpMessageHandlerFactory(this IServiceCollection services, HttpMessageHandler handler) {
+ services.AddSingleton(new TestHttpMessageHandlerFactory(handler));
+
+ return services;
+ }
+
+ public static IServiceCollection AddTestHttpClientFacoty(this IServiceCollection services, HttpMessageHandler handler) {
+ services.AddSingleton(new TestHttpClientFactory(handler));
+
+ return services;
+ }
+
+ public static IServiceCollection AddTestHttpCallback(this IServiceCollection services, Func callback)
+ => services.AddTestHttpCallback(new TestHttpRequestCallback(callback));
+
+ public static IServiceCollection AddHttpCallback(this IServiceCollection services, Func> callback)
+ => services.AddTestHttpCallback(new TestHttpRequestAsyncCallback(callback));
+
+ public static IServiceCollection AddHttpCallback(this IServiceCollection services, Func> callback)
+ => services.AddTestHttpCallback(new TestHttpRequestAsyncCallback(callback));
+
+
+ public static IServiceCollection AddHttpCallback(this IServiceCollection services)
+ => services.AddTestHttpCallback(request => new HttpResponseMessage(HttpStatusCode.OK));
+
+
+ class TestHttpMessageHandlerFactory : IHttpMessageHandlerFactory {
+ private readonly HttpMessageHandler messageHandler;
+
+ public TestHttpMessageHandlerFactory(HttpMessageHandler messageHandler) {
+ this.messageHandler = messageHandler;
+ }
+
+ public HttpMessageHandler CreateHandler(string name) {
+ return messageHandler;
+ }
+ }
+
+ class TestHttpClientFactory : IHttpClientFactory {
+ private readonly HttpMessageHandler messageHandler;
+
+ public TestHttpClientFactory(HttpMessageHandler messageHandler) {
+ this.messageHandler = messageHandler;
+ }
+
+ public HttpClient CreateClient(string name) {
+ return new HttpClient(messageHandler, false);
+ }
+ }
+
+ class TestHttpRequestAsyncCallback : IHttpRequestCallback {
+ private readonly Func>? func;
+ private readonly Func>? cancellableFunc;
+
+ public TestHttpRequestAsyncCallback(Func> func) {
+ cancellableFunc = func;
+ }
+
+ public TestHttpRequestAsyncCallback(Func> func) {
+ this.func = func;
+ }
+
+ public Task HandleRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
+ if (func != null)
+ return func(request);
+
+ if (cancellableFunc != null)
+ return cancellableFunc(request, cancellationToken);
+
+ throw new InvalidOperationException();
+ }
+ }
+
+ class TestHttpRequestCallback : IHttpRequestCallback {
+ private readonly Func func;
+
+ public TestHttpRequestCallback(Func func) {
+ this.func = func;
+ }
+
+ public Task HandleRequestAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
+ var response = func(request);
+ return Task.FromResult(response);
+ }
+
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Deveel.Webhooks.XUnit/Deveel.Webhooks.XUnit.csproj b/test/Deveel.Webhooks.XUnit/Deveel.Webhooks.XUnit.csproj
index 2f0a679..caf54f2 100644
--- a/test/Deveel.Webhooks.XUnit/Deveel.Webhooks.XUnit.csproj
+++ b/test/Deveel.Webhooks.XUnit/Deveel.Webhooks.XUnit.csproj
@@ -24,6 +24,7 @@
+
diff --git a/test/Deveel.Webhooks.XUnit/Util/IHttpRequestCallback.cs b/test/Deveel.Webhooks.XUnit/Util/IHttpRequestCallback.cs
deleted file mode 100644
index 7d8e0fe..0000000
--- a/test/Deveel.Webhooks.XUnit/Util/IHttpRequestCallback.cs
+++ /dev/null
@@ -1,26 +0,0 @@
-// Copyright 2022-2023 Deveel
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Text;
-using System.Threading.Tasks;
-using System.Threading;
-
-namespace Deveel.Util {
- interface IHttpRequestCallback {
- Task RequestsAsync(HttpRequestMessage request, CancellationToken cancellationToken);
- }
-}
diff --git a/test/Deveel.Webhooks.XUnit/Util/ServiceCollectionExtensions.cs b/test/Deveel.Webhooks.XUnit/Util/ServiceCollectionExtensions.cs
deleted file mode 100644
index eaf4cfa..0000000
--- a/test/Deveel.Webhooks.XUnit/Util/ServiceCollectionExtensions.cs
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright 2022-2023 Deveel
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Collections.Generic;
-using System.Net;
-using System.Net.Http;
-using System.Threading;
-using System.Threading.Tasks;
-
-using Microsoft.Extensions.DependencyInjection;
-
-using RichardSzalay.MockHttp;
-
-namespace Deveel.Util {
- static class ServiceCollectionExtensions {
- public static IServiceCollection AddTestHttpClient(this IServiceCollection services, IHttpRequestCallback callback) {
- return services.AddSingleton(provider => {
- var factory = new MockHttpClientFactory();
- var handler = new MockHttpMessageHandler();
- handler.When("*")
- .Respond(request => callback.RequestsAsync(request, default));
-
- var client = handler.ToHttpClient();
- factory.AddClient("", client);
-
- return factory;
- });
- }
-
- public static IServiceCollection AddTestHttpClient(this IServiceCollection services, Func callback)
- => services.AddTestHttpClient(new TestHttpRequestCallback(callback));
-
- public static IServiceCollection AddTestHttpClient(this IServiceCollection services, Func> callback)
- => services.AddTestHttpClient(new TestHttpRequestAsyncCallback(callback));
-
- public static IServiceCollection AddTestHttpClient(this IServiceCollection services, Func> callback)
- => services.AddTestHttpClient(new TestHttpRequestAsyncCallback(callback));
-
-
- public static IServiceCollection AddTestHttpClient(this IServiceCollection services)
- => services.AddTestHttpClient(request => new HttpResponseMessage(HttpStatusCode.OK));
-
- class MockHttpClientFactory : IHttpClientFactory {
- public MockHttpClientFactory() {
-
- }
-
- public void AddClient(string name, HttpClient client) {
- clients.Add(name, client);
- }
-
- private readonly Dictionary clients = new Dictionary();
-
- public HttpClient CreateClient(string name) {
- if (!clients.TryGetValue(name, out var client))
- throw new Exception($"No client with name '{name}' was registered");
-
- return client;
- }
- }
- }
-}
diff --git a/test/Deveel.Webhooks.XUnit/Util/TestHttpRequestCallback.cs b/test/Deveel.Webhooks.XUnit/Util/TestHttpRequestCallback.cs
deleted file mode 100644
index b50fdee..0000000
--- a/test/Deveel.Webhooks.XUnit/Util/TestHttpRequestCallback.cs
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright 2022-2023 Deveel
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-using System;
-using System.Collections.Generic;
-using System.Net.Http;
-using System.Text;
-using System.Threading.Tasks;
-using System.Threading;
-
-namespace Deveel.Util {
- class TestHttpRequestCallback : IHttpRequestCallback {
- private readonly Func func;
-
- public TestHttpRequestCallback(Func func) {
- this.func = func;
- }
-
- public Task RequestsAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
- var response = func(request);
- return Task.FromResult(response);
- }
-
- }
-
- class TestHttpRequestAsyncCallback : IHttpRequestCallback {
- private readonly Func>? func;
- private readonly Func>? cancellableFunc;
-
- public TestHttpRequestAsyncCallback(Func> func) {
- cancellableFunc = func;
- }
-
- public TestHttpRequestAsyncCallback(Func> func) {
- this.func = func;
- }
-
- public Task RequestsAsync(HttpRequestMessage request, CancellationToken cancellationToken) {
- if (func != null)
- return func(request);
- if (cancellableFunc != null)
- return cancellableFunc(request, cancellationToken);
-
- throw new InvalidOperationException();
- }
- }
-}
diff --git a/test/Deveel.Webhooks.XUnit/Webhooks/TenantWebhookNotificationTests.cs b/test/Deveel.Webhooks.XUnit/Webhooks/TenantWebhookNotificationTests.cs
index e0eaf72..5f1c227 100644
--- a/test/Deveel.Webhooks.XUnit/Webhooks/TenantWebhookNotificationTests.cs
+++ b/test/Deveel.Webhooks.XUnit/Webhooks/TenantWebhookNotificationTests.cs
@@ -25,7 +25,7 @@ namespace Deveel.Webhooks {
[Trait("Category", "Webhooks")]
[Trait("Category", "Notification")]
public class TenantWebhookNotificationTests : WebhookServiceTestBase {
- private const int TimeOutSeconds = 2;
+ private const int TimeOutSeconds = 3;
private bool testTimeout = false;
private readonly string tenantId = Guid.NewGuid().ToString();
@@ -48,6 +48,7 @@ protected override void ConfigureServices(IServiceCollection services) {
.UseTenantSubscriptionResolver(ServiceLifetime.Singleton)
.UseSender(options => {
options.Retry.MaxRetries = 2;
+ options.Retry.MaxDelay = TimeSpan.FromMilliseconds(200);
options.Retry.Timeout = TimeSpan.FromSeconds(TimeOutSeconds);
}));
diff --git a/test/Deveel.Webhooks.XUnit/Webhooks/WebhookServiceTestBase.cs b/test/Deveel.Webhooks.XUnit/Webhooks/WebhookServiceTestBase.cs
index 8a6af25..7cb03eb 100644
--- a/test/Deveel.Webhooks.XUnit/Webhooks/WebhookServiceTestBase.cs
+++ b/test/Deveel.Webhooks.XUnit/Webhooks/WebhookServiceTestBase.cs
@@ -17,8 +17,6 @@
using System.Net.Http;
using System.Threading.Tasks;
-using Deveel.Util;
-
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
@@ -35,7 +33,7 @@ protected WebhookServiceTestBase(ITestOutputHelper outputHelper) {
private IServiceProvider BuildServiceProvider(ITestOutputHelper outputHelper) {
var services = new ServiceCollection()
.AddWebhookSubscriptions(buidler => ConfigureWebhookService(buidler))
- .AddTestHttpClient(OnRequestAsync)
+ .AddHttpCallback(OnRequestAsync)
.AddLogging(logging => logging.AddXUnit(outputHelper).SetMinimumLevel(LogLevel.Trace));
ConfigureServices(services);