diff --git a/src/HttpClientInterception/HttpClientInterceptorOptions.cs b/src/HttpClientInterception/HttpClientInterceptorOptions.cs index b286cde6..7a881b1f 100644 --- a/src/HttpClientInterception/HttpClientInterceptorOptions.cs +++ b/src/HttpClientInterception/HttpClientInterceptorOptions.cs @@ -526,7 +526,8 @@ private static void PopulateHeaders(HttpHeaders headers, IEnumerable BuildResponseAsync(HttpRequestMessage request, HttpInterceptionResponse response) { - var result = new HttpResponseMessage(response.StatusCode); + var statusCode = response.StatusCodeFactory?.Invoke() ?? response.StatusCode; + var result = new HttpResponseMessage(statusCode); try { diff --git a/src/HttpClientInterception/HttpInterceptionResponse.cs b/src/HttpClientInterception/HttpInterceptionResponse.cs index 9e7bc044..49f70648 100644 --- a/src/HttpClientInterception/HttpInterceptionResponse.cs +++ b/src/HttpClientInterception/HttpInterceptionResponse.cs @@ -27,6 +27,8 @@ internal sealed class HttpInterceptionResponse internal Func>? ContentFactory { get; set; } + internal Func? StatusCodeFactory { get; set; } + internal Func>? ContentStream { get; set; } internal string? ContentMediaType { get; set; } diff --git a/src/HttpClientInterception/HttpRequestInterceptionBuilder.cs b/src/HttpClientInterception/HttpRequestInterceptionBuilder.cs index 4e184aa4..c4bdd918 100644 --- a/src/HttpClientInterception/HttpRequestInterceptionBuilder.cs +++ b/src/HttpClientInterception/HttpRequestInterceptionBuilder.cs @@ -40,6 +40,8 @@ public class HttpRequestInterceptionBuilder private HttpStatusCode _statusCode = HttpStatusCode.OK; + private Func? _statusCodeFactory; + private UriBuilder _uriBuilder = new(); private Version? _version; @@ -733,7 +735,34 @@ public HttpRequestInterceptionBuilder WithStatus(int statusCode) public HttpRequestInterceptionBuilder WithStatus(HttpStatusCode statusCode) { _statusCode = statusCode; + _statusCodeFactory = null; + IncrementRevision(); + return this; + } + + /// + /// Sets the function to use to build the response status code. + /// + /// A delegate to a method that returns the HTTP status code. + /// + /// The current . + /// + /// + /// Pass a value of to reset to no status code factory. + /// + public HttpRequestInterceptionBuilder WithStatus(Func? statusCodeFactory) + { + if (statusCodeFactory is null) + { + _statusCodeFactory = null; + } + else + { + _statusCodeFactory = statusCodeFactory; + } + IncrementRevision(); + return this; } diff --git a/tests/HttpClientInterception.Tests/HttpRequestInterceptionBuilderTests.cs b/tests/HttpClientInterception.Tests/HttpRequestInterceptionBuilderTests.cs index c096b86a..15244495 100644 --- a/tests/HttpClientInterception.Tests/HttpRequestInterceptionBuilderTests.cs +++ b/tests/HttpClientInterception.Tests/HttpRequestInterceptionBuilderTests.cs @@ -2585,6 +2585,259 @@ public static async Task Can_Do_Custom_Matching_Even_Number_Requests_Return_200_ fourthResponse.StatusCode.ShouldBe(HttpStatusCode.TooManyRequests); } + [Fact] + public static async Task WithStatus_Factory_Returns_Dynamic_Status_Code() + { + // Arrange + var requestUri = "https://test.local/post"; + var expected = "Dynamic content"; + var callCount = 0; + + var builder = new HttpRequestInterceptionBuilder() + .ForUrl(requestUri) + .WithStatus(() => + { + callCount++; + return callCount == 1 ? HttpStatusCode.OK : HttpStatusCode.NotFound; + }) + .WithContent(expected); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act + var response1 = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.OK); + var response2 = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.NotFound); + + // Assert + response1.ShouldBe(expected); + response2.ShouldBe(expected); + callCount.ShouldBe(2); + } + + [Fact] + public static async Task WithStatus_Factory_With_Null_Resets_To_Static_Status_Code() + { + // Arrange + var requestUri = "https://test.local/post"; + var expected = "Test content"; + + var builder = new HttpRequestInterceptionBuilder() + .ForUrl(requestUri) + .WithStatus(HttpStatusCode.BadRequest) + .WithStatus(null) // Reset to static + .WithContent(expected); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act + var response = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.BadRequest); + + // Assert + response.ShouldBe(expected); + } + + [Fact] + public static async Task WithStatus_Factory_Overrides_Static_Status_Code() + { + // Arrange + var requestUri = "https://test.local/post"; + var expected = "Test content"; + + var builder = new HttpRequestInterceptionBuilder() + .ForUrl(requestUri) + .WithStatus(HttpStatusCode.BadRequest) // Static status code + .WithStatus(() => HttpStatusCode.OK) // Factory overrides static + .WithContent(expected); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act + var response = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.OK); + + // Assert + response.ShouldBe(expected); + } + + [Fact] + public static async Task WithStatus_Static_Overrides_Factory_Status_Code() + { + // Arrange + var requestUri = "https://test.local/post"; + var expected = "Test content"; + + var builder = new HttpRequestInterceptionBuilder() + .ForUrl(requestUri) + .WithStatus(() => HttpStatusCode.OK) // Factory status code + .WithStatus(HttpStatusCode.BadRequest) // Static overrides factory + .WithContent(expected); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act + var response = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.BadRequest); + + // Assert + response.ShouldBe(expected); + } + + [Fact] + public static async Task WithStatus_Factory_With_Conditional_Logic() + { + // Arrange + var requestUri = "https://test.local/post"; + var expected = "Test content"; + var isFirstRequest = true; + + var builder = new HttpRequestInterceptionBuilder() + .ForUrl(requestUri) + .WithStatus(() => + { + if (isFirstRequest) + { + isFirstRequest = false; + return HttpStatusCode.OK; + } + + return HttpStatusCode.TooManyRequests; + }) + .WithContent(expected); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act + var response1 = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.OK); + var response2 = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.TooManyRequests); + + // Assert + response1.ShouldBe(expected); + response2.ShouldBe(expected); + } + + [Fact] + public static async Task WithStatus_Factory_With_Exception_Handling() + { + // Arrange + var requestUri = "https://test.local/post"; + var expected = "Test content"; + var shouldThrow = true; + + var builder = new HttpRequestInterceptionBuilder() + .ForUrl(requestUri) + .WithStatus(() => + { + if (shouldThrow) + { + shouldThrow = false; + throw new InvalidOperationException("Test exception"); + } + + return HttpStatusCode.OK; + }) + .WithContent(expected); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act & Assert + await Should.ThrowAsync(async () => + await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.OK)); + } + + [Fact] + public static async Task WithStatus_Factory_With_Multiple_Registrations() + { + // Arrange + var requestUri1 = "https://test.local/post"; + var requestUri2 = "https://bing.com/"; + var expected = "Test content"; + + var builder1 = new HttpRequestInterceptionBuilder() + .ForUrl(requestUri1) + .WithStatus(() => HttpStatusCode.OK) + .WithContent(expected); + + var builder2 = new HttpRequestInterceptionBuilder() + .ForUrl(requestUri2) + .WithStatus(() => HttpStatusCode.BadRequest) + .WithContent(expected); + + var options = new HttpClientInterceptorOptions() + .Register(builder1) + .Register(builder2); + + // Act + var response1 = await HttpAssert.GetAsync(options, requestUri1, HttpStatusCode.OK); + var response2 = await HttpAssert.GetAsync(options, requestUri2, HttpStatusCode.BadRequest); + + // Assert + response1.ShouldBe(expected); + response2.ShouldBe(expected); + } + + [Fact] + public static async Task WithStatus_Factory_With_Interception_Callback() + { + // Arrange + var requestUri = "https://test.local/post"; + var expected = "Test content"; + var callbackCount = 0; + var statusCodeCount = 0; + + var builder = new HttpRequestInterceptionBuilder() + .ForUrl(requestUri) + .WithStatus(() => + { + statusCodeCount++; + return HttpStatusCode.OK; + }) + .WithInterceptionCallback((request) => + { + callbackCount++; + return Task.FromResult(true); + }) + .WithContent(expected); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act + var response = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.OK); + + // Assert + response.ShouldBe(expected); + callbackCount.ShouldBe(1); + statusCodeCount.ShouldBe(1); + } + + [Fact] + public static async Task WithStatus_Factory_Called_For_Each_Request() + { + // Arrange + var requestUri = "https://test.local/post"; + var expected = "Test content"; + var factoryCallCount = 0; + + var builder = new HttpRequestInterceptionBuilder() + .ForUrl(requestUri) + .WithStatus(() => + { + factoryCallCount++; + return HttpStatusCode.OK; + }) + .WithContent(expected); + + var options = new HttpClientInterceptorOptions().Register(builder); + + // Act + var response1 = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.OK); + var response2 = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.OK); + var response3 = await HttpAssert.GetAsync(options, requestUri, HttpStatusCode.OK); + + // Assert + response1.ShouldBe(expected); + response2.ShouldBe(expected); + response3.ShouldBe(expected); + factoryCallCount.ShouldBe(3); // Factory should be called for each request + } + private sealed class CustomObject { internal enum Color