up
Some checks failed
AOC Guard CI / aoc-guard (push) Has been cancelled
AOC Guard CI / aoc-verify (push) Has been cancelled
Concelier Attestation Tests / attestation-tests (push) Has been cancelled
Docs CI / lint-and-preview (push) Has been cancelled
Policy Lint & Smoke / policy-lint (push) Has been cancelled

This commit is contained in:
StellaOps Bot
2025-11-28 09:40:40 +02:00
parent 1c6730a1d2
commit 05da719048
206 changed files with 34741 additions and 1751 deletions

View File

@@ -0,0 +1,319 @@
using System.Collections.Immutable;
using FluentAssertions;
using Microsoft.Extensions.Time.Testing;
using StellaOps.Policy.Engine.IncrementalOrchestrator;
using StellaOps.Policy.Engine.Telemetry;
using Xunit;
namespace StellaOps.Policy.Engine.Tests.IncrementalOrchestrator;
public sealed class IncrementalOrchestratorTests
{
#region PolicyChangeEvent Tests
[Fact]
public void CreateAdvisoryUpdated_CreatesValidEvent()
{
var now = DateTimeOffset.UtcNow;
var evt = PolicyChangeEventFactory.CreateAdvisoryUpdated(
tenantId: "test-tenant",
advisoryId: "GHSA-test-001",
vulnerabilityId: "CVE-2021-12345",
affectedPurls: ["pkg:npm/lodash", "pkg:npm/express"],
source: "concelier",
occurredAt: now,
createdAt: now);
evt.ChangeType.Should().Be(PolicyChangeType.AdvisoryUpdated);
evt.TenantId.Should().Be("test-tenant");
evt.AdvisoryId.Should().Be("GHSA-test-001");
evt.VulnerabilityId.Should().Be("CVE-2021-12345");
evt.AffectedPurls.Should().HaveCount(2);
evt.EventId.Should().StartWith("pce-");
evt.ContentHash.Should().NotBeNullOrEmpty();
}
[Fact]
public void CreateVexUpdated_CreatesValidEvent()
{
var now = DateTimeOffset.UtcNow;
var evt = PolicyChangeEventFactory.CreateVexUpdated(
tenantId: "test-tenant",
vulnerabilityId: "CVE-2021-12345",
affectedProductKeys: ["pkg:npm/lodash"],
source: "excititor",
occurredAt: now,
createdAt: now);
evt.ChangeType.Should().Be(PolicyChangeType.VexStatementUpdated);
evt.VulnerabilityId.Should().Be("CVE-2021-12345");
evt.AffectedProductKeys.Should().ContainSingle();
}
[Fact]
public void CreateSbomUpdated_CreatesValidEvent()
{
var now = DateTimeOffset.UtcNow;
var evt = PolicyChangeEventFactory.CreateSbomUpdated(
tenantId: "test-tenant",
sbomId: "sbom-123",
productKey: "myapp:v1.0.0",
componentPurls: ["pkg:npm/lodash@4.17.21"],
source: "scanner",
occurredAt: now,
createdAt: now);
evt.ChangeType.Should().Be(PolicyChangeType.SbomUpdated);
evt.AffectedSbomIds.Should().Contain("sbom-123");
evt.AffectedProductKeys.Should().Contain("myapp:v1.0.0");
}
[Fact]
public void ComputeContentHash_IsDeterministic()
{
var hash1 = PolicyChangeEvent.ComputeContentHash(
PolicyChangeType.AdvisoryUpdated,
"tenant",
"ADV-001",
"CVE-001",
["pkg:npm/a", "pkg:npm/b"],
null,
null);
var hash2 = PolicyChangeEvent.ComputeContentHash(
PolicyChangeType.AdvisoryUpdated,
"tenant",
"ADV-001",
"CVE-001",
["pkg:npm/b", "pkg:npm/a"], // Different order
null,
null);
hash1.Should().Be(hash2); // Should be equal due to sorting
}
[Fact]
public void ComputeContentHash_DiffersForDifferentInput()
{
var hash1 = PolicyChangeEvent.ComputeContentHash(
PolicyChangeType.AdvisoryUpdated,
"tenant",
"ADV-001",
"CVE-001",
null, null, null);
var hash2 = PolicyChangeEvent.ComputeContentHash(
PolicyChangeType.AdvisoryUpdated,
"tenant",
"ADV-002", // Different advisory
"CVE-001",
null, null, null);
hash1.Should().NotBe(hash2);
}
[Fact]
public void CreateManualTrigger_IncludesRequestedBy()
{
var now = DateTimeOffset.UtcNow;
var evt = PolicyChangeEventFactory.CreateManualTrigger(
tenantId: "test-tenant",
policyIds: ["policy-1"],
sbomIds: ["sbom-1"],
productKeys: null,
requestedBy: "admin@example.com",
createdAt: now);
evt.ChangeType.Should().Be(PolicyChangeType.ManualTrigger);
evt.Metadata.Should().ContainKey("requestedBy");
evt.Metadata["requestedBy"].Should().Be("admin@example.com");
}
#endregion
#region IncrementalPolicyOrchestrator Tests
[Fact]
public async Task ProcessAsync_ProcessesEvents()
{
var eventSource = new InMemoryPolicyChangeEventSource();
var submitter = new TestSubmitter();
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var orchestrator = new IncrementalPolicyOrchestrator(
eventSource, submitter, idempotencyStore,
timeProvider: timeProvider);
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-001", "CVE-001", ["pkg:npm/test"],
"test", timeProvider.GetUtcNow(), timeProvider.GetUtcNow()));
var result = await orchestrator.ProcessAsync(CancellationToken.None);
result.TotalEventsRead.Should().Be(1);
result.BatchesProcessed.Should().Be(1);
submitter.SubmittedBatches.Should().HaveCount(1);
}
[Fact]
public async Task ProcessAsync_DeduplicatesEvents()
{
var eventSource = new InMemoryPolicyChangeEventSource();
var submitter = new TestSubmitter();
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var orchestrator = new IncrementalPolicyOrchestrator(
eventSource, submitter, idempotencyStore,
timeProvider: timeProvider);
var evt = PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-001", "CVE-001", ["pkg:npm/test"],
"test", timeProvider.GetUtcNow(), timeProvider.GetUtcNow());
// Mark as already seen
await idempotencyStore.MarkSeenAsync(evt.EventId, timeProvider.GetUtcNow(), CancellationToken.None);
eventSource.Enqueue(evt);
var result = await orchestrator.ProcessAsync(CancellationToken.None);
result.TotalEventsRead.Should().Be(1);
result.EventsSkippedDuplicate.Should().Be(1);
result.BatchesProcessed.Should().Be(0);
}
[Fact]
public async Task ProcessAsync_SkipsOldEvents()
{
var options = new IncrementalOrchestratorOptions
{
MaxEventAge = TimeSpan.FromHours(1)
};
var eventSource = new InMemoryPolicyChangeEventSource();
var submitter = new TestSubmitter();
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var orchestrator = new IncrementalPolicyOrchestrator(
eventSource, submitter, idempotencyStore, options,
timeProvider: timeProvider);
// Create an old event
var oldTime = timeProvider.GetUtcNow().AddHours(-2);
var evt = PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-001", "CVE-001", ["pkg:npm/test"],
"test", oldTime, oldTime);
eventSource.Enqueue(evt);
var result = await orchestrator.ProcessAsync(CancellationToken.None);
result.TotalEventsRead.Should().Be(1);
result.EventsSkippedOld.Should().Be(1);
result.BatchesProcessed.Should().Be(0);
}
[Fact]
public async Task ProcessAsync_GroupsByTenant()
{
var eventSource = new InMemoryPolicyChangeEventSource();
var submitter = new TestSubmitter();
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var orchestrator = new IncrementalPolicyOrchestrator(
eventSource, submitter, idempotencyStore,
timeProvider: timeProvider);
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-001", "CVE-001", [], "test",
timeProvider.GetUtcNow(), timeProvider.GetUtcNow()));
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant2", "ADV-002", "CVE-002", [], "test",
timeProvider.GetUtcNow(), timeProvider.GetUtcNow()));
var result = await orchestrator.ProcessAsync(CancellationToken.None);
result.BatchesProcessed.Should().Be(2); // One per tenant
submitter.SubmittedBatches.Select(b => b.TenantId).Should()
.BeEquivalentTo(["tenant1", "tenant2"]);
}
[Fact]
public async Task ProcessAsync_SortsByPriority()
{
var eventSource = new InMemoryPolicyChangeEventSource();
var submitter = new TestSubmitter();
var idempotencyStore = new InMemoryPolicyChangeIdempotencyStore();
var timeProvider = new FakeTimeProvider(DateTimeOffset.UtcNow);
var orchestrator = new IncrementalPolicyOrchestrator(
eventSource, submitter, idempotencyStore,
timeProvider: timeProvider);
// Add normal priority first
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-001", "CVE-001", [], "test",
timeProvider.GetUtcNow(), timeProvider.GetUtcNow(),
priority: PolicyChangePriority.Normal));
// Add emergency priority second
eventSource.Enqueue(PolicyChangeEventFactory.CreateAdvisoryUpdated(
"tenant1", "ADV-002", "CVE-002", [], "test",
timeProvider.GetUtcNow(), timeProvider.GetUtcNow(),
priority: PolicyChangePriority.Emergency));
await orchestrator.ProcessAsync(CancellationToken.None);
// Emergency should be processed first (separate batch due to priority)
submitter.SubmittedBatches.Should().HaveCount(2);
submitter.SubmittedBatches[0].Priority.Should().Be(PolicyChangePriority.Emergency);
}
#endregion
#region RuleHitSamplingOptions Tests
[Fact]
public void Default_HasReasonableSamplingRates()
{
var options = RuleHitSamplingOptions.Default;
options.BaseSamplingRate.Should().BeInRange(0.0, 1.0);
options.VexOverrideSamplingRate.Should().Be(1.0); // Always sample VEX
options.IncidentModeSamplingRate.Should().Be(1.0);
}
[Fact]
public void FullSampling_SamplesEverything()
{
var options = RuleHitSamplingOptions.FullSampling;
options.BaseSamplingRate.Should().Be(1.0);
options.VexOverrideSamplingRate.Should().Be(1.0);
options.HighSeveritySamplingRate.Should().Be(1.0);
}
#endregion
private sealed class TestSubmitter : IPolicyReEvaluationSubmitter
{
public List<PolicyChangeBatch> SubmittedBatches { get; } = [];
public Task<PolicyReEvaluationResult> SubmitAsync(
PolicyChangeBatch batch,
CancellationToken cancellationToken)
{
SubmittedBatches.Add(batch);
return Task.FromResult(new PolicyReEvaluationResult
{
Succeeded = true,
JobIds = [$"job-{batch.BatchId}"],
ProcessingTimeMs = 1
});
}
}
}