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()
{