using System.Net;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
namespace StellaOps.TestKit.Fixtures;
///
/// Provides an in-memory HTTP test server using WebApplicationFactory for contract testing.
///
/// The entry point type of the web application (usually Program).
///
/// Usage:
///
/// public class ApiTests : IClassFixture<HttpFixtureServer<Program>>
/// {
/// private readonly HttpClient _client;
///
/// public ApiTests(HttpFixtureServer<Program> fixture)
/// {
/// _client = fixture.CreateClient();
/// }
///
/// [Fact]
/// public async Task GetHealth_ReturnsOk()
/// {
/// var response = await _client.GetAsync("/health");
/// response.EnsureSuccessStatusCode();
/// }
/// }
///
///
public sealed class HttpFixtureServer : WebApplicationFactory
where TProgram : class
{
private readonly Action? _configureServices;
///
/// Creates a new HTTP fixture server with optional service configuration.
///
/// Optional action to configure test services (e.g., replace dependencies with mocks).
public HttpFixtureServer(Action? configureServices = null)
{
_configureServices = configureServices;
}
///
/// Configures the web host for testing (disables HTTPS redirection, applies custom services).
///
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.ConfigureServices(services =>
{
// Apply user-provided service configuration (e.g., mock dependencies)
_configureServices?.Invoke(services);
});
builder.UseEnvironment("Test");
}
///
/// Creates an HttpClient configured to communicate with the test server.
///
public new HttpClient CreateClient()
{
return base.CreateClient();
}
///
/// Creates an HttpClient with custom configuration.
///
public HttpClient CreateClient(Action configure)
{
var client = CreateClient();
configure(client);
return client;
}
}
///
/// Provides a stub HTTP message handler for hermetic HTTP tests without external dependencies.
///
///
/// Usage:
///
/// var handler = new HttpMessageHandlerStub()
/// .WhenRequest("https://api.example.com/data")
/// .Responds(HttpStatusCode.OK, "{\"status\":\"ok\"}");
///
/// var httpClient = new HttpClient(handler);
/// var response = await httpClient.GetAsync("https://api.example.com/data");
/// // response.StatusCode == HttpStatusCode.OK
///
///
public sealed class HttpMessageHandlerStub : HttpMessageHandler
{
private readonly Dictionary>> _handlers = new();
private Func>? _defaultHandler;
///
/// Configures a response for a specific URL.
///
public HttpMessageHandlerStub WhenRequest(string url, Func> handler)
{
_handlers[url] = handler;
return this;
}
///
/// Configures a simple response for a specific URL.
///
public HttpMessageHandlerStub WhenRequest(string url, HttpStatusCode statusCode, string? content = null)
{
return WhenRequest(url, _ => Task.FromResult(new HttpResponseMessage(statusCode)
{
Content = content != null ? new StringContent(content) : null
}));
}
///
/// Configures a default handler for unmatched requests.
///
public HttpMessageHandlerStub WhenAnyRequest(Func> handler)
{
_defaultHandler = handler;
return this;
}
///
/// Sends the HTTP request through the stub handler.
///
protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var url = request.RequestUri?.ToString() ?? string.Empty;
if (_handlers.TryGetValue(url, out var handler))
{
return await handler(request);
}
if (_defaultHandler != null)
{
return await _defaultHandler(request);
}
// Default: 404 Not Found for unmatched requests
return new HttpResponseMessage(HttpStatusCode.NotFound)
{
Content = new StringContent($"No stub configured for {url}")
};
}
}