diff --git a/.gitattributes b/.gitattributes index 50ca329f..2e46fbac 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,2 @@ +* text=auto *.sh eol=lf diff --git a/src/HttpClientInterception/DelegateHelpers.cs b/src/HttpClientInterception/DelegateHelpers.cs index 008e422a..308ba99f 100644 --- a/src/HttpClientInterception/DelegateHelpers.cs +++ b/src/HttpClientInterception/DelegateHelpers.cs @@ -3,6 +3,7 @@ using System; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace JustEat.HttpClientInterception @@ -25,14 +26,14 @@ internal static class DelegateHelpers /// /// The converted delegate if has a value; otherwise . /// - internal static Func>? ConvertToBooleanTask(Action? onIntercepted) + internal static Func>? ConvertToBooleanTask(Action? onIntercepted) { if (onIntercepted == null) { return null; } - return (message) => + return (message, _) => { onIntercepted(message); return TrueTask; @@ -47,14 +48,14 @@ internal static class DelegateHelpers /// /// The converted delegate if has a value; otherwise . /// - internal static Func>? ConvertToBooleanTask(Predicate? onIntercepted) + internal static Func>? ConvertToBooleanTask(Predicate? onIntercepted) { if (onIntercepted == null) { return null; } - return (message) => Task.FromResult(onIntercepted(message)); + return (message, _) => Task.FromResult(onIntercepted(message)); } /// @@ -65,18 +66,54 @@ internal static class DelegateHelpers /// /// The converted delegate if has a value; otherwise . /// - internal static Func>? ConvertToBooleanTask(Func? onIntercepted) + internal static Func>? ConvertToBooleanTask(Func? onIntercepted) { if (onIntercepted == null) { return null; } - return async (message) => + return ConvertToBooleanTask((message, _) => onIntercepted(message)); + } + + /// + /// Converts a function delegate for an intercepted message to return a + /// which returns . + /// + /// An optional delegate to convert. + /// + /// The converted delegate if has a value; otherwise . + /// + internal static Func>? ConvertToBooleanTask(Func? onIntercepted) + { + if (onIntercepted == null) + { + return null; + } + + return async (message, token) => { - await onIntercepted(message).ConfigureAwait(false); + await onIntercepted(message, token).ConfigureAwait(false); return true; }; } + + /// + /// Converts a function delegate for an intercepted message to return a + /// which returns . + /// + /// An optional delegate to convert. + /// + /// The converted delegate if has a value; otherwise . + /// + internal static Func>? ConvertToBooleanTask(Func>? onIntercepted) + { + if (onIntercepted == null) + { + return null; + } + + return async (message, _) => await onIntercepted(message).ConfigureAwait(false); + } } } diff --git a/src/HttpClientInterception/HttpClientInterceptorOptions.cs b/src/HttpClientInterception/HttpClientInterceptorOptions.cs index a5ef2fbe..b131c0b1 100644 --- a/src/HttpClientInterception/HttpClientInterceptorOptions.cs +++ b/src/HttpClientInterception/HttpClientInterceptorOptions.cs @@ -330,6 +330,7 @@ public HttpClientInterceptorOptions Register(HttpRequestInterceptionBuilder buil /// Gets the HTTP response, if any, set up for the specified HTTP request as an asynchronous operation. /// /// The HTTP request to try and get the intercepted response for. + /// The optional token to monitor for cancellation requests. /// /// A that returns the HTTP response to use, if any, /// for ; otherwise . @@ -337,7 +338,7 @@ public HttpClientInterceptorOptions Register(HttpRequestInterceptionBuilder buil /// /// is . /// - public virtual async Task GetResponseAsync(HttpRequestMessage request) + public virtual async Task GetResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { if (request == null) { @@ -354,7 +355,7 @@ public HttpClientInterceptorOptions Register(HttpRequestInterceptionBuilder buil var response = matchResult.Item2; // If Item1 is true, then Item2 is non-null - if (response!.OnIntercepted != null && !await response.OnIntercepted(request).ConfigureAwait(false)) + if (response!.OnIntercepted != null && !await response.OnIntercepted(request, cancellationToken).ConfigureAwait(false)) { return null; } diff --git a/src/HttpClientInterception/HttpInterceptionResponse.cs b/src/HttpClientInterception/HttpInterceptionResponse.cs index 420b1a06..9a9d5a52 100644 --- a/src/HttpClientInterception/HttpInterceptionResponse.cs +++ b/src/HttpClientInterception/HttpInterceptionResponse.cs @@ -6,6 +6,7 @@ using System.IO; using System.Net; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace JustEat.HttpClientInterception @@ -48,7 +49,7 @@ internal sealed class HttpInterceptionResponse internal IEnumerable>>? ResponseHeaders { get; set; } - internal Func>? OnIntercepted { get; set; } + internal Func>? OnIntercepted { get; set; } internal Version? Version { get; set; } } diff --git a/src/HttpClientInterception/HttpRequestInterceptionBuilder.cs b/src/HttpClientInterception/HttpRequestInterceptionBuilder.cs index afaf6738..20b1d72b 100644 --- a/src/HttpClientInterception/HttpRequestInterceptionBuilder.cs +++ b/src/HttpClientInterception/HttpRequestInterceptionBuilder.cs @@ -6,6 +6,7 @@ using System.IO; using System.Net; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; namespace JustEat.HttpClientInterception @@ -35,7 +36,7 @@ public class HttpRequestInterceptionBuilder private HttpMethod _method = HttpMethod.Get; - private Func>? _onIntercepted; + private Func>? _onIntercepted; private Func>? _requestMatcher; @@ -79,7 +80,7 @@ public HttpRequestInterceptionBuilder() /// public HttpRequestInterceptionBuilder For(Predicate predicate) { - _requestMatcher = predicate == null ? null : DelegateHelpers.ConvertToBooleanTask(predicate); + _requestMatcher = predicate == null ? null : new Func>((message) => Task.FromResult(predicate(message))); return this; } @@ -690,6 +691,39 @@ public HttpRequestInterceptionBuilder WithInterceptionCallback(Func. /// public HttpRequestInterceptionBuilder WithInterceptionCallback(Func> onIntercepted) + { + _onIntercepted = DelegateHelpers.ConvertToBooleanTask(onIntercepted); + return this; + } + + /// + /// Sets an asynchronous callback to use to use when a request is intercepted that returns + /// if the request should be intercepted or + /// if the request should not be intercepted. + /// + /// A delegate to a method to await when a request is intercepted. + /// + /// The current . + /// + public HttpRequestInterceptionBuilder WithInterceptionCallback(Func onIntercepted) + { + _onIntercepted = DelegateHelpers.ConvertToBooleanTask(onIntercepted); + return this; + } + + /// + /// Sets an asynchronous callback to use to use when a request is intercepted that returns + /// if the request should be intercepted or + /// if the request should not be intercepted. + /// + /// + /// A delegate to a method to await when a request is intercepted which returns a + /// indicating whether the request should be intercepted or not. + /// + /// + /// The current . + /// + public HttpRequestInterceptionBuilder WithInterceptionCallback(Func> onIntercepted) { _onIntercepted = onIntercepted; return this; diff --git a/src/HttpClientInterception/InterceptingHttpMessageHandler.cs b/src/HttpClientInterception/InterceptingHttpMessageHandler.cs index da2718d3..c5ecb2d7 100644 --- a/src/HttpClientInterception/InterceptingHttpMessageHandler.cs +++ b/src/HttpClientInterception/InterceptingHttpMessageHandler.cs @@ -54,7 +54,7 @@ protected override async Task SendAsync(HttpRequestMessage await _options.OnSend(request).ConfigureAwait(false); } - var response = await _options.GetResponseAsync(request).ConfigureAwait(false); + var response = await _options.GetResponseAsync(request, cancellationToken).ConfigureAwait(false); if (response == null && _options.OnMissingRegistration != null) { diff --git a/src/HttpClientInterception/PublicAPI.Shipped.txt b/src/HttpClientInterception/PublicAPI.Shipped.txt index 04227da3..232093da 100644 --- a/src/HttpClientInterception/PublicAPI.Shipped.txt +++ b/src/HttpClientInterception/PublicAPI.Shipped.txt @@ -48,6 +48,8 @@ JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithContentStream( JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Action onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Func> onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Func onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder +JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Func> onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder +JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Func onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithInterceptionCallback(System.Predicate onIntercepted) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithMediaType(string mediaType) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder JustEat.HttpClientInterception.HttpRequestInterceptionBuilder.WithReason(string reasonPhrase) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder @@ -88,4 +90,4 @@ static JustEat.HttpClientInterception.HttpRequestInterceptionBuilderExtensions.W static JustEat.HttpClientInterception.HttpRequestInterceptionBuilderExtensions.WithFormContent(this JustEat.HttpClientInterception.HttpRequestInterceptionBuilder builder, System.Collections.Generic.IEnumerable> parameters) -> JustEat.HttpClientInterception.HttpRequestInterceptionBuilder virtual JustEat.HttpClientInterception.HttpClientInterceptorOptions.CreateHttpClient(System.Net.Http.HttpMessageHandler innerHandler = null) -> System.Net.Http.HttpClient virtual JustEat.HttpClientInterception.HttpClientInterceptorOptions.CreateHttpMessageHandler() -> System.Net.Http.DelegatingHandler -virtual JustEat.HttpClientInterception.HttpClientInterceptorOptions.GetResponseAsync(System.Net.Http.HttpRequestMessage request) -> System.Threading.Tasks.Task +virtual JustEat.HttpClientInterception.HttpClientInterceptorOptions.GetResponseAsync(System.Net.Http.HttpRequestMessage request, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.Task diff --git a/tests/HttpClientInterception.Tests/Examples.cs b/tests/HttpClientInterception.Tests/Examples.cs index 6f295efe..79191559 100644 --- a/tests/HttpClientInterception.Tests/Examples.cs +++ b/tests/HttpClientInterception.Tests/Examples.cs @@ -7,6 +7,7 @@ using System.Net; using System.Net.Http; using System.Text.Json; +using System.Threading; using System.Threading.Tasks; using JustEat.HttpClientInterception.GitHub; using Newtonsoft.Json.Linq; @@ -627,5 +628,44 @@ public static async Task Intercept_Http_Get_For_Json_Object_Using_System_Text_Js content.RootElement.GetProperty("Id").GetInt32().ShouldBe(1); content.RootElement.GetProperty("Link").GetString().ShouldBe("https://www.just-eat.co.uk/privacy-policy"); } + + [Fact] + public static async Task Inject_Latency_For_Http_Get_With_Cancellation() + { + // Arrange + var latency = TimeSpan.FromMilliseconds(50); + + var builder = new HttpRequestInterceptionBuilder() + .ForHost("www.google.co.uk") + .WithInterceptionCallback(async (_, token) => + { + try + { + await Task.Delay(latency, token); + } + catch (TaskCanceledException) + { + // Ignored + } + finally + { + // Assert + token.IsCancellationRequested.ShouldBeTrue(); + } + }); + + var options = new HttpClientInterceptorOptions() + .Register(builder); + + using var cts = new CancellationTokenSource(TimeSpan.Zero); + + using var client = options.CreateHttpClient(); + + // Act + await client.GetAsync("http://www.google.co.uk", cts.Token); + + // Assert + cts.IsCancellationRequested.ShouldBeTrue(); + } } } diff --git a/tests/HttpClientInterception.Tests/HttpClientInterceptorOptionsTests.cs b/tests/HttpClientInterception.Tests/HttpClientInterceptorOptionsTests.cs index a74339ac..b16b866e 100644 --- a/tests/HttpClientInterception.Tests/HttpClientInterceptorOptionsTests.cs +++ b/tests/HttpClientInterception.Tests/HttpClientInterceptorOptionsTests.cs @@ -8,6 +8,7 @@ using System.Net; using System.Net.Http; using System.Net.Http.Headers; +using System.Threading; using System.Threading.Tasks; using Shouldly; using Xunit; @@ -762,9 +763,9 @@ internal HeaderMatchingOptions(Predicate matchHeaders) _matchHeaders = matchHeaders; } - public override async Task GetResponseAsync(HttpRequestMessage request) + public override async Task GetResponseAsync(HttpRequestMessage request, CancellationToken cancellationToken = default) { - HttpResponseMessage response = await base.GetResponseAsync(request); + HttpResponseMessage response = await base.GetResponseAsync(request, cancellationToken); if (response != null && _matchHeaders(request.Headers)) { diff --git a/tests/HttpClientInterception.Tests/HttpRequestInterceptionBuilderTests.cs b/tests/HttpClientInterception.Tests/HttpRequestInterceptionBuilderTests.cs index b63fa682..739026a8 100644 --- a/tests/HttpClientInterception.Tests/HttpRequestInterceptionBuilderTests.cs +++ b/tests/HttpClientInterception.Tests/HttpRequestInterceptionBuilderTests.cs @@ -1016,6 +1016,35 @@ Task OnInterceptedAsync(HttpRequestMessage request) wasDelegateInvoked.ShouldBeTrue(); } + [Fact] + public static async Task Register_For_Callback_Invokes_Delegate_With_CancellationToken_And_Intercepts_If_Returns_True() + { + // Arrange + var requestUri = new Uri("https://api.just-eat.com/"); + var content = new { foo = "bar" }; + + bool wasDelegateInvoked = false; + + Task OnInterceptedAsync(HttpRequestMessage request, CancellationToken token) + { + wasDelegateInvoked = true; + return Task.FromResult(true); + } + + var builder = new HttpRequestInterceptionBuilder() + .ForPost() + .ForUri(requestUri) + .WithInterceptionCallback(OnInterceptedAsync); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act + await HttpAssert.PostAsync(options, requestUri.ToString(), content); + + // Assert + wasDelegateInvoked.ShouldBeTrue(); + } + [Fact] public static async Task Register_For_Callback_Invokes_Delegate_And_Does_Not_Intercept_If_Returns_False() { @@ -1049,6 +1078,39 @@ Task OnInterceptedAsync(HttpRequestMessage request) wasDelegateInvoked.ShouldBeTrue(); } + [Fact] + public static async Task Register_For_Callback_Invokes_Delegate_With_CancellationToken_And_Does_Not_Intercept_If_Returns_False() + { + // Arrange + var requestUri = new Uri("https://api.just-eat.com/"); + var content = new { foo = "bar" }; + + bool wasDelegateInvoked = false; + + Task OnInterceptedAsync(HttpRequestMessage request, CancellationToken token) + { + wasDelegateInvoked = true; + return Task.FromResult(false); + } + + var builder = new HttpRequestInterceptionBuilder() + .ForPost() + .ForUri(requestUri) + .WithInterceptionCallback(OnInterceptedAsync); + + var options = new HttpClientInterceptorOptions() + .ThrowsOnMissingRegistration() + .Register(builder); + + // Act + var exception = await Assert.ThrowsAsync( + () => HttpAssert.PostAsync(options, requestUri.ToString(), content)); + + // Assert + exception.Message.ShouldStartWith("No HTTP response is configured for "); + wasDelegateInvoked.ShouldBeTrue(); + } + [Fact] public static async Task Register_For_Callback_Clears_Delegate_For_Action_If_Set_To_Null() { @@ -1097,6 +1159,54 @@ public static async Task Register_For_Callback_Clears_Delegate_For_Predicate_If_ wasDelegateInvoked.ShouldBeFalse(); } + [Fact] + public static async Task Register_For_Callback_Clears_Delegate_For_Predicate_With_Cancellation_If_Set_To_Null() + { + // Arrange + var requestUri = new Uri("https://api.just-eat.com/"); + var content = new { foo = "bar" }; + + bool wasDelegateInvoked = false; + + var builder = new HttpRequestInterceptionBuilder() + .ForPost() + .ForUri(requestUri) + .WithInterceptionCallback((request) => wasDelegateInvoked = true) + .WithInterceptionCallback(null as Func); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act + await HttpAssert.PostAsync(options, requestUri.ToString(), content); + + // Assert + wasDelegateInvoked.ShouldBeFalse(); + } + + [Fact] + public static async Task Register_For_Callback_Clears_Delegate_For_Async_Predicate_If_Set_To_Null() + { + // Arrange + var requestUri = new Uri("https://api.just-eat.com/"); + var content = new { foo = "bar" }; + + bool wasDelegateInvoked = false; + + var builder = new HttpRequestInterceptionBuilder() + .ForPost() + .ForUri(requestUri) + .WithInterceptionCallback((request) => wasDelegateInvoked = true) + .WithInterceptionCallback(null as Func>); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act + await HttpAssert.PostAsync(options, requestUri.ToString(), content); + + // Assert + wasDelegateInvoked.ShouldBeFalse(); + } + [Fact] public static async Task Builder_For_Any_Host_Registers_Interception() {