165 lines
6.2 KiB
C#
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");
|
|
}
|
|
}
|