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