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.
239 lines
10 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|