Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly.
- Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps.
- Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges.
- Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges.
- Set up project file for the test project with necessary dependencies and configurations.
- Include JSON fixture files for testing purposes.
This commit is contained in:
master
2025-11-04 07:49:39 +02:00
parent f72c5c513a
commit 2eb6852d34
491 changed files with 39445 additions and 3917 deletions

View File

@@ -4,12 +4,13 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Mongo.Repositories;
using StellaOps.Scheduler.Worker.Graph;
using StellaOps.Scheduler.Worker.Graph.Cartographer;
using StellaOps.Scheduler.Worker.Graph.Scheduler;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Mongo.Repositories;
using StellaOps.Scheduler.Worker.Graph;
using StellaOps.Scheduler.Worker.Graph.Cartographer;
using StellaOps.Scheduler.Worker.Graph.Scheduler;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Observability;
using Xunit;
namespace StellaOps.Scheduler.Worker.Tests;

View File

@@ -6,10 +6,11 @@ using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Storage.Mongo.Repositories;
using StellaOps.Scheduler.Worker.Graph;
using StellaOps.Scheduler.Worker.Graph.Cartographer;
using StellaOps.Scheduler.Worker.Graph.Scheduler;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Graph;
using StellaOps.Scheduler.Worker.Graph.Cartographer;
using StellaOps.Scheduler.Worker.Graph.Scheduler;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Observability;
using Xunit;
namespace StellaOps.Scheduler.Worker.Tests;

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Immutable;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
@@ -46,11 +47,12 @@ public sealed class PolicyRunExecutionServiceTests
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
using var metrics = new SchedulerWorkerMetrics();
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.Unchanged(job)
};
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, NullLogger<PolicyRunExecutionService>.Instance);
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.Unchanged(job)
};
var webhook = new RecordingPolicySimulationWebhookClient();
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, webhook, NullLogger<PolicyRunExecutionService>.Instance);
var job = CreateJob(status: PolicyRunJobStatus.Dispatching) with
{
@@ -63,26 +65,29 @@ public sealed class PolicyRunExecutionServiceTests
Assert.Equal(PolicyRunExecutionResultType.Cancelled, result.Type);
Assert.Equal(PolicyRunJobStatus.Cancelled, result.UpdatedJob.Status);
Assert.True(repository.ReplaceCalled);
Assert.Equal("test-dispatch", repository.ExpectedLeaseOwner);
Assert.True(repository.ReplaceCalled);
Assert.Equal("test-dispatch", repository.ExpectedLeaseOwner);
Assert.Single(webhook.Payloads);
Assert.Equal("cancelled", webhook.Payloads[0].Result);
}
[Fact]
public async Task ExecuteAsync_SubmitsJob_OnSuccess()
{
var repository = new RecordingPolicyRunJobRepository();
var client = new StubPolicyRunClient
{
Result = PolicyRunSubmissionResult.Succeeded("run:P-7:2025", DateTimeOffset.Parse("2025-10-28T10:01:00Z"))
};
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
using var metrics = new SchedulerWorkerMetrics();
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.Unchanged(job)
};
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, NullLogger<PolicyRunExecutionService>.Instance);
var client = new StubPolicyRunClient
{
Result = PolicyRunSubmissionResult.Succeeded("run:P-7:2025", DateTimeOffset.Parse("2025-10-28T10:01:00Z"))
};
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
using var metrics = new SchedulerWorkerMetrics();
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.Unchanged(job)
};
var webhook = new RecordingPolicySimulationWebhookClient();
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, webhook, NullLogger<PolicyRunExecutionService>.Instance);
var job = CreateJob(status: PolicyRunJobStatus.Dispatching) with
{
@@ -93,11 +98,12 @@ public sealed class PolicyRunExecutionServiceTests
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(PolicyRunExecutionResultType.Submitted, result.Type);
Assert.Equal(PolicyRunJobStatus.Submitted, result.UpdatedJob.Status);
Assert.Equal("run:P-7:2025", result.UpdatedJob.RunId);
Assert.Equal(job.AttemptCount + 1, result.UpdatedJob.AttemptCount);
Assert.Null(result.UpdatedJob.LastError);
Assert.True(repository.ReplaceCalled);
Assert.Equal(PolicyRunJobStatus.Submitted, result.UpdatedJob.Status);
Assert.Equal("run:P-7:2025", result.UpdatedJob.RunId);
Assert.Equal(job.AttemptCount + 1, result.UpdatedJob.AttemptCount);
Assert.Null(result.UpdatedJob.LastError);
Assert.True(repository.ReplaceCalled);
Assert.Empty(webhook.Payloads);
}
[Fact]
@@ -109,13 +115,14 @@ public sealed class PolicyRunExecutionServiceTests
Result = PolicyRunSubmissionResult.Failed("timeout")
};
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
using var metrics = new SchedulerWorkerMetrics();
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.Unchanged(job)
};
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, NullLogger<PolicyRunExecutionService>.Instance);
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
using var metrics = new SchedulerWorkerMetrics();
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.Unchanged(job)
};
var webhook = new RecordingPolicySimulationWebhookClient();
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, webhook, NullLogger<PolicyRunExecutionService>.Instance);
var job = CreateJob(status: PolicyRunJobStatus.Dispatching) with
{
@@ -127,9 +134,10 @@ public sealed class PolicyRunExecutionServiceTests
Assert.Equal(PolicyRunExecutionResultType.Retrying, result.Type);
Assert.Equal(PolicyRunJobStatus.Pending, result.UpdatedJob.Status);
Assert.Equal(job.AttemptCount + 1, result.UpdatedJob.AttemptCount);
Assert.Equal("timeout", result.UpdatedJob.LastError);
Assert.True(result.UpdatedJob.AvailableAt > job.AvailableAt);
Assert.Equal(job.AttemptCount + 1, result.UpdatedJob.AttemptCount);
Assert.Equal("timeout", result.UpdatedJob.LastError);
Assert.True(result.UpdatedJob.AvailableAt > job.AvailableAt);
Assert.Empty(webhook.Payloads);
}
[Fact]
@@ -144,12 +152,13 @@ public sealed class PolicyRunExecutionServiceTests
optionsValue.Policy.Dispatch.MaxAttempts = 1;
var options = Microsoft.Extensions.Options.Options.Create(optionsValue);
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
using var metrics = new SchedulerWorkerMetrics();
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.Unchanged(job)
};
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, NullLogger<PolicyRunExecutionService>.Instance);
using var metrics = new SchedulerWorkerMetrics();
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.Unchanged(job)
};
var webhook = new RecordingPolicySimulationWebhookClient();
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, webhook, NullLogger<PolicyRunExecutionService>.Instance);
var job = CreateJob(status: PolicyRunJobStatus.Dispatching, attemptCount: 0) with
{
@@ -157,11 +166,13 @@ public sealed class PolicyRunExecutionServiceTests
LeaseExpiresAt = timeProvider.GetUtcNow().AddMinutes(1)
};
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(PolicyRunExecutionResultType.Failed, result.Type);
Assert.Equal(PolicyRunJobStatus.Failed, result.UpdatedJob.Status);
Assert.Equal("bad request", result.UpdatedJob.LastError);
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(PolicyRunExecutionResultType.Failed, result.Type);
Assert.Equal(PolicyRunJobStatus.Failed, result.UpdatedJob.Status);
Assert.Equal("bad request", result.UpdatedJob.LastError);
Assert.Single(webhook.Payloads);
Assert.Equal("failed", webhook.Payloads[0].Result);
}
[Fact]
@@ -172,11 +183,12 @@ public sealed class PolicyRunExecutionServiceTests
var options = Microsoft.Extensions.Options.Options.Create(CloneOptions());
var timeProvider = new TestTimeProvider(DateTimeOffset.Parse("2025-10-28T10:00:00Z"));
using var metrics = new SchedulerWorkerMetrics();
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.NoWork(job, "empty")
};
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, NullLogger<PolicyRunExecutionService>.Instance);
var targeting = new StubPolicyRunTargetingService
{
OnEnsureTargets = job => PolicyRunTargetingResult.NoWork(job, "empty")
};
var webhook = new RecordingPolicySimulationWebhookClient();
var service = new PolicyRunExecutionService(repository, client, options, timeProvider, metrics, targeting, webhook, NullLogger<PolicyRunExecutionService>.Instance);
var job = CreateJob(status: PolicyRunJobStatus.Dispatching, inputs: PolicyRunInputs.Empty) with
{
@@ -186,10 +198,12 @@ public sealed class PolicyRunExecutionServiceTests
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(PolicyRunExecutionResultType.NoOp, result.Type);
Assert.Equal(PolicyRunJobStatus.Completed, result.UpdatedJob.Status);
Assert.True(repository.ReplaceCalled);
Assert.Equal("test-dispatch", repository.ExpectedLeaseOwner);
Assert.Equal(PolicyRunExecutionResultType.NoOp, result.Type);
Assert.Equal(PolicyRunJobStatus.Completed, result.UpdatedJob.Status);
Assert.True(repository.ReplaceCalled);
Assert.Equal("test-dispatch", repository.ExpectedLeaseOwner);
Assert.Single(webhook.Payloads);
Assert.Equal("succeeded", webhook.Payloads[0].Result);
}
private static PolicyRunJob CreateJob(PolicyRunJobStatus status, int attemptCount = 0, PolicyRunInputs? inputs = null)
@@ -253,15 +267,23 @@ public sealed class PolicyRunExecutionServiceTests
IdempotencyHeader = WorkerOptions.Policy.Api.IdempotencyHeader,
RequestTimeout = WorkerOptions.Policy.Api.RequestTimeout
},
Targeting = new SchedulerWorkerOptions.PolicyOptions.TargetingOptions
{
Enabled = WorkerOptions.Policy.Targeting.Enabled,
MaxSboms = WorkerOptions.Policy.Targeting.MaxSboms,
DefaultUsageOnly = WorkerOptions.Policy.Targeting.DefaultUsageOnly
}
}
};
}
Targeting = new SchedulerWorkerOptions.PolicyOptions.TargetingOptions
{
Enabled = WorkerOptions.Policy.Targeting.Enabled,
MaxSboms = WorkerOptions.Policy.Targeting.MaxSboms,
DefaultUsageOnly = WorkerOptions.Policy.Targeting.DefaultUsageOnly
},
Webhook = new SchedulerWorkerOptions.PolicyOptions.WebhookOptions
{
Enabled = WorkerOptions.Policy.Webhook.Enabled,
Endpoint = WorkerOptions.Policy.Webhook.Endpoint,
ApiKeyHeader = WorkerOptions.Policy.Webhook.ApiKeyHeader,
ApiKey = WorkerOptions.Policy.Webhook.ApiKey,
TimeoutSeconds = WorkerOptions.Policy.Webhook.TimeoutSeconds
}
}
};
}
private sealed class StubPolicyRunTargetingService : IPolicyRunTargetingService
{
@@ -271,8 +293,19 @@ public sealed class PolicyRunExecutionServiceTests
=> Task.FromResult(OnEnsureTargets?.Invoke(job) ?? PolicyRunTargetingResult.Unchanged(job));
}
private sealed class RecordingPolicyRunJobRepository : IPolicyRunJobRepository
{
private sealed class RecordingPolicySimulationWebhookClient : IPolicySimulationWebhookClient
{
public List<PolicySimulationWebhookPayload> Payloads { get; } = new();
public Task NotifyAsync(PolicySimulationWebhookPayload payload, CancellationToken cancellationToken)
{
Payloads.Add(payload);
return Task.CompletedTask;
}
}
private sealed class RecordingPolicyRunJobRepository : IPolicyRunJobRepository
{
public bool ReplaceCalled { get; private set; }
public string? ExpectedLeaseOwner { get; private set; }
public PolicyRunJob? LastJob { get; private set; }
@@ -280,17 +313,20 @@ public sealed class PolicyRunExecutionServiceTests
public Task<PolicyRunJob?> GetAsync(string tenantId, string jobId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
=> Task.FromResult<PolicyRunJob?>(null);
public Task<PolicyRunJob?> GetByRunIdAsync(string tenantId, string runId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
=> Task.FromResult<PolicyRunJob?>(null);
public Task InsertAsync(PolicyRunJob job, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
{
LastJob = job;
return Task.CompletedTask;
}
public Task<PolicyRunJob?> LeaseAsync(string leaseOwner, DateTimeOffset now, TimeSpan leaseDuration, int maxAttempts, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
=> Task.FromResult<PolicyRunJob?>(null);
public Task<PolicyRunJob?> GetByRunIdAsync(string tenantId, string runId, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
=> Task.FromResult<PolicyRunJob?>(null);
public Task InsertAsync(PolicyRunJob job, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
{
LastJob = job;
return Task.CompletedTask;
}
public Task<long> CountAsync(string tenantId, PolicyRunMode mode, IReadOnlyCollection<PolicyRunJobStatus> statuses, CancellationToken cancellationToken = default)
=> Task.FromResult(0L);
public Task<PolicyRunJob?> LeaseAsync(string leaseOwner, DateTimeOffset now, TimeSpan leaseDuration, int maxAttempts, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
=> Task.FromResult<PolicyRunJob?>(null);
public Task<bool> ReplaceAsync(PolicyRunJob job, string? expectedLeaseOwner = null, IClientSessionHandle? session = null, CancellationToken cancellationToken = default)
{

View File

@@ -0,0 +1,146 @@
using System;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using StellaOps.Scheduler.Models;
using StellaOps.Scheduler.Worker.Options;
using StellaOps.Scheduler.Worker.Policy;
using Xunit;
namespace StellaOps.Scheduler.Worker.Tests;
public sealed class PolicySimulationWebhookClientTests
{
[Fact]
public async Task NotifyAsync_Disabled_DoesNotInvokeEndpoint()
{
var handler = new RecordingHandler();
using var httpClient = new HttpClient(handler);
var options = CreateOptions();
var client = new HttpPolicySimulationWebhookClient(httpClient, options, NullLogger<HttpPolicySimulationWebhookClient>.Instance);
var payload = PolicySimulationWebhookPayloadFactory.Create(CreateStatus(), DateTimeOffset.UtcNow);
await client.NotifyAsync(payload, CancellationToken.None);
Assert.False(handler.WasInvoked);
}
[Fact]
public async Task NotifyAsync_SendsPayload_WhenEnabled()
{
var handler = new RecordingHandler(new HttpResponseMessage(HttpStatusCode.Accepted));
using var httpClient = new HttpClient(handler);
var options = CreateOptions(o =>
{
o.Policy.Webhook.Enabled = true;
o.Policy.Webhook.Endpoint = "https://example.org/webhooks/policy";
o.Policy.Webhook.ApiKeyHeader = "X-Test-Key";
o.Policy.Webhook.ApiKey = "secret";
o.Policy.Webhook.TimeoutSeconds = 5;
});
var client = new HttpPolicySimulationWebhookClient(httpClient, options, NullLogger<HttpPolicySimulationWebhookClient>.Instance);
var observedAt = DateTimeOffset.UtcNow;
var payload = PolicySimulationWebhookPayloadFactory.Create(CreateStatus(), observedAt);
await client.NotifyAsync(payload, CancellationToken.None);
Assert.True(handler.WasInvoked);
Assert.NotNull(handler.LastRequest);
Assert.Equal("https://example.org/webhooks/policy", handler.LastRequest!.RequestUri!.ToString());
Assert.True(handler.LastRequest.Headers.Contains("X-Test-Key"));
Assert.True(handler.LastRequest.Headers.Contains("X-StellaOps-Run-Id"));
Assert.Equal("secret", handler.LastRequest.Headers.GetValues("X-Test-Key").Single());
}
private static PolicyRunStatus CreateStatus()
{
var now = DateTimeOffset.UtcNow;
var job = new PolicyRunJob(
SchemaVersion: SchedulerSchemaVersions.PolicyRunJob,
Id: "job",
TenantId: "tenant",
PolicyId: "policy",
PolicyVersion: 1,
Mode: PolicyRunMode.Simulate,
Priority: PolicyRunPriority.Normal,
PriorityRank: 0,
RunId: "run:policy:123",
RequestedBy: "tester",
CorrelationId: "corr",
Metadata: null,
Inputs: PolicyRunInputs.Empty,
QueuedAt: now,
Status: PolicyRunJobStatus.Completed,
AttemptCount: 1,
LastAttemptAt: now,
LastError: null,
CreatedAt: now,
UpdatedAt: now,
AvailableAt: now,
SubmittedAt: now,
CompletedAt: now,
LeaseOwner: null,
LeaseExpiresAt: null,
CancellationRequested: false,
CancellationRequestedAt: null,
CancellationReason: null,
CancelledAt: null);
return PolicyRunStatusFactory.Create(job, now);
}
private static IOptionsMonitor<SchedulerWorkerOptions> CreateOptions(Action<SchedulerWorkerOptions>? configure = null)
{
var value = new SchedulerWorkerOptions();
configure?.Invoke(value);
return new StaticOptionsMonitor<SchedulerWorkerOptions>(value);
}
private sealed class RecordingHandler : HttpMessageHandler
{
private readonly HttpResponseMessage _response;
public RecordingHandler(HttpResponseMessage? response = null)
{
_response = response ?? new HttpResponseMessage(HttpStatusCode.OK);
}
public bool WasInvoked { get; private set; }
public HttpRequestMessage? LastRequest { get; private set; }
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
WasInvoked = true;
LastRequest = request;
return Task.FromResult(_response);
}
}
private sealed class StaticOptionsMonitor<T> : IOptionsMonitor<T>
{
private sealed class NoopDisposable : IDisposable
{
public static readonly IDisposable Instance = new NoopDisposable();
public void Dispose()
{
}
}
public StaticOptionsMonitor(T value)
{
CurrentValue = value;
}
public T CurrentValue { get; }
public T Get(string? name) => CurrentValue;
public IDisposable OnChange(Action<T, string?> listener) => NoopDisposable.Instance;
}
}