audit work, fixed StellaOps.sln warnings/errors, fixed tests, sprints work, new advisories

This commit is contained in:
master
2026-01-07 18:49:59 +02:00
parent 04ec098046
commit 608a7f85c0
866 changed files with 56323 additions and 6231 deletions

View File

@@ -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]

View File

@@ -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>

View File

@@ -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
};
}
}

View File

@@ -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
{

View File

@@ -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();
}
}

View File

@@ -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

View File

@@ -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
};
}