- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism. - Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions. - Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests. - Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
181 lines
5.8 KiB
C#
181 lines
5.8 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 Task InitializeAsync() => Task.CompletedTask;
|
|
|
|
Task IAsyncLifetime.DisposeAsync() => Task.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);
|
|
}
|
|
}
|