Files
git.stella-ops.org/src/__Libraries/StellaOps.TestKit/Fixtures/WebServiceFixture.cs

184 lines
5.7 KiB
C#

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;
/// <summary>
/// Test fixture for ASP.NET web services using WebApplicationFactory.
/// Provides isolated service hosting with deterministic configuration.
/// </summary>
/// <typeparam name="TProgram">The program entry point (typically Program class).</typeparam>
public class WebServiceFixture<TProgram> : WebApplicationFactory<TProgram>, IAsyncLifetime
where TProgram : class
{
private readonly Action<IServiceCollection>? _configureServices;
private readonly Action<IWebHostBuilder>? _configureWebHost;
public WebServiceFixture(
Action<IServiceCollection>? configureServices = null,
Action<IWebHostBuilder>? configureWebHost = null)
{
_configureServices = configureServices;
_configureWebHost = configureWebHost;
}
/// <summary>
/// Gets the environment name for tests. Defaults to "Testing".
/// </summary>
protected virtual string EnvironmentName => "Testing";
protected override void ConfigureWebHost(IWebHostBuilder builder)
{
builder.UseEnvironment(EnvironmentName);
builder.ConfigureServices(services =>
{
// Add default test services
services.AddSingleton<TestRequestContext>();
// Apply custom configuration
_configureServices?.Invoke(services);
});
_configureWebHost?.Invoke(builder);
}
/// <summary>
/// Creates an HttpClient with optional authentication.
/// </summary>
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;
}
/// <summary>
/// Creates an HttpClient with a specific tenant header.
/// </summary>
public HttpClient CreateTenantClient(string tenantId, string? bearerToken = null)
{
var client = CreateAuthenticatedClient(bearerToken);
client.DefaultRequestHeaders.Add("X-Tenant-Id", tenantId);
return client;
}
public virtual ValueTask InitializeAsync() => ValueTask.CompletedTask;
}
/// <summary>
/// Provides test request context for tracking.
/// </summary>
public sealed class TestRequestContext
{
private readonly List<RequestRecord> _requests = new();
public void RecordRequest(string method, string path, int statusCode)
{
lock (_requests)
{
_requests.Add(new RequestRecord(method, path, statusCode, DateTime.UtcNow));
}
}
public IReadOnlyList<RequestRecord> GetRequests()
{
lock (_requests)
{
return _requests.ToList();
}
}
public sealed record RequestRecord(string Method, string Path, int StatusCode, DateTime Timestamp);
}
/// <summary>
/// Extension methods for web service testing.
/// </summary>
public static class WebServiceTestExtensions
{
/// <summary>
/// Sends a request with malformed content type header.
/// </summary>
public static async Task<HttpResponseMessage> 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);
}
/// <summary>
/// Sends a request with oversized payload.
/// </summary>
public static async Task<HttpResponseMessage> 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);
}
/// <summary>
/// Sends a request with wrong HTTP method.
/// </summary>
public static async Task<HttpResponseMessage> 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));
}
/// <summary>
/// Sends a request without authentication.
/// </summary>
public static async Task<HttpResponseMessage> 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));
}
/// <summary>
/// Sends a request with expired token.
/// </summary>
public static async Task<HttpResponseMessage> SendWithExpiredTokenAsync(
this HttpClient client,
string url,
string expiredToken)
{
client.DefaultRequestHeaders.Authorization =
new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", expiredToken);
return await client.GetAsync(url);
}
}