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);