Files
git.stella-ops.org/src/Findings/__Tests/StellaOps.Findings.Ledger.Tests/Integration/FeatureVerificationProbeTests.cs
2026-02-12 10:27:23 +02:00

148 lines
6.2 KiB
C#

using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Testing;
using Xunit;
namespace StellaOps.Findings.Ledger.Tests.Integration;
[Trait("Category", "Integration")]
public sealed class FeatureVerificationProbeTests : IClassFixture<FindingsLedgerWebApplicationFactory>
{
private readonly FindingsLedgerWebApplicationFactory _factory;
public FeatureVerificationProbeTests(FindingsLedgerWebApplicationFactory factory)
{
_factory = factory;
}
[Fact(DisplayName = "Feature verification probe captures request/response evidence")]
public async Task CaptureFeatureProbeEvidence()
{
var scenario = Environment.GetEnvironmentVariable("STELLA_FEATURE_PROBE_SCENARIO");
var outputPath = Environment.GetEnvironmentVariable("STELLA_FEATURE_PROBE_OUT");
// Keep this test no-op for normal test runs unless explicitly invoked for feature verification.
if (string.IsNullOrWhiteSpace(scenario) || string.IsNullOrWhiteSpace(outputPath))
{
return;
}
using var authedClient = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
using var anonClient = _factory.CreateClient(new WebApplicationFactoryClientOptions { AllowAutoRedirect = false });
authedClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", "test-token");
authedClient.DefaultRequestHeaders.Add("X-Scopes", "findings:read findings:write");
authedClient.DefaultRequestHeaders.Add("X-Tenant-Id", "11111111-1111-1111-1111-111111111111");
authedClient.DefaultRequestHeaders.Add("X-Stella-Tenant", "tenant-qa");
var requests = BuildScenario(scenario);
var capturedAt = DateTimeOffset.UtcNow;
var results = new List<ProbeResult>(requests.Count);
foreach (var req in requests)
{
var client = req.Auth ? authedClient : anonClient;
using var message = new HttpRequestMessage(new HttpMethod(req.Method), req.Path);
if (req.JsonBody is not null)
{
message.Content = new StringContent(req.JsonBody, Encoding.UTF8, "application/json");
}
var requestAt = DateTimeOffset.UtcNow;
using var response = await client.SendAsync(message);
var body = await response.Content.ReadAsStringAsync();
var snippet = body.Length > 600 ? body[..600] : body;
results.Add(new ProbeResult(
req.Description,
req.Method,
req.Path,
req.ExpectedStatus,
(int)response.StatusCode,
req.Assertion,
(int)response.StatusCode == req.ExpectedStatus ? "pass" : "fail",
requestAt.ToString("O"),
snippet));
}
var artifact = new ProbeArtifact(
Type: "api",
Module: "api",
Feature: scenario,
BaseUrl: "in-memory-testserver",
CapturedAtUtc: capturedAt.ToString("O"),
Requests: results,
Verdict: results.All(r => string.Equals(r.Result, "pass", StringComparison.Ordinal)) ? "pass" : "fail");
var directory = Path.GetDirectoryName(outputPath);
if (!string.IsNullOrWhiteSpace(directory))
{
Directory.CreateDirectory(directory);
}
var json = JsonSerializer.Serialize(artifact, new JsonSerializerOptions { WriteIndented = true });
await File.WriteAllTextAsync(outputPath, json);
Assert.All(results, r => Assert.Equal("pass", r.Result));
}
private static IReadOnlyList<ProbeRequest> BuildScenario(string scenario)
{
if (string.Equals(scenario, "policy-trace-panel", StringComparison.Ordinal))
{
return
[
new("List finding summaries with auth", "GET", "/api/v1/findings/summaries", true, 200, null, "authorized summaries query succeeds"),
new("Invalid finding id returns bad request", "GET", "/api/v1/findings/not-a-guid/summary", true, 400, null, "invalid GUID is rejected"),
new("Unknown finding summary returns not found", "GET", $"/api/v1/findings/{Guid.NewGuid():D}/summary", true, 404, null, "unknown finding returns 404"),
new("Unknown finding evidence graph returns not found", "GET", $"/api/v1/findings/{Guid.NewGuid():D}/evidence-graph", true, 404, null, "unknown graph returns 404"),
new("Unauthorized summaries request is rejected", "GET", "/api/v1/findings/summaries", false, 401, null, "missing token returns 401")
];
}
if (string.Equals(scenario, "score-api-endpoints", StringComparison.Ordinal))
{
return
[
new("Scoring policy endpoint returns active policy", "GET", "/api/v1/scoring/policy", true, 200, null, "authorized policy read succeeds"),
new("Batch scoring rejects empty list", "POST", "/api/v1/findings/scores", true, 400, "{\"findingIds\":[]}", "empty batch rejected"),
new("Cached score unknown finding returns not found", "GET", "/api/v1/findings/CVE-9999-0000%40pkg%3Anpm%2Fnone%401.0.0/score", true, 404, null, "unknown score returns 404"),
new("Scoring policy without auth is rejected", "GET", "/api/v1/scoring/policy", false, 401, null, "missing token returns 401")
];
}
throw new InvalidOperationException($"Unknown feature probe scenario: {scenario}");
}
private sealed record ProbeRequest(
string Description,
string Method,
string Path,
bool Auth,
int ExpectedStatus,
string? JsonBody,
string Assertion);
private sealed record ProbeResult(
string Description,
string Method,
string Path,
int ExpectedStatus,
int ActualStatus,
string Assertion,
string Result,
string RequestCapturedAtUtc,
string ResponseSnippet);
private sealed record ProbeArtifact(
string Type,
string Module,
string Feature,
string BaseUrl,
string CapturedAtUtc,
IReadOnlyList<ProbeResult> Requests,
string Verdict);
}