audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories
This commit is contained in:
@@ -67,8 +67,8 @@ public class ObservationDecayTests
|
||||
var after = observedAt.AddDays(20);
|
||||
|
||||
// Act & Assert
|
||||
decay.IsStale(before).Should().BeFalse();
|
||||
decay.IsStale(after).Should().BeTrue();
|
||||
decay.CheckIsStale(before).Should().BeFalse();
|
||||
decay.CheckIsStale(after).Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -12,10 +12,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" />
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="Moq" />
|
||||
<PackageReference Include="xunit.v3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
using System.Collections.Immutable;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Confidence.Models;
|
||||
using StellaOps.Policy.Determinization;
|
||||
using StellaOps.Policy.Determinization.Evidence;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
using StellaOps.Policy.Determinization.Scoring;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.Gates.Determinization;
|
||||
using StellaOps.Policy.Engine.Policies;
|
||||
@@ -15,6 +19,7 @@ namespace StellaOps.Policy.Engine.Tests.Gates.Determinization;
|
||||
|
||||
public class DeterminizationGateTests
|
||||
{
|
||||
private static readonly DateTimeOffset Now = DateTimeOffset.UtcNow;
|
||||
private readonly Mock<ISignalSnapshotBuilder> _snapshotBuilderMock;
|
||||
private readonly Mock<IUncertaintyScoreCalculator> _uncertaintyCalculatorMock;
|
||||
private readonly Mock<IDecayedConfidenceCalculator> _decayCalculatorMock;
|
||||
@@ -28,7 +33,7 @@ public class DeterminizationGateTests
|
||||
_decayCalculatorMock = new Mock<IDecayedConfidenceCalculator>();
|
||||
_trustAggregatorMock = new Mock<TrustScoreAggregator>();
|
||||
|
||||
var options = Options.Create(new DeterminizationOptions());
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new DeterminizationOptions());
|
||||
var policy = new DeterminizationPolicy(options, NullLogger<DeterminizationPolicy>.Instance);
|
||||
|
||||
_gate = new DeterminizationGate(
|
||||
@@ -45,13 +50,12 @@ public class DeterminizationGateTests
|
||||
{
|
||||
// Arrange
|
||||
var snapshot = CreateSnapshot();
|
||||
var uncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = 0.45,
|
||||
Tier = UncertaintyTier.Moderate,
|
||||
Completeness = 0.55,
|
||||
MissingSignals = []
|
||||
};
|
||||
var uncertaintyScore = UncertaintyScore.Create(
|
||||
entropy: 0.45,
|
||||
gaps: Array.Empty<SignalGap>(),
|
||||
presentWeight: 55,
|
||||
maxWeight: 100,
|
||||
calculatedAt: Now);
|
||||
|
||||
_snapshotBuilderMock
|
||||
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
@@ -76,12 +80,7 @@ public class DeterminizationGateTests
|
||||
Environment = "development"
|
||||
};
|
||||
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
FinalScore = 0.5,
|
||||
FinalTrustLevel = TrustLevel.Medium,
|
||||
Claims = []
|
||||
};
|
||||
var mergeResult = CreateMergeResult(VexStatus.UnderInvestigation, 0.5);
|
||||
|
||||
// Act
|
||||
var result = await _gate.EvaluateAsync(mergeResult, context);
|
||||
@@ -109,13 +108,12 @@ public class DeterminizationGateTests
|
||||
{
|
||||
// Arrange
|
||||
var snapshot = CreateSnapshot();
|
||||
var uncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = 0.5,
|
||||
Tier = UncertaintyTier.Moderate,
|
||||
Completeness = 0.5,
|
||||
MissingSignals = []
|
||||
};
|
||||
var uncertaintyScore = UncertaintyScore.Create(
|
||||
entropy: 0.5,
|
||||
gaps: Array.Empty<SignalGap>(),
|
||||
presentWeight: 50,
|
||||
maxWeight: 100,
|
||||
calculatedAt: Now);
|
||||
|
||||
_snapshotBuilderMock
|
||||
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
@@ -140,12 +138,7 @@ public class DeterminizationGateTests
|
||||
Environment = "development"
|
||||
};
|
||||
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
FinalScore = 0.5,
|
||||
FinalTrustLevel = TrustLevel.Medium,
|
||||
Claims = []
|
||||
};
|
||||
var mergeResult = CreateMergeResult(VexStatus.UnderInvestigation, 0.5);
|
||||
|
||||
// Act
|
||||
var result = await _gate.EvaluateAsync(mergeResult, context);
|
||||
@@ -160,13 +153,12 @@ public class DeterminizationGateTests
|
||||
{
|
||||
// Arrange
|
||||
var snapshot = CreateSnapshot();
|
||||
var uncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = 0.2,
|
||||
Tier = UncertaintyTier.Low,
|
||||
Completeness = 0.8,
|
||||
MissingSignals = []
|
||||
};
|
||||
var uncertaintyScore = UncertaintyScore.Create(
|
||||
entropy: 0.2,
|
||||
gaps: Array.Empty<SignalGap>(),
|
||||
presentWeight: 80,
|
||||
maxWeight: 100,
|
||||
calculatedAt: Now);
|
||||
|
||||
_snapshotBuilderMock
|
||||
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
@@ -191,12 +183,7 @@ public class DeterminizationGateTests
|
||||
Environment = "production"
|
||||
};
|
||||
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
FinalScore = 0.8,
|
||||
FinalTrustLevel = TrustLevel.High,
|
||||
Claims = []
|
||||
};
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected, 0.8);
|
||||
|
||||
// Act
|
||||
var result = await _gate.EvaluateAsync(mergeResult, context);
|
||||
@@ -212,11 +199,35 @@ public class DeterminizationGateTests
|
||||
Purl = "pkg:npm/test@1.0.0",
|
||||
Epss = SignalState<EpssEvidence>.NotQueried(),
|
||||
Vex = SignalState<VexClaimSummary>.NotQueried(),
|
||||
Reachability = SignalState<ReachabilityEvidence>.NotQueried(),
|
||||
Runtime = SignalState<RuntimeEvidence>.NotQueried(),
|
||||
Reachability = SignalState<StellaOps.Policy.Determinization.Evidence.ReachabilityEvidence>.NotQueried(),
|
||||
Runtime = SignalState<StellaOps.Policy.Determinization.Evidence.RuntimeEvidence>.NotQueried(),
|
||||
Backport = SignalState<BackportEvidence>.NotQueried(),
|
||||
Sbom = SignalState<SbomLineageEvidence>.NotQueried(),
|
||||
Cvss = SignalState<CvssEvidence>.NotQueried(),
|
||||
SnapshotAt = DateTimeOffset.UtcNow
|
||||
SnapshotAt = Now
|
||||
};
|
||||
|
||||
private static MergeResult CreateMergeResult(VexStatus status, double confidence)
|
||||
{
|
||||
var winningClaim = new ScoredClaim
|
||||
{
|
||||
SourceId = "test-source",
|
||||
Status = status,
|
||||
OriginalScore = confidence,
|
||||
AdjustedScore = confidence,
|
||||
ScopeSpecificity = 1,
|
||||
Accepted = true,
|
||||
Reason = "test"
|
||||
};
|
||||
|
||||
return new MergeResult
|
||||
{
|
||||
Status = status,
|
||||
Confidence = confidence,
|
||||
HasConflicts = false,
|
||||
AllClaims = ImmutableArray.Create(winningClaim),
|
||||
WinningClaim = winningClaim,
|
||||
Conflicts = ImmutableArray<ConflictRecord>.Empty
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ public sealed class FacetQuotaGateIntegrationTests
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultMaxChurnPercent = 10.0m
|
||||
DefaultQuota = new FacetQuota { MaxChurnPercent = 10.0m }
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
@@ -131,8 +131,7 @@ public sealed class FacetQuotaGateIntegrationTests
|
||||
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultAction = QuotaExceededAction.Block
|
||||
Enabled = true
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
@@ -160,8 +159,7 @@ public sealed class FacetQuotaGateIntegrationTests
|
||||
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultAction = QuotaExceededAction.RequireVex
|
||||
Enabled = true
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
@@ -343,15 +341,9 @@ public sealed class FacetQuotaGateIntegrationTests
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultMaxChurnPercent = 10.0m,
|
||||
FacetOverrides = new Dictionary<string, FacetQuotaOverride>
|
||||
{
|
||||
["os-packages"] = new FacetQuotaOverride
|
||||
{
|
||||
MaxChurnPercent = 30m, // Higher threshold for OS packages
|
||||
Action = QuotaExceededAction.Warn
|
||||
}
|
||||
}
|
||||
DefaultQuota = new FacetQuota { MaxChurnPercent = 10.0m },
|
||||
FacetQuotas = ImmutableDictionary<string, FacetQuota>.Empty
|
||||
.Add("os-packages", new FacetQuota { MaxChurnPercent = 30m })
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
@@ -443,14 +435,16 @@ public sealed class FacetQuotaGateIntegrationTests
|
||||
|
||||
private FacetSeal CreateSealWithTimestamp(string imageDigest, int fileCount, DateTimeOffset createdAt)
|
||||
{
|
||||
var files = Enumerable.Range(0, fileCount)
|
||||
.Select(i => new FacetFileEntry($"/file{i}.txt", $"sha256:{i:x8}", 100, null))
|
||||
.ToImmutableArray();
|
||||
|
||||
var facetEntry = new FacetEntry(
|
||||
FacetId: "test-facet",
|
||||
Files: files,
|
||||
MerkleRoot: $"sha256:facet{fileCount:x8}");
|
||||
var facetEntry = new FacetEntry
|
||||
{
|
||||
FacetId = "test-facet",
|
||||
Name = "Test Facet",
|
||||
Category = FacetCategory.OsPackages,
|
||||
Selectors = ["/file*.txt"],
|
||||
MerkleRoot = $"sha256:facet{fileCount:x8}",
|
||||
FileCount = fileCount,
|
||||
TotalBytes = fileCount * 100L
|
||||
};
|
||||
|
||||
return new FacetSeal
|
||||
{
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Copyright (c) 2026 StellaOps
|
||||
// Sprint: SPRINT_20260106_001_003_POLICY_determinization_gates
|
||||
// Task: DPE-023, DPE-024 - Integration tests for determinization gate in policy pipeline
|
||||
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy.Determinization;
|
||||
using StellaOps.Policy.Engine.DependencyInjection;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.Gates.Determinization;
|
||||
using StellaOps.Policy.Engine.Policies;
|
||||
using StellaOps.Policy.Engine.Subscriptions;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Integration;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for determinization gate within the policy pipeline.
|
||||
/// Tests DI wiring, gate registration, and signal update handling.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "20260106.001.003")]
|
||||
[Trait("Task", "DPE-023")]
|
||||
public sealed class DeterminizationGateIntegrationTests
|
||||
{
|
||||
private static ServiceCollection CreateServicesWithConfiguration()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection()
|
||||
.Build();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
return services;
|
||||
}
|
||||
|
||||
#region DI Wiring Tests
|
||||
|
||||
[Fact(DisplayName = "AddDeterminizationEngine registers all required services")]
|
||||
public void AddDeterminizationEngine_RegistersAllServices()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
|
||||
// Act
|
||||
services.AddLogging();
|
||||
services.AddDeterminizationEngine();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Assert: All services should be resolvable
|
||||
provider.GetService<IDeterminizationGate>().Should().NotBeNull();
|
||||
provider.GetService<IDeterminizationPolicy>().Should().NotBeNull();
|
||||
provider.GetService<ISignalUpdateSubscription>().Should().NotBeNull();
|
||||
provider.GetService<DeterminizationGateMetrics>().Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "AddPolicyEngine includes determinization services")]
|
||||
public void AddPolicyEngine_IncludesDeterminizationServices()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
|
||||
// Act
|
||||
services.AddLogging();
|
||||
services.AddMemoryCache();
|
||||
services.AddPolicyEngine();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Assert: Determinization services should be available
|
||||
provider.GetService<IDeterminizationGate>().Should().NotBeNull();
|
||||
provider.GetService<IDeterminizationPolicy>().Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "Determinization services are registered as singletons")]
|
||||
public void DeterminizationServices_AreRegisteredAsSingletons()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
services.AddLogging();
|
||||
services.AddDeterminizationEngine();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var gate1 = provider.GetRequiredService<IDeterminizationGate>();
|
||||
var gate2 = provider.GetRequiredService<IDeterminizationGate>();
|
||||
|
||||
// Assert: Same instance (singleton)
|
||||
gate1.Should().BeSameAs(gate2);
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "DeterminizationGateMetrics is resolvable")]
|
||||
public void DeterminizationGateMetrics_IsResolvable()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
services.AddLogging();
|
||||
services.AddDeterminizationEngine();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var metrics = provider.GetService<DeterminizationGateMetrics>();
|
||||
|
||||
// Assert
|
||||
metrics.Should().NotBeNull();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Options Tests
|
||||
|
||||
[Fact(DisplayName = "DeterminizationOptions are bound from configuration")]
|
||||
public void DeterminizationOptions_AreBoundFromConfiguration()
|
||||
{
|
||||
// Arrange
|
||||
var configData = new Dictionary<string, string?>
|
||||
{
|
||||
["Determinization:ManualReviewEntropyThreshold"] = "0.65",
|
||||
["Determinization:RefreshEntropyThreshold"] = "0.45",
|
||||
["Determinization:ConfidenceHalfLifeDays"] = "21"
|
||||
};
|
||||
|
||||
var services = new ServiceCollection();
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection(configData)
|
||||
.Build();
|
||||
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
services.AddOptions<DeterminizationOptions>()
|
||||
.Bind(configuration.GetSection("Determinization"));
|
||||
|
||||
// Act
|
||||
var provider = services.BuildServiceProvider();
|
||||
var options = provider.GetRequiredService<IOptions<DeterminizationOptions>>();
|
||||
|
||||
// Assert
|
||||
options.Value.ManualReviewEntropyThreshold.Should().Be(0.65);
|
||||
options.Value.RefreshEntropyThreshold.Should().Be(0.45);
|
||||
options.Value.ConfidenceHalfLifeDays.Should().Be(21);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for signal update re-evaluation.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
[Trait("Sprint", "20260106.001.003")]
|
||||
[Trait("Task", "DPE-024")]
|
||||
public sealed class SignalUpdateIntegrationTests
|
||||
{
|
||||
private static ServiceCollection CreateServicesWithConfiguration()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
var configuration = new ConfigurationBuilder()
|
||||
.AddInMemoryCollection()
|
||||
.Build();
|
||||
services.AddSingleton<IConfiguration>(configuration);
|
||||
return services;
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "SignalUpdateHandler is registered via AddDeterminizationEngine")]
|
||||
public void SignalUpdateHandler_IsRegisteredViaDeterminizationEngine()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
services.AddLogging();
|
||||
services.AddDeterminizationEngine();
|
||||
|
||||
// Act
|
||||
var provider = services.BuildServiceProvider();
|
||||
var handler = provider.GetService<ISignalUpdateSubscription>();
|
||||
|
||||
// Assert
|
||||
handler.Should().NotBeNull();
|
||||
handler.Should().BeOfType<SignalUpdateHandler>();
|
||||
}
|
||||
|
||||
[Fact(DisplayName = "SignalUpdateHandler receives all dependencies")]
|
||||
public void SignalUpdateHandler_ReceivesAllDependencies()
|
||||
{
|
||||
// Arrange
|
||||
var services = CreateServicesWithConfiguration();
|
||||
services.AddLogging();
|
||||
services.AddDeterminizationEngine();
|
||||
var provider = services.BuildServiceProvider();
|
||||
|
||||
// Act
|
||||
var handler = provider.GetRequiredService<ISignalUpdateSubscription>();
|
||||
|
||||
// Assert: If dependencies were missing, this would fail to resolve
|
||||
handler.Should().NotBeNull();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Determinization;
|
||||
using StellaOps.Policy.Determinization.Evidence;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
using StellaOps.Policy.Engine.Policies;
|
||||
|
||||
@@ -10,11 +11,12 @@ namespace StellaOps.Policy.Engine.Tests.Policies;
|
||||
|
||||
public class DeterminizationPolicyTests
|
||||
{
|
||||
private static readonly DateTimeOffset Now = DateTimeOffset.UtcNow;
|
||||
private readonly DeterminizationPolicy _policy;
|
||||
|
||||
public DeterminizationPolicyTests()
|
||||
{
|
||||
var options = Options.Create(new DeterminizationOptions());
|
||||
var options = Microsoft.Extensions.Options.Options.Create(new DeterminizationOptions());
|
||||
_policy = new DeterminizationPolicy(options, NullLogger<DeterminizationPolicy>.Instance);
|
||||
}
|
||||
|
||||
@@ -22,12 +24,16 @@ public class DeterminizationPolicyTests
|
||||
public void Evaluate_RuntimeEvidenceLoaded_ReturnsEscalated()
|
||||
{
|
||||
// Arrange
|
||||
var runtimeEvidence = new RuntimeEvidence
|
||||
{
|
||||
Detected = true,
|
||||
Source = "tracer",
|
||||
ObservationStart = Now.AddHours(-1),
|
||||
ObservationEnd = Now,
|
||||
Confidence = 0.95
|
||||
};
|
||||
var context = CreateContext(
|
||||
runtime: new SignalState<RuntimeEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new RuntimeEvidence { ObservedLoaded = true }
|
||||
});
|
||||
runtime: SignalState<RuntimeEvidence>.Queried(runtimeEvidence, Now));
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
@@ -42,12 +48,15 @@ public class DeterminizationPolicyTests
|
||||
public void Evaluate_HighEpss_ReturnsQuarantined()
|
||||
{
|
||||
// Arrange
|
||||
var epssEvidence = new EpssEvidence
|
||||
{
|
||||
Cve = "CVE-2024-0001",
|
||||
Epss = 0.8,
|
||||
Percentile = 0.95,
|
||||
PublishedAt = Now.AddDays(-1)
|
||||
};
|
||||
var context = CreateContext(
|
||||
epss: new SignalState<EpssEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new EpssEvidence { Score = 0.8 }
|
||||
},
|
||||
epss: SignalState<EpssEvidence>.Queried(epssEvidence, Now),
|
||||
environment: DeploymentEnvironment.Production);
|
||||
|
||||
// Act
|
||||
@@ -63,12 +72,14 @@ public class DeterminizationPolicyTests
|
||||
public void Evaluate_ReachableCode_ReturnsQuarantined()
|
||||
{
|
||||
// Arrange
|
||||
var reachabilityEvidence = new ReachabilityEvidence
|
||||
{
|
||||
Status = ReachabilityStatus.Reachable,
|
||||
AnalyzedAt = Now,
|
||||
Confidence = 0.9
|
||||
};
|
||||
var context = CreateContext(
|
||||
reachability: new SignalState<ReachabilityEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new ReachabilityEvidence { IsReachable = true, Confidence = 0.9 }
|
||||
});
|
||||
reachability: SignalState<ReachabilityEvidence>.Queried(reachabilityEvidence, Now));
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
@@ -135,12 +146,14 @@ public class DeterminizationPolicyTests
|
||||
public void Evaluate_UnreachableWithHighConfidence_ReturnsAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var reachabilityEvidence = new ReachabilityEvidence
|
||||
{
|
||||
Status = ReachabilityStatus.Unreachable,
|
||||
AnalyzedAt = Now,
|
||||
Confidence = 0.9
|
||||
};
|
||||
var context = CreateContext(
|
||||
reachability: new SignalState<ReachabilityEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new ReachabilityEvidence { IsReachable = false, Confidence = 0.9 }
|
||||
},
|
||||
reachability: SignalState<ReachabilityEvidence>.Queried(reachabilityEvidence, Now),
|
||||
trustScore: 0.8);
|
||||
|
||||
// Act
|
||||
@@ -156,12 +169,15 @@ public class DeterminizationPolicyTests
|
||||
public void Evaluate_VexNotAffected_ReturnsAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var vexSummary = new VexClaimSummary
|
||||
{
|
||||
Status = "not_affected",
|
||||
Confidence = 0.9,
|
||||
StatementCount = 2,
|
||||
ComputedAt = Now
|
||||
};
|
||||
var context = CreateContext(
|
||||
vex: new SignalState<VexClaimSummary>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new VexClaimSummary { IsNotAffected = true, IssuerTrust = 0.9 }
|
||||
},
|
||||
vex: SignalState<VexClaimSummary>.Queried(vexSummary, Now),
|
||||
trustScore: 0.8);
|
||||
|
||||
// Act
|
||||
@@ -249,22 +265,21 @@ public class DeterminizationPolicyTests
|
||||
Backport = SignalState<BackportEvidence>.NotQueried(),
|
||||
Sbom = SignalState<SbomLineageEvidence>.NotQueried(),
|
||||
Cvss = SignalState<CvssEvidence>.NotQueried(),
|
||||
SnapshotAt = DateTimeOffset.UtcNow
|
||||
SnapshotAt = Now
|
||||
};
|
||||
|
||||
return new DeterminizationContext
|
||||
{
|
||||
SignalSnapshot = snapshot,
|
||||
UncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = entropy,
|
||||
Tier = tier,
|
||||
Completeness = 1.0 - entropy,
|
||||
MissingSignals = []
|
||||
},
|
||||
UncertaintyScore = UncertaintyScore.Create(
|
||||
entropy,
|
||||
Array.Empty<SignalGap>(),
|
||||
presentWeight: (1.0 - entropy) * 100,
|
||||
maxWeight: 100,
|
||||
calculatedAt: Now),
|
||||
Decay = new ObservationDecay
|
||||
{
|
||||
LastSignalUpdate = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
LastSignalUpdate = Now.AddDays(-1),
|
||||
AgeDays = 1,
|
||||
DecayedMultiplier = isStale ? 0.3 : 0.9,
|
||||
IsStale = isStale
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.Subscriptions;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Subscriptions;
|
||||
|
||||
public class SignalUpdateHandlerTests
|
||||
{
|
||||
private readonly Mock<IObservationRepository> _observationRepositoryMock;
|
||||
private readonly Mock<IDeterminizationGate> _gateMock;
|
||||
private readonly Mock<IEventPublisher> _eventPublisherMock;
|
||||
private readonly SignalUpdateHandler _handler;
|
||||
|
||||
public SignalUpdateHandlerTests()
|
||||
{
|
||||
_observationRepositoryMock = new Mock<IObservationRepository>();
|
||||
_gateMock = new Mock<IDeterminizationGate>();
|
||||
_eventPublisherMock = new Mock<IEventPublisher>();
|
||||
_handler = new SignalUpdateHandler(
|
||||
_observationRepositoryMock.Object,
|
||||
_gateMock.Object,
|
||||
_eventPublisherMock.Object,
|
||||
NullLogger<SignalUpdateHandler>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WithNoAffectedObservations_CompletesWithoutError()
|
||||
{
|
||||
// Arrange
|
||||
var evt = CreateSignalUpdatedEvent(DeterminizationEventTypes.EpssUpdated);
|
||||
_observationRepositoryMock
|
||||
.Setup(r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<CveObservation>());
|
||||
|
||||
// Act
|
||||
var act = async () => await _handler.HandleAsync(evt);
|
||||
|
||||
// Assert
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WithAffectedObservations_ProcessesEachObservation()
|
||||
{
|
||||
// Arrange
|
||||
var evt = CreateSignalUpdatedEvent(DeterminizationEventTypes.VexUpdated);
|
||||
var observations = new[]
|
||||
{
|
||||
CreateObservation(ObservationState.PendingDeterminization),
|
||||
CreateObservation(ObservationState.PendingDeterminization)
|
||||
};
|
||||
|
||||
_observationRepositoryMock
|
||||
.Setup(r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(observations);
|
||||
|
||||
// Act
|
||||
await _handler.HandleAsync(evt);
|
||||
|
||||
// Assert
|
||||
_observationRepositoryMock.Verify(
|
||||
r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, It.IsAny<CancellationToken>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_WithException_ContinuesProcessingOtherObservations()
|
||||
{
|
||||
// Arrange
|
||||
var evt = CreateSignalUpdatedEvent(DeterminizationEventTypes.ReachabilityUpdated);
|
||||
var observations = new[]
|
||||
{
|
||||
CreateObservation(ObservationState.PendingDeterminization),
|
||||
CreateObservation(ObservationState.PendingDeterminization)
|
||||
};
|
||||
|
||||
_observationRepositoryMock
|
||||
.Setup(r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(observations);
|
||||
|
||||
// Act - should not throw even if internal processing has issues
|
||||
var act = async () => await _handler.HandleAsync(evt);
|
||||
|
||||
// Assert
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(DeterminizationEventTypes.EpssUpdated)]
|
||||
[InlineData(DeterminizationEventTypes.VexUpdated)]
|
||||
[InlineData(DeterminizationEventTypes.ReachabilityUpdated)]
|
||||
[InlineData(DeterminizationEventTypes.RuntimeUpdated)]
|
||||
[InlineData(DeterminizationEventTypes.BackportUpdated)]
|
||||
public async Task HandleAsync_SupportsAllEventTypes(string eventType)
|
||||
{
|
||||
// Arrange
|
||||
var evt = CreateSignalUpdatedEvent(eventType);
|
||||
_observationRepositoryMock
|
||||
.Setup(r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(Array.Empty<CveObservation>());
|
||||
|
||||
// Act
|
||||
var act = async () => await _handler.HandleAsync(evt);
|
||||
|
||||
// Assert
|
||||
await act.Should().NotThrowAsync();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task HandleAsync_RespectsCancellationToken()
|
||||
{
|
||||
// Arrange
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.Cancel();
|
||||
var evt = CreateSignalUpdatedEvent(DeterminizationEventTypes.EpssUpdated);
|
||||
|
||||
_observationRepositoryMock
|
||||
.Setup(r => r.FindByCveAndPurlAsync(evt.CveId, evt.Purl, cts.Token))
|
||||
.ThrowsAsync(new OperationCanceledException());
|
||||
|
||||
// Act & Assert
|
||||
await Assert.ThrowsAsync<OperationCanceledException>(
|
||||
() => _handler.HandleAsync(evt, cts.Token));
|
||||
}
|
||||
|
||||
private static SignalUpdatedEvent CreateSignalUpdatedEvent(string eventType) =>
|
||||
new()
|
||||
{
|
||||
EventType = eventType,
|
||||
CveId = "CVE-2024-0001",
|
||||
Purl = "pkg:npm/test@1.0.0",
|
||||
UpdatedAt = DateTimeOffset.UtcNow,
|
||||
Source = "TestSource"
|
||||
};
|
||||
|
||||
private static CveObservation CreateObservation(ObservationState state) =>
|
||||
new()
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CveId = "CVE-2024-0001",
|
||||
SubjectPurl = "pkg:npm/test@1.0.0",
|
||||
State = state,
|
||||
ObservedAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user