Restructure solution layout by module
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
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 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user