Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Determinization;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
using StellaOps.Policy.Engine.Gates;
|
||||
using StellaOps.Policy.Engine.Gates.Determinization;
|
||||
using StellaOps.Policy.Engine.Policies;
|
||||
using StellaOps.Policy.Gates;
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates.Determinization;
|
||||
|
||||
public class DeterminizationGateTests
|
||||
{
|
||||
private readonly Mock<ISignalSnapshotBuilder> _snapshotBuilderMock;
|
||||
private readonly Mock<IUncertaintyScoreCalculator> _uncertaintyCalculatorMock;
|
||||
private readonly Mock<IDecayedConfidenceCalculator> _decayCalculatorMock;
|
||||
private readonly Mock<TrustScoreAggregator> _trustAggregatorMock;
|
||||
private readonly DeterminizationGate _gate;
|
||||
|
||||
public DeterminizationGateTests()
|
||||
{
|
||||
_snapshotBuilderMock = new Mock<ISignalSnapshotBuilder>();
|
||||
_uncertaintyCalculatorMock = new Mock<IUncertaintyScoreCalculator>();
|
||||
_decayCalculatorMock = new Mock<IDecayedConfidenceCalculator>();
|
||||
_trustAggregatorMock = new Mock<TrustScoreAggregator>();
|
||||
|
||||
var options = Options.Create(new DeterminizationOptions());
|
||||
var policy = new DeterminizationPolicy(options, NullLogger<DeterminizationPolicy>.Instance);
|
||||
|
||||
_gate = new DeterminizationGate(
|
||||
policy,
|
||||
_uncertaintyCalculatorMock.Object,
|
||||
_decayCalculatorMock.Object,
|
||||
_trustAggregatorMock.Object,
|
||||
_snapshotBuilderMock.Object,
|
||||
NullLogger<DeterminizationGate>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_BuildsCorrectMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var snapshot = CreateSnapshot();
|
||||
var uncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = 0.45,
|
||||
Tier = UncertaintyTier.Moderate,
|
||||
Completeness = 0.55,
|
||||
MissingSignals = []
|
||||
};
|
||||
|
||||
_snapshotBuilderMock
|
||||
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(snapshot);
|
||||
|
||||
_uncertaintyCalculatorMock
|
||||
.Setup(x => x.Calculate(It.IsAny<SignalSnapshot>()))
|
||||
.Returns(uncertaintyScore);
|
||||
|
||||
_decayCalculatorMock
|
||||
.Setup(x => x.Calculate(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>()))
|
||||
.Returns(0.85);
|
||||
|
||||
_trustAggregatorMock
|
||||
.Setup(x => x.Aggregate(It.IsAny<SignalSnapshot>(), It.IsAny<UncertaintyScore>()))
|
||||
.Returns(0.7);
|
||||
|
||||
var context = new PolicyGateContext
|
||||
{
|
||||
CveId = "CVE-2024-0001",
|
||||
SubjectKey = "pkg:npm/test@1.0.0",
|
||||
Environment = "development"
|
||||
};
|
||||
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
FinalScore = 0.5,
|
||||
FinalTrustLevel = TrustLevel.Medium,
|
||||
Claims = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Details.Should().ContainKey("uncertainty_entropy");
|
||||
result.Details["uncertainty_entropy"].Should().Be(0.45);
|
||||
|
||||
result.Details.Should().ContainKey("uncertainty_tier");
|
||||
result.Details["uncertainty_tier"].Should().Be("Moderate");
|
||||
|
||||
result.Details.Should().ContainKey("uncertainty_completeness");
|
||||
result.Details["uncertainty_completeness"].Should().Be(0.55);
|
||||
|
||||
result.Details.Should().ContainKey("trust_score");
|
||||
result.Details["trust_score"].Should().Be(0.7);
|
||||
|
||||
result.Details.Should().ContainKey("decay_multiplier");
|
||||
result.Details.Should().ContainKey("decay_is_stale");
|
||||
result.Details.Should().ContainKey("decay_age_days");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WithGuardRails_IncludesGuardrailsMetadata()
|
||||
{
|
||||
// Arrange
|
||||
var snapshot = CreateSnapshot();
|
||||
var uncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = 0.5,
|
||||
Tier = UncertaintyTier.Moderate,
|
||||
Completeness = 0.5,
|
||||
MissingSignals = []
|
||||
};
|
||||
|
||||
_snapshotBuilderMock
|
||||
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(snapshot);
|
||||
|
||||
_uncertaintyCalculatorMock
|
||||
.Setup(x => x.Calculate(It.IsAny<SignalSnapshot>()))
|
||||
.Returns(uncertaintyScore);
|
||||
|
||||
_decayCalculatorMock
|
||||
.Setup(x => x.Calculate(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>()))
|
||||
.Returns(0.85);
|
||||
|
||||
_trustAggregatorMock
|
||||
.Setup(x => x.Aggregate(It.IsAny<SignalSnapshot>(), It.IsAny<UncertaintyScore>()))
|
||||
.Returns(0.3);
|
||||
|
||||
var context = new PolicyGateContext
|
||||
{
|
||||
CveId = "CVE-2024-0001",
|
||||
SubjectKey = "pkg:npm/test@1.0.0",
|
||||
Environment = "development"
|
||||
};
|
||||
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
FinalScore = 0.5,
|
||||
FinalTrustLevel = TrustLevel.Medium,
|
||||
Claims = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Details.Should().ContainKey("guardrails_monitoring");
|
||||
result.Details.Should().ContainKey("guardrails_reeval_after");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task EvaluateAsync_WithMatchedRule_IncludesRuleName()
|
||||
{
|
||||
// Arrange
|
||||
var snapshot = CreateSnapshot();
|
||||
var uncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = 0.2,
|
||||
Tier = UncertaintyTier.Low,
|
||||
Completeness = 0.8,
|
||||
MissingSignals = []
|
||||
};
|
||||
|
||||
_snapshotBuilderMock
|
||||
.Setup(x => x.BuildAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(snapshot);
|
||||
|
||||
_uncertaintyCalculatorMock
|
||||
.Setup(x => x.Calculate(It.IsAny<SignalSnapshot>()))
|
||||
.Returns(uncertaintyScore);
|
||||
|
||||
_decayCalculatorMock
|
||||
.Setup(x => x.Calculate(It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>(), It.IsAny<double>()))
|
||||
.Returns(0.9);
|
||||
|
||||
_trustAggregatorMock
|
||||
.Setup(x => x.Aggregate(It.IsAny<SignalSnapshot>(), It.IsAny<UncertaintyScore>()))
|
||||
.Returns(0.8);
|
||||
|
||||
var context = new PolicyGateContext
|
||||
{
|
||||
CveId = "CVE-2024-0001",
|
||||
SubjectKey = "pkg:npm/test@1.0.0",
|
||||
Environment = "production"
|
||||
};
|
||||
|
||||
var mergeResult = new MergeResult
|
||||
{
|
||||
FinalScore = 0.8,
|
||||
FinalTrustLevel = TrustLevel.High,
|
||||
Claims = []
|
||||
};
|
||||
|
||||
// Act
|
||||
var result = await _gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Details.Should().ContainKey("matched_rule");
|
||||
result.Details["matched_rule"].Should().NotBeNull();
|
||||
}
|
||||
|
||||
private static SignalSnapshot CreateSnapshot() => new()
|
||||
{
|
||||
Cve = "CVE-2024-0001",
|
||||
Purl = "pkg:npm/test@1.0.0",
|
||||
Epss = SignalState<EpssEvidence>.NotQueried(),
|
||||
Vex = SignalState<VexClaimSummary>.NotQueried(),
|
||||
Reachability = SignalState<ReachabilityEvidence>.NotQueried(),
|
||||
Runtime = SignalState<RuntimeEvidence>.NotQueried(),
|
||||
Backport = SignalState<BackportEvidence>.NotQueried(),
|
||||
Sbom = SignalState<SbomLineageEvidence>.NotQueried(),
|
||||
Cvss = SignalState<CvssEvidence>.NotQueried(),
|
||||
SnapshotAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
// -----------------------------------------------------------------------------
|
||||
// FacetQuotaGateIntegrationTests.cs
|
||||
// Sprint: SPRINT_20260105_002_003_FACET (QTA-015)
|
||||
// Task: QTA-015 - Integration tests for facet quota gate pipeline
|
||||
// Description: End-to-end tests for facet drift detection and quota enforcement
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Moq;
|
||||
using StellaOps.Facet;
|
||||
using StellaOps.Policy.Confidence.Models;
|
||||
using StellaOps.Policy.Gates;
|
||||
using StellaOps.Policy.TrustLattice;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Gates;
|
||||
|
||||
/// <summary>
|
||||
/// Integration tests for the facet quota gate pipeline.
|
||||
/// Tests end-to-end flow from drift reports through gate evaluation.
|
||||
/// </summary>
|
||||
[Trait("Category", "Integration")]
|
||||
public sealed class FacetQuotaGateIntegrationTests
|
||||
{
|
||||
private readonly InMemoryFacetSealStore _sealStore;
|
||||
private readonly Mock<IFacetDriftDetector> _driftDetector;
|
||||
private readonly FacetSealer _sealer;
|
||||
|
||||
public FacetQuotaGateIntegrationTests()
|
||||
{
|
||||
_sealStore = new InMemoryFacetSealStore();
|
||||
_driftDetector = new Mock<IFacetDriftDetector>();
|
||||
_sealer = new FacetSealer();
|
||||
}
|
||||
|
||||
#region Full Pipeline Tests
|
||||
|
||||
[Fact]
|
||||
public async Task FullPipeline_FirstScan_NoBaseline_PassesWithWarning()
|
||||
{
|
||||
// Arrange: No baseline seal exists
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
NoSealAction = NoSealAction.Warn
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
var context = new PolicyGateContext { Environment = "production" };
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue();
|
||||
result.Reason.Should().Be("no_baseline_seal");
|
||||
result.Details.Should().ContainKey("action");
|
||||
result.Details["action"].Should().Be("warn");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullPipeline_WithBaseline_NoDrift_Passes()
|
||||
{
|
||||
// Arrange: Create baseline seal
|
||||
var imageDigest = "sha256:abc123";
|
||||
var baselineSeal = CreateSeal(imageDigest, 100);
|
||||
await _sealStore.SaveAsync(baselineSeal);
|
||||
|
||||
// Setup drift detector to return no drift
|
||||
var driftReport = CreateDriftReport(imageDigest, baselineSeal.CombinedMerkleRoot, QuotaVerdict.Ok);
|
||||
SetupDriftDetector(driftReport);
|
||||
|
||||
var options = new FacetQuotaGateOptions { Enabled = true };
|
||||
var gate = CreateGate(options);
|
||||
|
||||
var context = CreateContextWithDriftReport(driftReport);
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue();
|
||||
result.Reason.Should().Be("quota_ok");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullPipeline_ExceedWarningThreshold_PassesWithWarning()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:def456";
|
||||
var baselineSeal = CreateSeal(imageDigest, 100);
|
||||
await _sealStore.SaveAsync(baselineSeal);
|
||||
|
||||
var driftReport = CreateDriftReport(imageDigest, baselineSeal.CombinedMerkleRoot, QuotaVerdict.Warning);
|
||||
SetupDriftDetector(driftReport);
|
||||
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultMaxChurnPercent = 10.0m
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
var context = CreateContextWithDriftReport(driftReport);
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue();
|
||||
result.Reason.Should().Be("quota_warning");
|
||||
result.Details.Should().ContainKey("breachedFacets");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullPipeline_ExceedBlockThreshold_Blocks()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:ghi789";
|
||||
var baselineSeal = CreateSeal(imageDigest, 100);
|
||||
await _sealStore.SaveAsync(baselineSeal);
|
||||
|
||||
var driftReport = CreateDriftReport(imageDigest, baselineSeal.CombinedMerkleRoot, QuotaVerdict.Blocked);
|
||||
SetupDriftDetector(driftReport);
|
||||
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultAction = QuotaExceededAction.Block
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
var context = CreateContextWithDriftReport(driftReport);
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeFalse();
|
||||
result.Reason.Should().Be("quota_exceeded");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task FullPipeline_RequiresVex_BlocksUntilVexProvided()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:jkl012";
|
||||
var baselineSeal = CreateSeal(imageDigest, 100);
|
||||
await _sealStore.SaveAsync(baselineSeal);
|
||||
|
||||
var driftReport = CreateDriftReport(imageDigest, baselineSeal.CombinedMerkleRoot, QuotaVerdict.RequiresVex);
|
||||
SetupDriftDetector(driftReport);
|
||||
|
||||
var options = new FacetQuotaGateOptions
|
||||
{
|
||||
Enabled = true,
|
||||
DefaultAction = QuotaExceededAction.RequireVex
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
var context = CreateContextWithDriftReport(driftReport);
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeFalse();
|
||||
result.Reason.Should().Be("requires_vex_authorization");
|
||||
result.Details.Should().ContainKey("vexRequired");
|
||||
((bool)result.Details["vexRequired"]).Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Multi-Facet Tests
|
||||
|
||||
[Fact]
|
||||
public async Task MultiFacet_MixedVerdicts_ReportsWorstCase()
|
||||
{
|
||||
// Arrange: Multiple facets with different verdicts
|
||||
var imageDigest = "sha256:multi123";
|
||||
var baselineSeal = CreateSeal(imageDigest, 100);
|
||||
await _sealStore.SaveAsync(baselineSeal);
|
||||
|
||||
var facetDrifts = new[]
|
||||
{
|
||||
CreateFacetDrift("os-packages", QuotaVerdict.Ok),
|
||||
CreateFacetDrift("app-dependencies", QuotaVerdict.Warning),
|
||||
CreateFacetDrift("config-files", QuotaVerdict.Blocked)
|
||||
};
|
||||
|
||||
var driftReport = new FacetDriftReport
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
BaselineSealId = baselineSeal.CombinedMerkleRoot,
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
FacetDrifts = [.. facetDrifts],
|
||||
OverallVerdict = QuotaVerdict.Blocked // Worst case
|
||||
};
|
||||
|
||||
SetupDriftDetector(driftReport);
|
||||
|
||||
var options = new FacetQuotaGateOptions { Enabled = true };
|
||||
var gate = CreateGate(options);
|
||||
|
||||
var context = CreateContextWithDriftReport(driftReport);
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeFalse();
|
||||
result.Reason.Should().Be("quota_exceeded");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MultiFacet_AllWithinQuota_Passes()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:allok456";
|
||||
var baselineSeal = CreateSeal(imageDigest, 100);
|
||||
await _sealStore.SaveAsync(baselineSeal);
|
||||
|
||||
var facetDrifts = new[]
|
||||
{
|
||||
CreateFacetDrift("os-packages", QuotaVerdict.Ok),
|
||||
CreateFacetDrift("app-dependencies", QuotaVerdict.Ok),
|
||||
CreateFacetDrift("config-files", QuotaVerdict.Ok)
|
||||
};
|
||||
|
||||
var driftReport = new FacetDriftReport
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
BaselineSealId = baselineSeal.CombinedMerkleRoot,
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
FacetDrifts = [.. facetDrifts],
|
||||
OverallVerdict = QuotaVerdict.Ok
|
||||
};
|
||||
|
||||
SetupDriftDetector(driftReport);
|
||||
|
||||
var options = new FacetQuotaGateOptions { Enabled = true };
|
||||
var gate = CreateGate(options);
|
||||
|
||||
var context = CreateContextWithDriftReport(driftReport);
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Seal Store Integration
|
||||
|
||||
[Fact]
|
||||
public async Task SealStore_SaveAndRetrieve_WorksCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:store123";
|
||||
var seal = CreateSeal(imageDigest, 50);
|
||||
|
||||
// Act
|
||||
await _sealStore.SaveAsync(seal);
|
||||
var retrieved = await _sealStore.GetLatestSealAsync(imageDigest);
|
||||
|
||||
// Assert
|
||||
retrieved.Should().NotBeNull();
|
||||
retrieved!.ImageDigest.Should().Be(imageDigest);
|
||||
retrieved.CombinedMerkleRoot.Should().Be(seal.CombinedMerkleRoot);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SealStore_MultipleSeals_ReturnsLatest()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:multi789";
|
||||
var seal1 = CreateSealWithTimestamp(imageDigest, 50, DateTimeOffset.UtcNow.AddHours(-2));
|
||||
var seal2 = CreateSealWithTimestamp(imageDigest, 55, DateTimeOffset.UtcNow.AddHours(-1));
|
||||
var seal3 = CreateSealWithTimestamp(imageDigest, 60, DateTimeOffset.UtcNow);
|
||||
|
||||
await _sealStore.SaveAsync(seal1);
|
||||
await _sealStore.SaveAsync(seal2);
|
||||
await _sealStore.SaveAsync(seal3);
|
||||
|
||||
// Act
|
||||
var latest = await _sealStore.GetLatestSealAsync(imageDigest);
|
||||
|
||||
// Assert
|
||||
latest.Should().NotBeNull();
|
||||
latest!.CreatedAt.Should().Be(seal3.CreatedAt);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task SealStore_History_ReturnsInDescendingOrder()
|
||||
{
|
||||
// Arrange
|
||||
var imageDigest = "sha256:history123";
|
||||
var seal1 = CreateSealWithTimestamp(imageDigest, 50, DateTimeOffset.UtcNow.AddHours(-2));
|
||||
var seal2 = CreateSealWithTimestamp(imageDigest, 55, DateTimeOffset.UtcNow.AddHours(-1));
|
||||
var seal3 = CreateSealWithTimestamp(imageDigest, 60, DateTimeOffset.UtcNow);
|
||||
|
||||
await _sealStore.SaveAsync(seal1);
|
||||
await _sealStore.SaveAsync(seal2);
|
||||
await _sealStore.SaveAsync(seal3);
|
||||
|
||||
// Act
|
||||
var history = await _sealStore.GetHistoryAsync(imageDigest, limit: 10);
|
||||
|
||||
// Assert
|
||||
history.Should().HaveCount(3);
|
||||
history[0].CreatedAt.Should().Be(seal3.CreatedAt);
|
||||
history[1].CreatedAt.Should().Be(seal2.CreatedAt);
|
||||
history[2].CreatedAt.Should().Be(seal1.CreatedAt);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Configuration Tests
|
||||
|
||||
[Fact]
|
||||
public async Task Configuration_PerFacetOverride_AppliesCorrectly()
|
||||
{
|
||||
// Arrange: os-packages has higher threshold
|
||||
var imageDigest = "sha256:override123";
|
||||
var baselineSeal = CreateSeal(imageDigest, 100);
|
||||
await _sealStore.SaveAsync(baselineSeal);
|
||||
|
||||
var driftReport = CreateDriftReportWithChurn(imageDigest, baselineSeal.CombinedMerkleRoot, "os-packages", 25m);
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
};
|
||||
var gate = CreateGate(options);
|
||||
|
||||
var context = CreateContextWithDriftReport(driftReport);
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert: 25% churn is within the 30% override threshold
|
||||
result.Passed.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Configuration_DisabledGate_BypassesAllChecks()
|
||||
{
|
||||
// Arrange
|
||||
var options = new FacetQuotaGateOptions { Enabled = false };
|
||||
var gate = CreateGate(options);
|
||||
|
||||
var context = new PolicyGateContext { Environment = "production" };
|
||||
var mergeResult = CreateMergeResult(VexStatus.NotAffected);
|
||||
|
||||
// Act
|
||||
var result = await gate.EvaluateAsync(mergeResult, context);
|
||||
|
||||
// Assert
|
||||
result.Passed.Should().BeTrue();
|
||||
result.Reason.Should().Be("Gate disabled");
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private FacetQuotaGate CreateGate(FacetQuotaGateOptions options)
|
||||
{
|
||||
return new FacetQuotaGate(options, _driftDetector.Object, NullLogger<FacetQuotaGate>.Instance);
|
||||
}
|
||||
|
||||
private void SetupDriftDetector(FacetDriftReport report)
|
||||
{
|
||||
_driftDetector
|
||||
.Setup(d => d.DetectDriftAsync(It.IsAny<FacetSeal>(), It.IsAny<FacetSeal>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(report);
|
||||
}
|
||||
|
||||
private static PolicyGateContext CreateContextWithDriftReport(FacetDriftReport report)
|
||||
{
|
||||
var json = JsonSerializer.Serialize(report);
|
||||
return new PolicyGateContext
|
||||
{
|
||||
Environment = "production",
|
||||
Metadata = new Dictionary<string, string>
|
||||
{
|
||||
["FacetDriftReport"] = json
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private static MergeResult CreateMergeResult(VexStatus status)
|
||||
{
|
||||
var claim = new ScoredClaim
|
||||
{
|
||||
SourceId = "test",
|
||||
Status = status,
|
||||
OriginalScore = 1.0,
|
||||
AdjustedScore = 1.0,
|
||||
ScopeSpecificity = 1,
|
||||
Accepted = true,
|
||||
Reason = "test"
|
||||
};
|
||||
|
||||
return new MergeResult
|
||||
{
|
||||
Status = status,
|
||||
Confidence = 0.9,
|
||||
HasConflicts = false,
|
||||
AllClaims = [claim],
|
||||
WinningClaim = claim,
|
||||
Conflicts = []
|
||||
};
|
||||
}
|
||||
|
||||
private FacetSeal CreateSeal(string imageDigest, int fileCount)
|
||||
{
|
||||
return CreateSealWithTimestamp(imageDigest, fileCount, DateTimeOffset.UtcNow);
|
||||
}
|
||||
|
||||
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}");
|
||||
|
||||
return new FacetSeal
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
SchemaVersion = "1.0.0",
|
||||
CreatedAt = createdAt,
|
||||
Facets = [facetEntry],
|
||||
CombinedMerkleRoot = $"sha256:combined{imageDigest.GetHashCode():x8}{createdAt.Ticks:x8}"
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetDriftReport CreateDriftReport(string imageDigest, string baselineSealId, QuotaVerdict verdict)
|
||||
{
|
||||
return new FacetDriftReport
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
BaselineSealId = baselineSealId,
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
FacetDrifts = [CreateFacetDrift("test-facet", verdict)],
|
||||
OverallVerdict = verdict
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetDriftReport CreateDriftReportWithChurn(
|
||||
string imageDigest,
|
||||
string baselineSealId,
|
||||
string facetId,
|
||||
decimal churnPercent)
|
||||
{
|
||||
var addedCount = (int)(churnPercent * 100 / 100); // For 100 baseline files
|
||||
var addedFiles = Enumerable.Range(0, addedCount)
|
||||
.Select(i => new FacetFileEntry($"/added{i}.txt", $"sha256:added{i}", 100, null))
|
||||
.ToImmutableArray();
|
||||
|
||||
var verdict = churnPercent switch
|
||||
{
|
||||
< 10 => QuotaVerdict.Ok,
|
||||
< 20 => QuotaVerdict.Warning,
|
||||
_ => QuotaVerdict.Blocked
|
||||
};
|
||||
|
||||
var facetDrift = new FacetDrift
|
||||
{
|
||||
FacetId = facetId,
|
||||
Added = addedFiles,
|
||||
Removed = [],
|
||||
Modified = [],
|
||||
DriftScore = churnPercent,
|
||||
QuotaVerdict = verdict,
|
||||
BaselineFileCount = 100
|
||||
};
|
||||
|
||||
return new FacetDriftReport
|
||||
{
|
||||
ImageDigest = imageDigest,
|
||||
BaselineSealId = baselineSealId,
|
||||
AnalyzedAt = DateTimeOffset.UtcNow,
|
||||
FacetDrifts = [facetDrift],
|
||||
OverallVerdict = verdict
|
||||
};
|
||||
}
|
||||
|
||||
private static FacetDrift CreateFacetDrift(string facetId, QuotaVerdict verdict)
|
||||
{
|
||||
var addedCount = verdict switch
|
||||
{
|
||||
QuotaVerdict.Warning => 15,
|
||||
QuotaVerdict.Blocked => 35,
|
||||
QuotaVerdict.RequiresVex => 50,
|
||||
_ => 0
|
||||
};
|
||||
|
||||
var addedFiles = Enumerable.Range(0, addedCount)
|
||||
.Select(i => new FacetFileEntry($"/added{i}.txt", $"sha256:added{i}", 100, null))
|
||||
.ToImmutableArray();
|
||||
|
||||
return new FacetDrift
|
||||
{
|
||||
FacetId = facetId,
|
||||
Added = addedFiles,
|
||||
Removed = [],
|
||||
Modified = [],
|
||||
DriftScore = addedCount,
|
||||
QuotaVerdict = verdict,
|
||||
BaselineFileCount = 100
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
@@ -0,0 +1,276 @@
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Options;
|
||||
using StellaOps.Policy;
|
||||
using StellaOps.Policy.Determinization;
|
||||
using StellaOps.Policy.Determinization.Models;
|
||||
using StellaOps.Policy.Engine.Policies;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Policies;
|
||||
|
||||
public class DeterminizationPolicyTests
|
||||
{
|
||||
private readonly DeterminizationPolicy _policy;
|
||||
|
||||
public DeterminizationPolicyTests()
|
||||
{
|
||||
var options = Options.Create(new DeterminizationOptions());
|
||||
_policy = new DeterminizationPolicy(options, NullLogger<DeterminizationPolicy>.Instance);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_RuntimeEvidenceLoaded_ReturnsEscalated()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
runtime: new SignalState<RuntimeEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new RuntimeEvidence { ObservedLoaded = true }
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.Escalated);
|
||||
result.MatchedRule.Should().Be("RuntimeEscalation");
|
||||
result.Reason.Should().Contain("Runtime evidence shows vulnerable code loaded");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_HighEpss_ReturnsQuarantined()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
epss: new SignalState<EpssEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new EpssEvidence { Score = 0.8 }
|
||||
},
|
||||
environment: DeploymentEnvironment.Production);
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.Blocked);
|
||||
result.MatchedRule.Should().Be("EpssQuarantine");
|
||||
result.Reason.Should().Contain("EPSS score");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ReachableCode_ReturnsQuarantined()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
reachability: new SignalState<ReachabilityEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new ReachabilityEvidence { IsReachable = true, Confidence = 0.9 }
|
||||
});
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.Blocked);
|
||||
result.MatchedRule.Should().Be("ReachabilityQuarantine");
|
||||
result.Reason.Should().Contain("reachable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_HighEntropyInProduction_ReturnsQuarantined()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
entropy: 0.5,
|
||||
environment: DeploymentEnvironment.Production);
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.Blocked);
|
||||
result.MatchedRule.Should().Be("ProductionEntropyBlock");
|
||||
result.Reason.Should().Contain("High uncertainty");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_StaleEvidence_ReturnsDeferred()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
isStale: true);
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.Deferred);
|
||||
result.MatchedRule.Should().Be("StaleEvidenceDefer");
|
||||
result.Reason.Should().Contain("stale");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ModerateUncertaintyInDev_ReturnsGuardedPass()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
entropy: 0.5,
|
||||
trustScore: 0.3,
|
||||
environment: DeploymentEnvironment.Development);
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.GuardedPass);
|
||||
result.MatchedRule.Should().Be("GuardedAllowNonProd");
|
||||
result.GuardRails.Should().NotBeNull();
|
||||
result.GuardRails!.EnableMonitoring.Should().BeTrue();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_UnreachableWithHighConfidence_ReturnsAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
reachability: new SignalState<ReachabilityEvidence>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new ReachabilityEvidence { IsReachable = false, Confidence = 0.9 }
|
||||
},
|
||||
trustScore: 0.8);
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.Pass);
|
||||
result.MatchedRule.Should().Be("UnreachableAllow");
|
||||
result.Reason.Should().Contain("unreachable");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_VexNotAffected_ReturnsAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
vex: new SignalState<VexClaimSummary>
|
||||
{
|
||||
HasValue = true,
|
||||
Value = new VexClaimSummary { IsNotAffected = true, IssuerTrust = 0.9 }
|
||||
},
|
||||
trustScore: 0.8);
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.Pass);
|
||||
result.MatchedRule.Should().Be("VexNotAffectedAllow");
|
||||
result.Reason.Should().Contain("not_affected");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_SufficientEvidenceLowEntropy_ReturnsAllowed()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
entropy: 0.2,
|
||||
trustScore: 0.8,
|
||||
environment: DeploymentEnvironment.Production);
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.Pass);
|
||||
result.MatchedRule.Should().Be("SufficientEvidenceAllow");
|
||||
result.Reason.Should().Contain("Sufficient evidence");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_ModerateUncertaintyTier_ReturnsGuardedPass()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
tier: UncertaintyTier.Moderate,
|
||||
trustScore: 0.5,
|
||||
entropy: 0.5);
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.GuardedPass);
|
||||
result.MatchedRule.Should().Be("GuardedAllowModerateUncertainty");
|
||||
result.GuardRails.Should().NotBeNull();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Evaluate_NoMatchingRule_ReturnsDeferred()
|
||||
{
|
||||
// Arrange
|
||||
var context = CreateContext(
|
||||
entropy: 0.9,
|
||||
trustScore: 0.1,
|
||||
environment: DeploymentEnvironment.Production);
|
||||
|
||||
// Act
|
||||
var result = _policy.Evaluate(context);
|
||||
|
||||
// Assert
|
||||
result.Status.Should().Be(PolicyVerdictStatus.Deferred);
|
||||
result.MatchedRule.Should().Be("DefaultDefer");
|
||||
result.Reason.Should().Contain("Insufficient evidence");
|
||||
}
|
||||
|
||||
private static DeterminizationContext CreateContext(
|
||||
SignalState<EpssEvidence>? epss = null,
|
||||
SignalState<VexClaimSummary>? vex = null,
|
||||
SignalState<ReachabilityEvidence>? reachability = null,
|
||||
SignalState<RuntimeEvidence>? runtime = null,
|
||||
double entropy = 0.0,
|
||||
double trustScore = 0.0,
|
||||
UncertaintyTier tier = UncertaintyTier.Minimal,
|
||||
DeploymentEnvironment environment = DeploymentEnvironment.Development,
|
||||
bool isStale = false)
|
||||
{
|
||||
var snapshot = new SignalSnapshot
|
||||
{
|
||||
Cve = "CVE-2024-0001",
|
||||
Purl = "pkg:npm/test@1.0.0",
|
||||
Epss = epss ?? SignalState<EpssEvidence>.NotQueried(),
|
||||
Vex = vex ?? SignalState<VexClaimSummary>.NotQueried(),
|
||||
Reachability = reachability ?? SignalState<ReachabilityEvidence>.NotQueried(),
|
||||
Runtime = runtime ?? SignalState<RuntimeEvidence>.NotQueried(),
|
||||
Backport = SignalState<BackportEvidence>.NotQueried(),
|
||||
Sbom = SignalState<SbomLineageEvidence>.NotQueried(),
|
||||
Cvss = SignalState<CvssEvidence>.NotQueried(),
|
||||
SnapshotAt = DateTimeOffset.UtcNow
|
||||
};
|
||||
|
||||
return new DeterminizationContext
|
||||
{
|
||||
SignalSnapshot = snapshot,
|
||||
UncertaintyScore = new UncertaintyScore
|
||||
{
|
||||
Entropy = entropy,
|
||||
Tier = tier,
|
||||
Completeness = 1.0 - entropy,
|
||||
MissingSignals = []
|
||||
},
|
||||
Decay = new ObservationDecay
|
||||
{
|
||||
LastSignalUpdate = DateTimeOffset.UtcNow.AddDays(-1),
|
||||
AgeDays = 1,
|
||||
DecayedMultiplier = isStale ? 0.3 : 0.9,
|
||||
IsStale = isStale
|
||||
},
|
||||
TrustScore = trustScore,
|
||||
Environment = environment
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
using FluentAssertions;
|
||||
using StellaOps.Policy.Determinization;
|
||||
using StellaOps.Policy.Engine.Policies;
|
||||
|
||||
namespace StellaOps.Policy.Engine.Tests.Policies;
|
||||
|
||||
public class DeterminizationRuleSetTests
|
||||
{
|
||||
[Fact]
|
||||
public void Default_RulesAreOrderedByPriority()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeterminizationOptions();
|
||||
|
||||
// Act
|
||||
var ruleSet = DeterminizationRuleSet.Default(options);
|
||||
|
||||
// Assert
|
||||
ruleSet.Rules.Should().HaveCountGreaterThan(0);
|
||||
|
||||
var priorities = ruleSet.Rules.Select(r => r.Priority).ToList();
|
||||
priorities.Should().BeInAscendingOrder("rules should be evaluable in priority order");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_RuntimeEscalationHasHighestPriority()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeterminizationOptions();
|
||||
|
||||
// Act
|
||||
var ruleSet = DeterminizationRuleSet.Default(options);
|
||||
|
||||
// Assert
|
||||
var runtimeRule = ruleSet.Rules.First(r => r.Name == "RuntimeEscalation");
|
||||
runtimeRule.Priority.Should().Be(10, "runtime escalation should have highest priority");
|
||||
|
||||
var allOtherRules = ruleSet.Rules.Where(r => r.Name != "RuntimeEscalation");
|
||||
allOtherRules.Should().AllSatisfy(r => r.Priority.Should().BeGreaterThan(10));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_DefaultDeferHasLowestPriority()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeterminizationOptions();
|
||||
|
||||
// Act
|
||||
var ruleSet = DeterminizationRuleSet.Default(options);
|
||||
|
||||
// Assert
|
||||
var defaultRule = ruleSet.Rules.First(r => r.Name == "DefaultDefer");
|
||||
defaultRule.Priority.Should().Be(100, "default defer should be catch-all with lowest priority");
|
||||
|
||||
var allOtherRules = ruleSet.Rules.Where(r => r.Name != "DefaultDefer");
|
||||
allOtherRules.Should().AllSatisfy(r => r.Priority.Should().BeLessThan(100));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_QuarantineRulesBeforeAllowRules()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeterminizationOptions();
|
||||
|
||||
// Act
|
||||
var ruleSet = DeterminizationRuleSet.Default(options);
|
||||
|
||||
// Assert
|
||||
var epssQuarantine = ruleSet.Rules.First(r => r.Name == "EpssQuarantine");
|
||||
var reachabilityQuarantine = ruleSet.Rules.First(r => r.Name == "ReachabilityQuarantine");
|
||||
var productionBlock = ruleSet.Rules.First(r => r.Name == "ProductionEntropyBlock");
|
||||
|
||||
var unreachableAllow = ruleSet.Rules.First(r => r.Name == "UnreachableAllow");
|
||||
var vexAllow = ruleSet.Rules.First(r => r.Name == "VexNotAffectedAllow");
|
||||
var sufficientEvidenceAllow = ruleSet.Rules.First(r => r.Name == "SufficientEvidenceAllow");
|
||||
|
||||
epssQuarantine.Priority.Should().BeLessThan(unreachableAllow.Priority);
|
||||
reachabilityQuarantine.Priority.Should().BeLessThan(vexAllow.Priority);
|
||||
productionBlock.Priority.Should().BeLessThan(sufficientEvidenceAllow.Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_AllRulesHaveUniquePriorities()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeterminizationOptions();
|
||||
|
||||
// Act
|
||||
var ruleSet = DeterminizationRuleSet.Default(options);
|
||||
|
||||
// Assert
|
||||
var priorities = ruleSet.Rules.Select(r => r.Priority).ToList();
|
||||
priorities.Should().OnlyHaveUniqueItems("each rule should have unique priority for deterministic ordering");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_AllRulesHaveNames()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeterminizationOptions();
|
||||
|
||||
// Act
|
||||
var ruleSet = DeterminizationRuleSet.Default(options);
|
||||
|
||||
// Assert
|
||||
ruleSet.Rules.Should().AllSatisfy(r =>
|
||||
{
|
||||
r.Name.Should().NotBeNullOrWhiteSpace("all rules must have names for audit trail");
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_Contains11Rules()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeterminizationOptions();
|
||||
|
||||
// Act
|
||||
var ruleSet = DeterminizationRuleSet.Default(options);
|
||||
|
||||
// Assert
|
||||
ruleSet.Rules.Should().HaveCount(11, "rule set should contain all 11 specified rules");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Default_ContainsExpectedRules()
|
||||
{
|
||||
// Arrange
|
||||
var options = new DeterminizationOptions();
|
||||
var expectedRuleNames = new[]
|
||||
{
|
||||
"RuntimeEscalation",
|
||||
"EpssQuarantine",
|
||||
"ReachabilityQuarantine",
|
||||
"ProductionEntropyBlock",
|
||||
"StaleEvidenceDefer",
|
||||
"GuardedAllowNonProd",
|
||||
"UnreachableAllow",
|
||||
"VexNotAffectedAllow",
|
||||
"SufficientEvidenceAllow",
|
||||
"GuardedAllowModerateUncertainty",
|
||||
"DefaultDefer"
|
||||
};
|
||||
|
||||
// Act
|
||||
var ruleSet = DeterminizationRuleSet.Default(options);
|
||||
|
||||
// Assert
|
||||
var actualNames = ruleSet.Rules.Select(r => r.Name).ToList();
|
||||
actualNames.Should().BeEquivalentTo(expectedRuleNames);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user