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