// -----------------------------------------------------------------------------
// ProofChainIntegrationTests.cs
// Sprint: SPRINT_3500_0004_0003_integration_tests_corpus
// Task: T1 - Proof Chain Integration Tests
// Description: End-to-end tests for complete proof chain workflow:
// scan → manifest → score → proof bundle → verify
// -----------------------------------------------------------------------------
using System.Net;
using System.Net.Http.Json;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace StellaOps.Integration.ProofChain;
///
/// End-to-end integration tests for the proof chain workflow.
/// Tests the complete flow: scan submission → manifest creation → score computation
/// → proof bundle generation → verification.
///
[Collection("ProofChainIntegration")]
public class ProofChainIntegrationTests : IAsyncLifetime
{
private readonly ProofChainTestFixture _fixture;
private HttpClient _client = null!;
public ProofChainIntegrationTests(ProofChainTestFixture fixture)
{
_fixture = fixture;
}
public async Task InitializeAsync()
{
_client = await _fixture.CreateClientAsync();
}
public Task DisposeAsync()
{
_client.Dispose();
return Task.CompletedTask;
}
#region T1-AC1: Test scan submission creates manifest
[Fact]
public async Task ScanSubmission_CreatesManifest_WithCorrectHashes()
{
// Arrange
var sbomContent = CreateMinimalSbom();
var scanRequest = new
{
sbom = sbomContent,
policyId = "default",
metadata = new { source = "integration-test" }
};
// Act
var response = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest);
// Assert
response.StatusCode.Should().Be(HttpStatusCode.Created);
var scanResult = await response.Content.ReadFromJsonAsync();
scanResult.Should().NotBeNull();
scanResult!.ScanId.Should().NotBeEmpty();
// Verify manifest was created
var manifestResponse = await _client.GetAsync($"/api/v1/scans/{scanResult.ScanId}/manifest");
manifestResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var manifest = await manifestResponse.Content.ReadFromJsonAsync();
manifest.Should().NotBeNull();
manifest!.SbomHash.Should().StartWith("sha256:");
manifest.ManifestHash.Should().StartWith("sha256:");
}
#endregion
#region T1-AC2: Test score computation produces deterministic results
[Fact]
public async Task ScoreComputation_IsDeterministic_WithSameInputs()
{
// Arrange
var sbomContent = CreateSbomWithVulnerability("CVE-2024-12345");
var scanRequest = new
{
sbom = sbomContent,
policyId = "default"
};
// Act - Run scan twice with identical inputs
var response1 = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest);
var scan1 = await response1.Content.ReadFromJsonAsync();
var response2 = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest);
var scan2 = await response2.Content.ReadFromJsonAsync();
// Assert - Both scans should produce identical manifest hashes
var manifest1 = await GetManifestAsync(scan1!.ScanId);
var manifest2 = await GetManifestAsync(scan2!.ScanId);
manifest1.SbomHash.Should().Be(manifest2.SbomHash);
manifest1.RulesHash.Should().Be(manifest2.RulesHash);
manifest1.PolicyHash.Should().Be(manifest2.PolicyHash);
}
#endregion
#region T1-AC3: Test proof bundle generation and signing
[Fact]
public async Task ProofBundle_IsGenerated_WithValidDsseEnvelope()
{
// Arrange
var sbomContent = CreateMinimalSbom();
var scanRequest = new { sbom = sbomContent, policyId = "default" };
// Act
var response = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest);
var scan = await response.Content.ReadFromJsonAsync();
// Get proof bundle
var proofsResponse = await _client.GetAsync($"/api/v1/scans/{scan!.ScanId}/proofs");
// Assert
proofsResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var proofs = await proofsResponse.Content.ReadFromJsonAsync();
proofs.Should().NotBeNull();
proofs!.Items.Should().NotBeEmpty();
var proof = proofs.Items.First();
proof.RootHash.Should().StartWith("sha256:");
proof.DsseEnvelopeValid.Should().BeTrue();
}
#endregion
#region T1-AC4: Test proof verification succeeds for valid bundles
[Fact]
public async Task ProofVerification_Succeeds_ForValidBundle()
{
// Arrange
var sbomContent = CreateMinimalSbom();
var scanRequest = new { sbom = sbomContent, policyId = "default" };
var response = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest);
var scan = await response.Content.ReadFromJsonAsync();
var proofsResponse = await _client.GetAsync($"/api/v1/scans/{scan!.ScanId}/proofs");
var proofs = await proofsResponse.Content.ReadFromJsonAsync();
var rootHash = proofs!.Items.First().RootHash;
// Act
var verifyResponse = await _client.PostAsJsonAsync(
$"/api/v1/scans/{scan.ScanId}/proofs/{rootHash}/verify",
new { });
// Assert
verifyResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var verifyResult = await verifyResponse.Content.ReadFromJsonAsync();
verifyResult.Should().NotBeNull();
verifyResult!.Valid.Should().BeTrue();
verifyResult.Checks.Should().Contain(c => c.Name == "dsse_signature" && c.Passed);
verifyResult.Checks.Should().Contain(c => c.Name == "merkle_root" && c.Passed);
}
#endregion
#region T1-AC5: Test verification fails for tampered bundles
[Fact]
public async Task ProofVerification_Fails_ForTamperedBundle()
{
// Arrange
var sbomContent = CreateMinimalSbom();
var scanRequest = new { sbom = sbomContent, policyId = "default" };
var response = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest);
var scan = await response.Content.ReadFromJsonAsync();
// Get a valid proof then tamper with the hash
var proofsResponse = await _client.GetAsync($"/api/v1/scans/{scan!.ScanId}/proofs");
var proofs = await proofsResponse.Content.ReadFromJsonAsync();
var originalHash = proofs!.Items.First().RootHash;
var tamperedHash = "sha256:" + new string('0', 64); // Tampered hash
// Act
var verifyResponse = await _client.PostAsJsonAsync(
$"/api/v1/scans/{scan.ScanId}/proofs/{tamperedHash}/verify",
new { });
// Assert
verifyResponse.StatusCode.Should().Be(HttpStatusCode.NotFound);
}
#endregion
#region T1-AC6: Test replay produces identical scores
[Fact]
public async Task ScoreReplay_ProducesIdenticalScore_WithSameManifest()
{
// Arrange
var sbomContent = CreateSbomWithVulnerability("CVE-2024-99999");
var scanRequest = new { sbom = sbomContent, policyId = "default" };
var response = await _client.PostAsJsonAsync("/api/v1/scans", scanRequest);
var scan = await response.Content.ReadFromJsonAsync();
var manifest = await GetManifestAsync(scan!.ScanId);
var originalProofs = await GetProofsAsync(scan.ScanId);
var originalRootHash = originalProofs.Items.First().RootHash;
// Act - Replay the score computation
var replayResponse = await _client.PostAsJsonAsync(
$"/api/v1/scans/{scan.ScanId}/score/replay",
new { manifestHash = manifest.ManifestHash });
// Assert
replayResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var replayResult = await replayResponse.Content.ReadFromJsonAsync();
replayResult.Should().NotBeNull();
replayResult!.RootHash.Should().Be(originalRootHash);
replayResult.Deterministic.Should().BeTrue();
}
#endregion
#region Helper Methods
private static string CreateMinimalSbom()
{
return JsonSerializer.Serialize(new
{
bomFormat = "CycloneDX",
specVersion = "1.5",
version = 1,
metadata = new
{
timestamp = DateTimeOffset.UtcNow.ToString("O"),
component = new
{
type = "application",
name = "integration-test-app",
version = "1.0.0"
}
},
components = Array.Empty