Files
git.stella-ops.org/src/Scheduler/__Tests/StellaOps.Scheduler.Worker.Tests/GraphOverlayExecutionServiceTests.cs
master 2eb6852d34
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled
Add unit tests for SBOM ingestion and transformation
- 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.
2025-11-04 07:49:39 +02:00

239 lines
10 KiB
C#

using System;
using System.Collections.Generic;
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.Worker.Observability;
using Xunit;
namespace StellaOps.Scheduler.Worker.Tests;
public sealed class GraphOverlayExecutionServiceTests
{
[Fact]
public async Task ExecuteAsync_Skips_WhenGraphDisabled()
{
var repository = new RecordingGraphJobRepository();
var cartographer = new StubCartographerOverlayClient();
var completion = new RecordingCompletionClient();
using var metrics = new SchedulerWorkerMetrics();
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
{
Graph = new SchedulerWorkerOptions.GraphOptions
{
Enabled = false
}
});
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
var job = CreateOverlayJob();
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(GraphOverlayExecutionResultType.Skipped, result.Type);
Assert.Equal("graph_processing_disabled", result.Reason);
Assert.Empty(completion.Notifications);
Assert.Equal(0, cartographer.CallCount);
}
[Fact]
public async Task ExecuteAsync_CompletesJob_OnSuccess()
{
var repository = new RecordingGraphJobRepository();
var cartographer = new StubCartographerOverlayClient
{
Result = new CartographerOverlayResult(
GraphJobStatus.Completed,
GraphSnapshotId: "graph_snap_2",
ResultUri: "oras://graph/overlay",
Error: null)
};
var completion = new RecordingCompletionClient();
using var metrics = new SchedulerWorkerMetrics();
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
{
Graph = new SchedulerWorkerOptions.GraphOptions
{
Enabled = true,
MaxAttempts = 2,
RetryBackoff = TimeSpan.FromMilliseconds(5)
}
});
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
var job = CreateOverlayJob();
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(GraphOverlayExecutionResultType.Completed, result.Type);
Assert.Single(completion.Notifications);
var notification = completion.Notifications[0];
Assert.Equal("Overlay", notification.JobType);
Assert.Equal(GraphJobStatus.Completed, notification.Status);
Assert.Equal("oras://graph/overlay", notification.ResultUri);
Assert.Equal("graph_snap_2", notification.GraphSnapshotId);
}
[Fact]
public async Task ExecuteAsync_Fails_AfterRetries()
{
var repository = new RecordingGraphJobRepository();
var cartographer = new StubCartographerOverlayClient
{
ExceptionToThrow = new InvalidOperationException("overlay failed")
};
var completion = new RecordingCompletionClient();
using var metrics = new SchedulerWorkerMetrics();
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
{
Graph = new SchedulerWorkerOptions.GraphOptions
{
Enabled = true,
MaxAttempts = 2,
RetryBackoff = TimeSpan.FromMilliseconds(1)
}
});
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
var job = CreateOverlayJob();
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(GraphOverlayExecutionResultType.Failed, result.Type);
Assert.Single(completion.Notifications);
Assert.Equal(GraphJobStatus.Failed, completion.Notifications[0].Status);
Assert.Equal("overlay failed", completion.Notifications[0].Error);
}
[Fact]
public async Task ExecuteAsync_Skips_WhenConcurrencyConflict()
{
var repository = new RecordingGraphJobRepository
{
ShouldReplaceSucceed = false
};
var cartographer = new StubCartographerOverlayClient();
var completion = new RecordingCompletionClient();
using var metrics = new SchedulerWorkerMetrics();
var options = Microsoft.Extensions.Options.Options.Create(new SchedulerWorkerOptions
{
Graph = new SchedulerWorkerOptions.GraphOptions
{
Enabled = true
}
});
var service = new GraphOverlayExecutionService(repository, cartographer, completion, options, metrics, TimeProvider.System, NullLogger<GraphOverlayExecutionService>.Instance);
var job = CreateOverlayJob();
var result = await service.ExecuteAsync(job, CancellationToken.None);
Assert.Equal(GraphOverlayExecutionResultType.Skipped, result.Type);
Assert.Equal("concurrency_conflict", result.Reason);
Assert.Empty(completion.Notifications);
Assert.Equal(0, cartographer.CallCount);
}
private static GraphOverlayJob CreateOverlayJob() => new(
id: "goj_1",
tenantId: "tenant-alpha",
graphSnapshotId: "snap-1",
overlayKind: GraphOverlayKind.Policy,
overlayKey: "policy@1",
status: GraphJobStatus.Pending,
trigger: GraphOverlayJobTrigger.Policy,
createdAt: DateTimeOffset.UtcNow,
subjects: Array.Empty<string>(),
attempts: 0,
metadata: Array.Empty<KeyValuePair<string, string>>());
private sealed class RecordingGraphJobRepository : IGraphJobRepository
{
public bool ShouldReplaceSucceed { get; set; } = true;
public int RunningReplacements { get; private set; }
public Task<bool> TryReplaceOverlayAsync(GraphOverlayJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
{
if (!ShouldReplaceSucceed)
{
return Task.FromResult(false);
}
RunningReplacements++;
return Task.FromResult(true);
}
public Task<bool> TryReplaceAsync(GraphBuildJob job, GraphJobStatus expectedStatus, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<GraphBuildJob> ReplaceAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<GraphOverlayJob> ReplaceAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task InsertAsync(GraphBuildJob job, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task InsertAsync(GraphOverlayJob job, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<GraphBuildJob?> GetBuildJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<GraphOverlayJob?> GetOverlayJobAsync(string tenantId, string jobId, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<IReadOnlyList<GraphBuildJob>> ListBuildJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<IReadOnlyList<GraphOverlayJob>> ListOverlayJobsAsync(GraphJobStatus? status, int limit, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
public Task<IReadOnlyCollection<GraphOverlayJob>> ListOverlayJobsAsync(string tenantId, CancellationToken cancellationToken = default)
=> throw new NotImplementedException();
}
private sealed class StubCartographerOverlayClient : ICartographerOverlayClient
{
public CartographerOverlayResult Result { get; set; } = new(GraphJobStatus.Completed, null, null, null);
public Exception? ExceptionToThrow { get; set; }
public int CallCount { get; private set; }
public Task<CartographerOverlayResult> StartOverlayAsync(GraphOverlayJob job, CancellationToken cancellationToken)
{
CallCount++;
if (ExceptionToThrow is not null)
{
throw ExceptionToThrow;
}
return Task.FromResult(Result);
}
}
private sealed class RecordingCompletionClient : IGraphJobCompletionClient
{
public List<GraphJobCompletionRequestDto> Notifications { get; } = new();
public Task NotifyAsync(GraphJobCompletionRequestDto request, CancellationToken cancellationToken)
{
Notifications.Add(request);
return Task.CompletedTask;
}
}
}