using System.Net.Http.Json; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Mvc.Testing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Xunit; namespace StellaOps.TestKit.Fixtures; /// /// Test fixture for ASP.NET web services using WebApplicationFactory. /// Provides isolated service hosting with deterministic configuration. /// /// The program entry point (typically Program class). public class WebServiceFixture : WebApplicationFactory, IAsyncLifetime where TProgram : class { private readonly Action? _configureServices; private readonly Action? _configureWebHost; public WebServiceFixture( Action? configureServices = null, Action? configureWebHost = null) { _configureServices = configureServices; _configureWebHost = configureWebHost; } /// /// Gets the environment name for tests. Defaults to "Testing". /// protected virtual string EnvironmentName => "Testing"; protected override void ConfigureWebHost(IWebHostBuilder builder) { builder.UseEnvironment(EnvironmentName); builder.ConfigureServices(services => { // Add default test services services.AddSingleton(); // Apply custom configuration _configureServices?.Invoke(services); }); _configureWebHost?.Invoke(builder); } /// /// Creates an HttpClient with optional authentication. /// public HttpClient CreateAuthenticatedClient(string? bearerToken = null) { var client = CreateClient(); if (bearerToken != null) { client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", bearerToken); } return client; } /// /// Creates an HttpClient with a specific tenant header. /// public HttpClient CreateTenantClient(string tenantId, string? bearerToken = null) { var client = CreateAuthenticatedClient(bearerToken); client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId); return client; } public virtual Task InitializeAsync() => Task.CompletedTask; Task IAsyncLifetime.DisposeAsync() => Task.CompletedTask; } /// /// Provides test request context for tracking. /// public sealed class TestRequestContext { private readonly List _requests = new(); public void RecordRequest(string method, string path, int statusCode) { lock (_requests) { _requests.Add(new RequestRecord(method, path, statusCode, DateTime.UtcNow)); } } public IReadOnlyList GetRequests() { lock (_requests) { return _requests.ToList(); } } public sealed record RequestRecord(string Method, string Path, int StatusCode, DateTime Timestamp); } /// /// Extension methods for web service testing. /// public static class WebServiceTestExtensions { /// /// Sends a request with malformed content type header. /// public static async Task SendWithMalformedContentTypeAsync( this HttpClient client, HttpMethod method, string url, string? body = null) { var request = new HttpRequestMessage(method, url); if (body != null) { request.Content = new StringContent(body); request.Content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/malformed-type"); } return await client.SendAsync(request); } /// /// Sends a request with oversized payload. /// public static async Task SendOversizedPayloadAsync( this HttpClient client, string url, int sizeInBytes) { var payload = new string('x', sizeInBytes); var content = new StringContent($"{{\"data\":\"{payload}\"}}"); content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/json"); return await client.PostAsync(url, content); } /// /// Sends a request with wrong HTTP method. /// public static async Task SendWithWrongMethodAsync( this HttpClient client, string url, HttpMethod expectedMethod) { // If expected is POST, send GET; if expected is GET, send DELETE, etc. var wrongMethod = expectedMethod == HttpMethod.Get ? HttpMethod.Delete : HttpMethod.Get; return await client.SendAsync(new HttpRequestMessage(wrongMethod, url)); } /// /// Sends a request without authentication. /// public static async Task SendWithoutAuthAsync( this HttpClient client, HttpMethod method, string url) { // Remove any existing auth header client.DefaultRequestHeaders.Authorization = null; return await client.SendAsync(new HttpRequestMessage(method, url)); } /// /// Sends a request with expired token. /// public static async Task SendWithExpiredTokenAsync( this HttpClient client, string url, string expiredToken) { client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", expiredToken); return await client.GetAsync(url); } }