// Licensed under AGPL-3.0-or-later. Copyright (C) 2026 StellaOps Contributors. // Sprint: SPRINT_20260110_012_007_RISK // Task: FCR-009 - Integration Tests using System.Collections.Immutable; using FluentAssertions; using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; using StellaOps.RiskEngine.Core.Contracts; using StellaOps.RiskEngine.Core.Providers.FixChain; using Xunit; namespace StellaOps.RiskEngine.Tests; [Trait("Category", "Integration")] public sealed class FixChainRiskIntegrationTests { private readonly FixChainRiskOptions _options; private readonly InMemoryFixChainAttestationClient _attestationClient; private readonly FixChainRiskProvider _provider; public FixChainRiskIntegrationTests() { _options = new FixChainRiskOptions { Enabled = true, FixedReduction = 0.90, PartialReduction = 0.50, MinConfidenceThreshold = 0.60m }; _attestationClient = new InMemoryFixChainAttestationClient(); _provider = new FixChainRiskProvider( _options, _attestationClient, NullLogger.Instance); } [Fact] public async Task FullWorkflow_FixedVerdict_ReducesRisk() { // Arrange var cveId = "CVE-2024-12345"; var binarySha256 = new string('a', 64); var attestation = new FixChainAttestationData { ContentDigest = "sha256:abc123", CveId = cveId, ComponentPurl = "pkg:deb/debian/openssl@3.0.11", BinarySha256 = binarySha256, Verdict = new FixChainVerdictData { Status = "fixed", Confidence = 0.97m, Rationale = ["3 vulnerable functions removed", "All paths eliminated"] }, GoldenSetId = "gs-openssl-0727", VerifiedAt = DateTimeOffset.UtcNow }; _attestationClient.AddAttestation(cveId, binarySha256, attestation); // Act var status = await _provider.GetFixStatusAsync(cveId, binarySha256); // Assert status.Should().NotBeNull(); status!.Verdict.Should().Be("fixed"); status.Confidence.Should().Be(0.97m); status.Rationale.Should().HaveCount(2); status.GoldenSetId.Should().Be("gs-openssl-0727"); // Verify risk adjustment var adjustment = _provider.ComputeRiskAdjustment(status); adjustment.Should().BeLessThan(0.3); // Significant reduction } [Fact] public async Task FullWorkflow_CreateRiskFactor_ProducesValidFactor() { // Arrange var cveId = "CVE-2024-67890"; var binarySha256 = new string('b', 64); var attestation = new FixChainAttestationData { ContentDigest = "sha256:def456", CveId = cveId, ComponentPurl = "pkg:npm/lodash@4.17.21", BinarySha256 = binarySha256, Verdict = new FixChainVerdictData { Status = "partial", Confidence = 0.75m, Rationale = ["2 paths eliminated", "1 path remaining"] }, VerifiedAt = DateTimeOffset.UtcNow }; _attestationClient.AddAttestation(cveId, binarySha256, attestation); // Act var status = await _provider.GetFixStatusAsync(cveId, binarySha256); var factor = _provider.CreateRiskFactor(status!); // Assert factor.Verdict.Should().Be(FixChainVerdictStatus.Partial); factor.Confidence.Should().Be(0.75m); factor.RiskModifier.Should().BeLessThan(0); factor.AttestationRef.Should().StartWith("fixchain://"); factor.Rationale.Should().HaveCount(2); } [Fact] public async Task FullWorkflow_DisplayModel_HasCorrectValues() { // Arrange var cveId = "CVE-2024-99999"; var binarySha256 = new string('c', 64); var attestation = new FixChainAttestationData { ContentDigest = "sha256:ghi789", CveId = cveId, ComponentPurl = "pkg:maven/org.example/lib@1.0.0", BinarySha256 = binarySha256, Verdict = new FixChainVerdictData { Status = "fixed", Confidence = 0.95m, Rationale = ["Fix verified"] }, GoldenSetId = "gs-example", VerifiedAt = DateTimeOffset.UtcNow }; _attestationClient.AddAttestation(cveId, binarySha256, attestation); // Act var status = await _provider.GetFixStatusAsync(cveId, binarySha256); var factor = _provider.CreateRiskFactor(status!); var display = factor.ToDisplay(); // Assert display.Label.Should().Be("Fix Verification"); display.Value.Should().Contain("Fixed"); display.Value.Should().Contain("95"); display.ImpactDirection.Should().Be("decrease"); display.EvidenceRef.Should().Contain("fixchain://"); display.Details.Should().ContainKey("golden_set_id"); } [Fact] public async Task FullWorkflow_Badge_HasCorrectStyle() { // Arrange var cveId = "CVE-2024-11111"; var binarySha256 = new string('d', 64); var attestation = new FixChainAttestationData { ContentDigest = "sha256:jkl012", CveId = cveId, ComponentPurl = "pkg:pypi/requests@2.28.0", BinarySha256 = binarySha256, Verdict = new FixChainVerdictData { Status = "inconclusive", Confidence = 0.45m, Rationale = ["Could not determine"] }, VerifiedAt = DateTimeOffset.UtcNow }; _attestationClient.AddAttestation(cveId, binarySha256, attestation); // Act var status = await _provider.GetFixStatusAsync(cveId, binarySha256); var factor = _provider.CreateRiskFactor(status!); var badge = factor.ToBadge(); // Assert badge.Status.Should().Be("Inconclusive"); badge.Color.Should().Be("gray"); } [Fact] public async Task FullWorkflow_MultipleAttestations_SameComponent() { // Arrange - add multiple CVE attestations for same component var binarySha256 = new string('e', 64); var cveIds = new[] { "CVE-2024-001", "CVE-2024-002", "CVE-2024-003" }; foreach (var cveId in cveIds) { _attestationClient.AddAttestation(cveId, binarySha256, new FixChainAttestationData { ContentDigest = $"sha256:{cveId}", CveId = cveId, ComponentPurl = "pkg:deb/debian/openssl@3.0.11", BinarySha256 = binarySha256, Verdict = new FixChainVerdictData { Status = "fixed", Confidence = 0.95m, Rationale = [$"Fix for {cveId}"] }, VerifiedAt = DateTimeOffset.UtcNow }); } // Act & Assert - each CVE can be queried individually foreach (var cveId in cveIds) { var status = await _provider.GetFixStatusAsync(cveId, binarySha256); status.Should().NotBeNull(); status!.Verdict.Should().Be("fixed"); } } [Fact] public async Task FullWorkflow_ScoreRequest_AppliesAdjustment() { // Arrange var signals = new Dictionary { [FixChainRiskProvider.SignalFixConfidence] = 0.90, [FixChainRiskProvider.SignalFixStatus] = FixChainRiskProvider.EncodeStatus("fixed") }; var request = new ScoreRequest("fixchain", "test-subject", signals); // Act var score = await _provider.ScoreAsync(request, CancellationToken.None); // Assert score.Should().BeLessThan(0.5); // Significant reduction applied } [Fact] public async Task FullWorkflow_DisabledProvider_NoAdjustment() { // Arrange var disabledOptions = new FixChainRiskOptions { Enabled = false }; var disabledProvider = new FixChainRiskProvider(disabledOptions); var signals = new Dictionary { [FixChainRiskProvider.SignalFixConfidence] = 1.0, [FixChainRiskProvider.SignalFixStatus] = FixChainRiskProvider.EncodeStatus("fixed") }; var request = new ScoreRequest("fixchain", "test-subject", signals); // Act var score = await disabledProvider.ScoreAsync(request, CancellationToken.None); // Assert score.Should().Be(1.0); // No adjustment when disabled } [Fact] public async Task FullWorkflow_NoAttestation_ReturnsNull() { // Act var status = await _provider.GetFixStatusAsync( "CVE-NONEXISTENT", new string('x', 64)); // Assert status.Should().BeNull(); } [Fact] public async Task FullWorkflow_GetForComponent_ReturnsMultiple() { // Arrange var componentPurl = "pkg:deb/debian/test@1.0.0"; var cves = new[] { "CVE-2024-A", "CVE-2024-B" }; foreach (var cveId in cves) { _attestationClient.AddAttestation(cveId, new string('f', 64), new FixChainAttestationData { ContentDigest = $"sha256:{cveId}", CveId = cveId, ComponentPurl = componentPurl, BinarySha256 = new string('f', 64), Verdict = new FixChainVerdictData { Status = "fixed", Confidence = 0.90m, Rationale = [] }, VerifiedAt = DateTimeOffset.UtcNow }); } // Act var attestations = await _attestationClient.GetForComponentAsync(componentPurl); // Assert attestations.Should().HaveCount(2); } } /// /// In-memory attestation client for testing. /// internal sealed class InMemoryFixChainAttestationClient : IFixChainAttestationClient { private readonly Dictionary _store = new(); private readonly Dictionary> _byComponent = new(); public void AddAttestation(string cveId, string binarySha256, FixChainAttestationData attestation) { var key = $"{cveId}:{binarySha256}"; _store[key] = attestation; if (!string.IsNullOrEmpty(attestation.ComponentPurl)) { if (!_byComponent.TryGetValue(attestation.ComponentPurl, out var list)) { list = []; _byComponent[attestation.ComponentPurl] = list; } list.Add(attestation); } } public Task GetFixChainAsync( string cveId, string binarySha256, string? componentPurl = null, CancellationToken ct = default) { var key = $"{cveId}:{binarySha256}"; return Task.FromResult(_store.GetValueOrDefault(key)); } public Task> GetForComponentAsync( string componentPurl, CancellationToken ct = default) { if (_byComponent.TryGetValue(componentPurl, out var list)) { return Task.FromResult(list.ToImmutableArray()); } return Task.FromResult(ImmutableArray.Empty); } }