using System.Collections.Generic; using System.Net; using System.Net.Http.Json; using System.Text.Json; using Microsoft.Extensions.DependencyInjection; using StellaOps.Scanner.ProofSpine; using Xunit; namespace StellaOps.Scanner.WebService.Tests; public sealed class ProofSpineEndpointsTests { [Fact] public async Task GetSpine_ReturnsSpine_WithVerification() { await using var factory = new ScannerApplicationFactory(); using var scope = factory.Services.CreateScope(); var builder = scope.ServiceProvider.GetRequiredService(); var repository = scope.ServiceProvider.GetRequiredService(); var spine = await builder .ForArtifact("sha256:feedface") .ForVulnerability("CVE-2025-0001") .WithPolicyProfile("default") .WithScanRun("scan-001") .AddSbomSlice("sha256:sbom", new[] { "pkg:a", "pkg:b" }, toolId: "sbom", toolVersion: "1.0.0") .AddPolicyEval( policyDigest: "sha256:policy", factors: new Dictionary { ["policy"] = "default" }, verdict: "not_affected", verdictReason: "component_not_present", toolId: "policy", toolVersion: "1.0.0") .BuildAsync(); await repository.SaveAsync(spine); var client = factory.CreateClient(); var response = await client.GetAsync($"/api/v1/spines/{spine.SpineId}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); Assert.Equal(spine.SpineId, body.GetProperty("spineId").GetString()); var segments = body.GetProperty("segments"); Assert.True(segments.GetArrayLength() > 0); Assert.True(body.TryGetProperty("verification", out _)); } [Fact] public async Task ListSpinesByScan_ReturnsSummaries_WithSegmentCount() { await using var factory = new ScannerApplicationFactory(); using var scope = factory.Services.CreateScope(); var builder = scope.ServiceProvider.GetRequiredService(); var repository = scope.ServiceProvider.GetRequiredService(); var spine = await builder .ForArtifact("sha256:feedface") .ForVulnerability("CVE-2025-0002") .WithPolicyProfile("default") .WithScanRun("scan-002") .AddSbomSlice("sha256:sbom", new[] { "pkg:a" }, toolId: "sbom", toolVersion: "1.0.0") .AddPolicyEval( policyDigest: "sha256:policy", factors: new Dictionary { ["policy"] = "default" }, verdict: "affected", verdictReason: "reachable", toolId: "policy", toolVersion: "1.0.0") .BuildAsync(); await repository.SaveAsync(spine); var client = factory.CreateClient(); var response = await client.GetAsync("/api/v1/scans/scan-002/spines"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); var items = body.GetProperty("items"); Assert.Equal(1, items.GetArrayLength()); Assert.Equal(spine.SpineId, items[0].GetProperty("spineId").GetString()); Assert.True(items[0].GetProperty("segmentCount").GetInt32() > 0); } [Fact] public async Task GetSpine_ReturnsInvalidStatus_WhenSegmentTampered() { await using var factory = new ScannerApplicationFactory(); using var scope = factory.Services.CreateScope(); var builder = scope.ServiceProvider.GetRequiredService(); var repository = scope.ServiceProvider.GetRequiredService(); var spine = await builder .ForArtifact("sha256:feedface") .ForVulnerability("CVE-2025-0003") .WithPolicyProfile("default") .WithScanRun("scan-003") .AddSbomSlice("sha256:sbom", new[] { "pkg:a" }, toolId: "sbom", toolVersion: "1.0.0") .AddPolicyEval( policyDigest: "sha256:policy", factors: new Dictionary { ["policy"] = "default" }, verdict: "affected", verdictReason: "reachable", toolId: "policy", toolVersion: "1.0.0") .BuildAsync(); var tamperedSegment = spine.Segments[0] with { ResultHash = spine.Segments[0].ResultHash + "00" }; var tampered = spine with { Segments = new[] { tamperedSegment, spine.Segments[1] } }; await repository.SaveAsync(tampered); var client = factory.CreateClient(); var response = await client.GetAsync($"/api/v1/spines/{spine.SpineId}"); Assert.Equal(HttpStatusCode.OK, response.StatusCode); var body = await response.Content.ReadFromJsonAsync(); var segments = body.GetProperty("segments"); Assert.Equal("invalid", segments[0].GetProperty("status").GetString()); } }