This commit is contained in:
StellaOps Bot
2026-01-07 21:30:44 +02:00
1359 changed files with 61692 additions and 11378 deletions

View File

@@ -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);
}