Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/Contract/ScannerOpenApiContractTests.cs
2026-01-17 21:32:08 +02:00

165 lines
6.2 KiB
C#

// -----------------------------------------------------------------------------
// ScannerOpenApiContractTests.cs
// Sprint: SPRINT_5100_0007_0006_webservice_contract
// Task: WEBSVC-5100-007
// Description: OpenAPI schema contract tests for Scanner.WebService
// -----------------------------------------------------------------------------
using FluentAssertions;
using StellaOps.TestKit;
using StellaOps.TestKit.Fixtures;
using Xunit;
namespace StellaOps.Scanner.WebService.Tests.Contract;
/// <summary>
/// Contract tests for Scanner.WebService OpenAPI schema.
/// Validates that the API contract remains stable and detects breaking changes.
/// </summary>
[Trait("Category", TestCategories.Contract)]
[Collection("ScannerWebService")]
public sealed class ScannerOpenApiContractTests : IClassFixture<ScannerApplicationFactory>
{
private readonly ScannerApplicationFactory _factory;
private readonly string _snapshotPath;
public ScannerOpenApiContractTests(ScannerApplicationFactory factory)
{
_factory = factory;
_snapshotPath = Path.Combine(AppContext.BaseDirectory, "Contract", "Expected", "scanner-openapi.json");
}
/// <summary>
/// Validates that the OpenAPI schema matches the expected snapshot.
/// </summary>
[Fact(Skip = "OpenAPI/Swagger not enabled in test environment")]
public async Task OpenApiSchema_MatchesSnapshot()
{
await ContractTestHelper.ValidateOpenApiSchemaAsync(_factory, _snapshotPath);
}
/// <summary>
/// Validates that all core Scanner endpoints exist in the schema.
/// </summary>
[Fact(Skip = "OpenAPI/Swagger not enabled in test environment")]
public async Task OpenApiSchema_ContainsCoreEndpoints()
{
// Note: Health endpoints are at root level (/healthz, /readyz), not under /api/v1
// SBOM endpoint is POST /api/v1/scans/{scanId}/sbom (not a standalone /api/v1/sbom)
// Reports endpoint is POST /api/v1/reports (not GET)
// Findings endpoints are under /api/v1/findings/{findingId}/evidence
var coreEndpoints = new[]
{
"/api/v1/scans",
"/api/v1/scans/{scanId}",
"/api/v1/reports",
"/api/v1/findings/{findingId}/evidence",
"/healthz",
"/readyz"
};
await ContractTestHelper.ValidateEndpointsExistAsync(_factory, coreEndpoints);
}
/// <summary>
/// Detects breaking changes in the OpenAPI schema.
/// </summary>
[Fact(Skip = "OpenAPI/Swagger not enabled in test environment")]
public async Task OpenApiSchema_NoBreakingChanges()
{
var changes = await ContractTestHelper.DetectBreakingChangesAsync(_factory, _snapshotPath);
if (changes.HasBreakingChanges)
{
var message = "Breaking API changes detected:\n" +
string.Join("\n", changes.BreakingChanges.Select(c => $" - {c}"));
Assert.Fail(message);
}
// Non-breaking changes are allowed in contract checks.
}
/// <summary>
/// Validates that security schemes are defined in the schema.
/// </summary>
[Fact(Skip = "OpenAPI/Swagger not enabled in test environment")]
public async Task OpenApiSchema_HasSecuritySchemes()
{
using var client = _factory.CreateClient();
var response = await client.GetAsync("/swagger/v1/swagger.json");
response.EnsureSuccessStatusCode();
var schemaJson = await response.Content.ReadAsStringAsync();
var schema = System.Text.Json.JsonDocument.Parse(schemaJson);
// Check for security schemes (Bearer token expected)
if (schema.RootElement.TryGetProperty("components", out var components) &&
components.TryGetProperty("securitySchemes", out var securitySchemes))
{
securitySchemes.EnumerateObject().Should().NotBeEmpty(
"OpenAPI schema should define security schemes");
}
}
/// <summary>
/// Validates that error responses are documented in the schema.
/// </summary>
[Fact(Skip = "OpenAPI/Swagger not enabled in test environment")]
public async Task OpenApiSchema_DocumentsErrorResponses()
{
using var client = _factory.CreateClient();
var response = await client.GetAsync("/swagger/v1/swagger.json");
response.EnsureSuccessStatusCode();
var schemaJson = await response.Content.ReadAsStringAsync();
var schema = System.Text.Json.JsonDocument.Parse(schemaJson);
if (schema.RootElement.TryGetProperty("paths", out var paths))
{
var hasErrorResponses = false;
foreach (var path in paths.EnumerateObject())
{
foreach (var method in path.Value.EnumerateObject())
{
if (method.Value.TryGetProperty("responses", out var responses))
{
// Check for 4xx or 5xx responses
foreach (var resp in responses.EnumerateObject())
{
if (resp.Name.StartsWith("4") || resp.Name.StartsWith("5"))
{
hasErrorResponses = true;
break;
}
}
}
}
if (hasErrorResponses) break;
}
hasErrorResponses.Should().BeTrue(
"OpenAPI schema should document error responses (4xx/5xx)");
}
}
/// <summary>
/// Validates schema determinism: multiple fetches produce identical output.
/// </summary>
[Fact(Skip = "OpenAPI/Swagger not enabled in test environment")]
public async Task OpenApiSchema_IsDeterministic()
{
var schemas = new List<string>();
for (int i = 0; i < 3; i++)
{
using var client = _factory.CreateClient();
var response = await client.GetAsync("/swagger/v1/swagger.json");
response.EnsureSuccessStatusCode();
schemas.Add(await response.Content.ReadAsStringAsync());
}
schemas.Distinct().Should().HaveCount(1,
"OpenAPI schema should be deterministic across fetches");
}
}