Merge branch 'main' of https://git.stella-ops.org/stella-ops.org/git.stella-ops.org
This commit is contained in:
@@ -5,11 +5,13 @@
|
||||
// Description: Integration tests for per-layer SBOM and composition recipe endpoints.
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Immutable;
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.DependencyInjection.Extensions;
|
||||
using StellaOps.Scanner.Emit.Composition;
|
||||
using StellaOps.Scanner.WebService.Contracts;
|
||||
using StellaOps.Scanner.WebService.Domain;
|
||||
@@ -313,19 +315,9 @@ public sealed class LayerSbomEndpointsTests
|
||||
[Fact]
|
||||
public async Task VerifyCompositionRecipe_WhenValid_ReturnsSuccess()
|
||||
{
|
||||
const string imageDigest = "sha256:8888888888888888888888888888888888888888888888888888888888888888";
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var scanId = await SubmitScanAsync(client, imageDigest);
|
||||
mockService.AddScan(scanId, imageDigest, CreateTestLayers(2));
|
||||
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
|
||||
mockService.SetVerificationResult(scanId, new CompositionRecipeVerificationResult
|
||||
{
|
||||
Valid = true,
|
||||
@@ -334,6 +326,14 @@ public sealed class LayerSbomEndpointsTests
|
||||
Errors = ImmutableArray<string>.Empty,
|
||||
});
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<ILayerSbomService>();
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
@@ -348,19 +348,9 @@ public sealed class LayerSbomEndpointsTests
|
||||
[Fact]
|
||||
public async Task VerifyCompositionRecipe_WhenInvalid_ReturnsErrors()
|
||||
{
|
||||
const string imageDigest = "sha256:9999999999999999999999999999999999999999999999999999999999999999";
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
var scanId = "scan-" + Guid.NewGuid().ToString("N");
|
||||
var mockService = new InMemoryLayerSbomService();
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var scanId = await SubmitScanAsync(client, imageDigest);
|
||||
mockService.AddScan(scanId, imageDigest, CreateTestLayers(2));
|
||||
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
|
||||
mockService.SetVerificationResult(scanId, new CompositionRecipeVerificationResult
|
||||
{
|
||||
Valid = false,
|
||||
@@ -369,6 +359,14 @@ public sealed class LayerSbomEndpointsTests
|
||||
Errors = ImmutableArray.Create("Merkle root mismatch: expected sha256:abc, got sha256:def"),
|
||||
});
|
||||
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<ILayerSbomService>();
|
||||
services.AddSingleton<ILayerSbomService>(mockService);
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
|
||||
var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null);
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
@@ -384,10 +382,10 @@ public sealed class LayerSbomEndpointsTests
|
||||
[Fact]
|
||||
public async Task VerifyCompositionRecipe_WhenScanNotFound_Returns404()
|
||||
{
|
||||
using var secrets = new TestSurfaceSecretsScope();
|
||||
using var factory = new ScannerApplicationFactory()
|
||||
.WithOverrides(configureServices: services =>
|
||||
{
|
||||
services.RemoveAll<ILayerSbomService>();
|
||||
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
|
||||
});
|
||||
using var client = factory.CreateClient();
|
||||
@@ -588,3 +586,94 @@ internal sealed class InMemoryLayerSbomService : ILayerSbomService
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Stub IScanCoordinator that supports pre-populating scans with specific IDs for testing.
|
||||
/// </summary>
|
||||
internal sealed class StubScanCoordinator : IScanCoordinator
|
||||
{
|
||||
private readonly ConcurrentDictionary<string, ScanSnapshot> _scans = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly TimeProvider _timeProvider;
|
||||
|
||||
/// <summary>
|
||||
/// Creates a StubScanCoordinator with default TimeProvider.System.
|
||||
/// </summary>
|
||||
public StubScanCoordinator()
|
||||
: this(TimeProvider.System)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a StubScanCoordinator for DI registration with injected dependencies.
|
||||
/// </summary>
|
||||
public StubScanCoordinator(TimeProvider timeProvider, IScanProgressPublisher progressPublisher)
|
||||
: this(timeProvider)
|
||||
{
|
||||
}
|
||||
|
||||
private StubScanCoordinator(TimeProvider timeProvider)
|
||||
{
|
||||
_timeProvider = timeProvider;
|
||||
}
|
||||
|
||||
public void AddScan(string scanId, string imageDigest)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var snapshot = new ScanSnapshot(
|
||||
new ScanId(scanId),
|
||||
new ScanTarget("test-image", imageDigest),
|
||||
ScanStatus.Succeeded,
|
||||
now.AddMinutes(-5),
|
||||
now,
|
||||
null, null, null);
|
||||
_scans[scanId] = snapshot;
|
||||
}
|
||||
|
||||
public ValueTask<ScanSubmissionResult> SubmitAsync(ScanSubmission submission, CancellationToken cancellationToken)
|
||||
{
|
||||
var now = _timeProvider.GetUtcNow();
|
||||
var scanId = new ScanId(Guid.NewGuid().ToString("N"));
|
||||
var snapshot = new ScanSnapshot(
|
||||
scanId,
|
||||
submission.Target,
|
||||
ScanStatus.Pending,
|
||||
now,
|
||||
now,
|
||||
null, null, null);
|
||||
_scans[scanId.Value] = snapshot;
|
||||
return ValueTask.FromResult(new ScanSubmissionResult(snapshot, true));
|
||||
}
|
||||
|
||||
public ValueTask<ScanSnapshot?> GetAsync(ScanId scanId, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_scans.TryGetValue(scanId.Value, out var snapshot))
|
||||
{
|
||||
return ValueTask.FromResult<ScanSnapshot?>(snapshot);
|
||||
}
|
||||
return ValueTask.FromResult<ScanSnapshot?>(null);
|
||||
}
|
||||
|
||||
public ValueTask<ScanSnapshot?> TryFindByTargetAsync(string? reference, string? digest, CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (var snapshot in _scans.Values)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(digest) &&
|
||||
string.Equals(snapshot.Target.Digest, digest, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ValueTask.FromResult<ScanSnapshot?>(snapshot);
|
||||
}
|
||||
if (!string.IsNullOrWhiteSpace(reference) &&
|
||||
string.Equals(snapshot.Target.Reference, reference, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return ValueTask.FromResult<ScanSnapshot?>(snapshot);
|
||||
}
|
||||
}
|
||||
return ValueTask.FromResult<ScanSnapshot?>(null);
|
||||
}
|
||||
|
||||
public ValueTask<bool> AttachReplayAsync(ScanId scanId, ReplayArtifacts replay, CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult(false);
|
||||
|
||||
public ValueTask<bool> AttachEntropyAsync(ScanId scanId, EntropySnapshot entropy, CancellationToken cancellationToken)
|
||||
=> ValueTask.FromResult(false);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user