audit notes work completed, test fixes work (95% done), new sprints, new data sources setup and configuration
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
using System.Text.Json;
|
||||
using FluentAssertions;
|
||||
using Microsoft.Extensions.Logging.Abstractions;
|
||||
using Microsoft.Extensions.Time.Testing;
|
||||
using StellaOps.Determinism;
|
||||
using StellaOps.Scanner.Sources.Configuration;
|
||||
using StellaOps.Scanner.Sources.Contracts;
|
||||
using StellaOps.Scanner.Sources.Domain;
|
||||
using StellaOps.Scanner.Sources.Handlers;
|
||||
using StellaOps.Scanner.Sources.Persistence;
|
||||
using StellaOps.Scanner.Sources.Triggers;
|
||||
using StellaOps.TestKit;
|
||||
using Xunit;
|
||||
|
||||
namespace StellaOps.Scanner.Sources.Tests.Triggers;
|
||||
|
||||
public sealed class SourceTriggerDispatcherTests
|
||||
{
|
||||
private static readonly FakeTimeProvider TimeProvider = new(
|
||||
new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
|
||||
private static readonly JsonDocument MinimalConfig = JsonDocument.Parse("{}");
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task DispatchAsync_QueuesTargetsAndCompletesRun()
|
||||
{
|
||||
var guidProvider = new SequentialGuidProvider();
|
||||
var source = CreateSource(guidProvider);
|
||||
source.Activate("tester", TimeProvider);
|
||||
|
||||
var sourceRepo = new InMemorySourceRepository();
|
||||
sourceRepo.Add(source);
|
||||
|
||||
var runRepo = new InMemoryRunRepository();
|
||||
var handler = new InMemoryHandler(new[]
|
||||
{
|
||||
ScanTarget.Image("registry.example.com/app:1.0.0"),
|
||||
ScanTarget.Image("registry.example.com/app:1.1.0")
|
||||
});
|
||||
|
||||
var queue = new InMemoryScanJobQueue(guidProvider);
|
||||
|
||||
var dispatcher = new SourceTriggerDispatcher(
|
||||
sourceRepo,
|
||||
runRepo,
|
||||
new[] { handler },
|
||||
queue,
|
||||
NullLogger<SourceTriggerDispatcher>.Instance,
|
||||
TimeProvider,
|
||||
guidProvider);
|
||||
|
||||
var result = await dispatcher.DispatchAsync(
|
||||
source.SourceId,
|
||||
SbomSourceRunTrigger.Manual,
|
||||
"manual",
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
result.Success.Should().BeTrue();
|
||||
result.JobsQueued.Should().Be(2);
|
||||
result.Run.ItemsDiscovered.Should().Be(2);
|
||||
result.Run.ItemsSucceeded.Should().Be(2);
|
||||
result.Run.Status.Should().Be(SbomSourceRunStatus.Succeeded);
|
||||
queue.Requests.Should().HaveCount(2);
|
||||
runRepo.Runs.Should().ContainKey(result.Run.RunId);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task ProcessScheduledSourcesAsync_DispatchesDueSources()
|
||||
{
|
||||
var guidProvider = new SequentialGuidProvider();
|
||||
var source = CreateSource(guidProvider);
|
||||
source.Activate("tester", TimeProvider);
|
||||
|
||||
var sourceRepo = new InMemorySourceRepository
|
||||
{
|
||||
DueSources = new List<SbomSource> { source }
|
||||
};
|
||||
sourceRepo.Add(source);
|
||||
|
||||
var runRepo = new InMemoryRunRepository();
|
||||
var handler = new InMemoryHandler(new[] { ScanTarget.Image("registry.example.com/app:1.0.0") });
|
||||
var queue = new InMemoryScanJobQueue(guidProvider);
|
||||
|
||||
var dispatcher = new SourceTriggerDispatcher(
|
||||
sourceRepo,
|
||||
runRepo,
|
||||
new[] { handler },
|
||||
queue,
|
||||
NullLogger<SourceTriggerDispatcher>.Instance,
|
||||
TimeProvider,
|
||||
guidProvider);
|
||||
|
||||
var processed = await dispatcher.ProcessScheduledSourcesAsync(
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
processed.Should().Be(1);
|
||||
runRepo.Runs.Should().HaveCount(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Category", TestCategories.Unit)]
|
||||
public async Task DispatchAsync_DisabledSource_ReturnsFailedRun()
|
||||
{
|
||||
var guidProvider = new SequentialGuidProvider();
|
||||
var source = CreateSource(guidProvider);
|
||||
source.Disable("tester", TimeProvider);
|
||||
|
||||
var sourceRepo = new InMemorySourceRepository();
|
||||
sourceRepo.Add(source);
|
||||
|
||||
var runRepo = new InMemoryRunRepository();
|
||||
var handler = new InMemoryHandler(Array.Empty<ScanTarget>());
|
||||
var queue = new InMemoryScanJobQueue(guidProvider);
|
||||
|
||||
var dispatcher = new SourceTriggerDispatcher(
|
||||
sourceRepo,
|
||||
runRepo,
|
||||
new[] { handler },
|
||||
queue,
|
||||
NullLogger<SourceTriggerDispatcher>.Instance,
|
||||
TimeProvider,
|
||||
guidProvider);
|
||||
|
||||
var result = await dispatcher.DispatchAsync(
|
||||
source.SourceId,
|
||||
SbomSourceRunTrigger.Manual,
|
||||
"manual",
|
||||
TestContext.Current.CancellationToken);
|
||||
|
||||
result.Success.Should().BeFalse();
|
||||
result.Error.Should().Contain("disabled");
|
||||
result.Run.Status.Should().Be(SbomSourceRunStatus.Failed);
|
||||
}
|
||||
|
||||
private static SbomSource CreateSource(IGuidProvider guidProvider)
|
||||
{
|
||||
return SbomSource.Create(
|
||||
tenantId: "tenant-1",
|
||||
name: "source-1",
|
||||
sourceType: SbomSourceType.Docker,
|
||||
configuration: MinimalConfig,
|
||||
createdBy: "tester",
|
||||
timeProvider: TimeProvider,
|
||||
guidProvider: guidProvider);
|
||||
}
|
||||
|
||||
private sealed class InMemorySourceRepository : ISbomSourceRepository
|
||||
{
|
||||
public Dictionary<Guid, SbomSource> Sources { get; } = new();
|
||||
public IReadOnlyList<SbomSource> DueSources { get; set; } = Array.Empty<SbomSource>();
|
||||
|
||||
public void Add(SbomSource source) => Sources[source.SourceId] = source;
|
||||
|
||||
public Task<SbomSource?> GetByIdAsync(string tenantId, Guid sourceId, CancellationToken ct = default)
|
||||
=> Task.FromResult(Sources.TryGetValue(sourceId, out var source) ? source : null);
|
||||
|
||||
public Task<SbomSource?> GetByIdAnyTenantAsync(Guid sourceId, CancellationToken ct = default)
|
||||
=> Task.FromResult(Sources.TryGetValue(sourceId, out var source) ? source : null);
|
||||
|
||||
public Task<SbomSource?> GetByNameAsync(string tenantId, string name, CancellationToken ct = default)
|
||||
=> Task.FromResult(Sources.Values.FirstOrDefault(s => s.TenantId == tenantId && s.Name == name));
|
||||
|
||||
public Task<PagedResponse<SbomSource>> ListAsync(
|
||||
string tenantId,
|
||||
ListSourcesRequest request,
|
||||
CancellationToken ct = default)
|
||||
=> throw new NotSupportedException("ListAsync is not used in these tests.");
|
||||
|
||||
public Task<IReadOnlyList<SbomSource>> GetDueScheduledSourcesAsync(
|
||||
DateTimeOffset asOf,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<SbomSource>>(DueSources.Take(limit).ToList());
|
||||
|
||||
public Task CreateAsync(SbomSource source, CancellationToken ct = default)
|
||||
{
|
||||
Sources[source.SourceId] = source;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UpdateAsync(SbomSource source, CancellationToken ct = default)
|
||||
{
|
||||
Sources[source.SourceId] = source;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task DeleteAsync(string tenantId, Guid sourceId, CancellationToken ct = default)
|
||||
{
|
||||
Sources.Remove(sourceId);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<bool> NameExistsAsync(
|
||||
string tenantId,
|
||||
string name,
|
||||
Guid? excludeSourceId = null,
|
||||
CancellationToken ct = default)
|
||||
=> Task.FromResult(Sources.Values.Any(s =>
|
||||
s.TenantId == tenantId
|
||||
&& s.Name == name
|
||||
&& s.SourceId != excludeSourceId));
|
||||
|
||||
public Task<IReadOnlyList<SbomSource>> SearchByNameAsync(string name, CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<SbomSource>>(Sources.Values
|
||||
.Where(s => s.Name.Contains(name, StringComparison.OrdinalIgnoreCase))
|
||||
.ToList());
|
||||
|
||||
public Task<IReadOnlyList<SbomSource>> GetDueForScheduledRunAsync(CancellationToken ct = default)
|
||||
=> Task.FromResult(DueSources);
|
||||
}
|
||||
|
||||
private sealed class InMemoryRunRepository : ISbomSourceRunRepository
|
||||
{
|
||||
public Dictionary<Guid, SbomSourceRun> Runs { get; } = new();
|
||||
|
||||
public Task<SbomSourceRun?> GetByIdAsync(Guid runId, CancellationToken ct = default)
|
||||
=> Task.FromResult(Runs.TryGetValue(runId, out var run) ? run : null);
|
||||
|
||||
public Task<PagedResponse<SbomSourceRun>> ListForSourceAsync(
|
||||
Guid sourceId,
|
||||
ListSourceRunsRequest request,
|
||||
CancellationToken ct = default)
|
||||
=> throw new NotSupportedException("ListForSourceAsync is not used in these tests.");
|
||||
|
||||
public Task CreateAsync(SbomSourceRun run, CancellationToken ct = default)
|
||||
{
|
||||
Runs[run.RunId] = run;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task UpdateAsync(SbomSourceRun run, CancellationToken ct = default)
|
||||
{
|
||||
Runs[run.RunId] = run;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task<IReadOnlyList<SbomSourceRun>> GetStaleRunsAsync(
|
||||
TimeSpan olderThan,
|
||||
int limit = 100,
|
||||
CancellationToken ct = default)
|
||||
=> Task.FromResult<IReadOnlyList<SbomSourceRun>>(Array.Empty<SbomSourceRun>());
|
||||
|
||||
public Task<SourceRunStats> GetStatsAsync(Guid sourceId, CancellationToken ct = default)
|
||||
=> Task.FromResult(new SourceRunStats());
|
||||
}
|
||||
|
||||
private sealed class InMemoryScanJobQueue : IScanJobQueue
|
||||
{
|
||||
private readonly IGuidProvider _guidProvider;
|
||||
public List<ScanJobRequest> Requests { get; } = new();
|
||||
|
||||
public InMemoryScanJobQueue(IGuidProvider guidProvider)
|
||||
{
|
||||
_guidProvider = guidProvider;
|
||||
}
|
||||
|
||||
public Task<Guid> EnqueueAsync(ScanJobRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Requests.Add(request);
|
||||
return Task.FromResult(_guidProvider.NewGuid());
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class InMemoryHandler : ISourceTypeHandler
|
||||
{
|
||||
private readonly IReadOnlyList<ScanTarget> _targets;
|
||||
|
||||
public InMemoryHandler(IReadOnlyList<ScanTarget> targets)
|
||||
{
|
||||
_targets = targets;
|
||||
}
|
||||
|
||||
public SbomSourceType SourceType => SbomSourceType.Docker;
|
||||
|
||||
public Task<IReadOnlyList<ScanTarget>> DiscoverTargetsAsync(
|
||||
SbomSource source,
|
||||
TriggerContext context,
|
||||
CancellationToken ct = default)
|
||||
=> Task.FromResult(_targets);
|
||||
|
||||
public ConfigValidationResult ValidateConfiguration(JsonDocument configuration)
|
||||
=> ConfigValidationResult.Success();
|
||||
|
||||
public Task<ConnectionTestResult> TestConnectionAsync(SbomSource source, CancellationToken ct = default)
|
||||
=> Task.FromResult(ConnectionTestResult.Succeeded(TimeProvider));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user