Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/LayerSbomEndpointsTests.cs
2026-01-17 21:32:08 +02:00

740 lines
28 KiB
C#

// -----------------------------------------------------------------------------
// LayerSbomEndpointsTests.cs
// Sprint: SPRINT_20260106_003_001_SCANNER_perlayer_sbom_api
// Task: T016 - Integration tests for layer SBOM API
// 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;
using StellaOps.Scanner.WebService.Services;
using StellaOps.TestKit;
namespace StellaOps.Scanner.WebService.Tests;
[Trait("Category", TestCategories.Integration)]
public sealed class LayerSbomEndpointsTests
{
private const string BasePath = "/api/v1/scans";
#region List Layers Tests
[Fact]
public async Task ListLayers_WhenScanExists_ReturnsLayers()
{
const string imageDigest = "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
// Submit scan via HTTP POST to get scan ID
var scanId = await SubmitScanAsync(client, imageDigest);
// Set up the mock layer service with the generated scan ID
mockService.AddScan(scanId, imageDigest, CreateTestLayers(3));
var response = await client.GetAsync($"{BasePath}/{scanId}/layers");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<LayerListResponseDto>();
Assert.NotNull(result);
Assert.Equal(scanId, result!.ScanId);
Assert.Equal(imageDigest, result.ImageDigest);
Assert.Equal(3, result.Layers.Count);
Assert.All(result.Layers, l => Assert.True(l.HasSbom));
}
[Fact]
public async Task ListLayers_WhenScanNotFound_Returns404()
{
using var secrets = new TestSurfaceSecretsScope();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/scan-not-found/layers");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task ListLayers_LayersOrderedByOrder()
{
const string imageDigest = "sha256:1111111111111111111111111111111111111111111111111111111111111111";
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
var layers = new[]
{
CreateLayerSummary("sha256:layer2", 2, 15),
CreateLayerSummary("sha256:layer0", 0, 42),
CreateLayerSummary("sha256:layer1", 1, 8),
};
mockService.AddScan(scanId, imageDigest, layers);
var response = await client.GetAsync($"{BasePath}/{scanId}/layers");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<LayerListResponseDto>();
Assert.NotNull(result);
// Verify layer order is as stored (service already orders by Order)
Assert.Equal(0, result!.Layers[0].Order);
Assert.Equal(1, result.Layers[1].Order);
Assert.Equal(2, result.Layers[2].Order);
}
#endregion
#region Get Layer SBOM Tests
[Fact]
public async Task GetLayerSbom_DefaultFormat_ReturnsCycloneDx()
{
const string imageDigest = "sha256:2222222222222222222222222222222222222222222222222222222222222222";
const string layerDigest = "sha256:layer123";
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
mockService.AddScan(scanId, imageDigest, CreateTestLayers(1, layerDigest));
mockService.AddLayerSbom(scanId, layerDigest, "cdx", CreateTestSbomBytes("cyclonedx"));
mockService.AddLayerSbom(scanId, layerDigest, "spdx", CreateTestSbomBytes("spdx"));
var response = await client.GetAsync($"{BasePath}/{scanId}/layers/{layerDigest}/sbom");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains("cyclonedx", response.Content.Headers.ContentType?.ToString());
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("cyclonedx", content);
}
[Fact]
public async Task GetLayerSbom_SpdxFormat_ReturnsSpdx()
{
const string imageDigest = "sha256:3333333333333333333333333333333333333333333333333333333333333333";
const string layerDigest = "sha256:layer123";
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
mockService.AddScan(scanId, imageDigest, CreateTestLayers(1, layerDigest));
mockService.AddLayerSbom(scanId, layerDigest, "cdx", CreateTestSbomBytes("cyclonedx"));
mockService.AddLayerSbom(scanId, layerDigest, "spdx", CreateTestSbomBytes("spdx"));
var response = await client.GetAsync($"{BasePath}/{scanId}/layers/{layerDigest}/sbom?format=spdx");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains("spdx", response.Content.Headers.ContentType?.ToString());
var content = await response.Content.ReadAsStringAsync();
Assert.Contains("spdx", content);
}
[Fact]
public async Task GetLayerSbom_SetsImmutableCacheHeaders()
{
const string imageDigest = "sha256:4444444444444444444444444444444444444444444444444444444444444444";
const string layerDigest = "sha256:layer123";
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
mockService.AddScan(scanId, imageDigest, CreateTestLayers(1, layerDigest));
mockService.AddLayerSbom(scanId, layerDigest, "cdx", CreateTestSbomBytes("cyclonedx"));
var response = await client.GetAsync($"{BasePath}/{scanId}/layers/{layerDigest}/sbom");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.NotNull(response.Headers.ETag);
Assert.Contains("immutable", response.Headers.CacheControl?.ToString());
Assert.True(response.Headers.Contains("X-StellaOps-Layer-Digest"));
Assert.True(response.Headers.Contains("X-StellaOps-Format"));
}
[Fact]
public async Task GetLayerSbom_WhenScanNotFound_Returns404()
{
using var secrets = new TestSurfaceSecretsScope();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/scan-not-found/layers/sha256:layer123/sbom");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetLayerSbom_WhenLayerNotFound_Returns404()
{
const string imageDigest = "sha256:5555555555555555555555555555555555555555555555555555555555555555";
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
mockService.AddScan(scanId, imageDigest, CreateTestLayers(1));
var response = await client.GetAsync($"{BasePath}/{scanId}/layers/sha256:nonexistent/sbom");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
#endregion
#region Composition Recipe Tests
[Fact]
public async Task GetCompositionRecipe_WhenExists_ReturnsRecipe()
{
const string imageDigest = "sha256:6666666666666666666666666666666666666666666666666666666666666666";
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
mockService.AddScan(scanId, imageDigest, CreateTestLayers(2));
mockService.AddCompositionRecipe(scanId, CreateTestRecipe(scanId, imageDigest, 2));
var response = await client.GetAsync($"{BasePath}/{scanId}/composition-recipe");
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<CompositionRecipeResponseDto>();
Assert.NotNull(result);
Assert.Equal(scanId, result!.ScanId);
Assert.Equal(imageDigest, result.ImageDigest);
Assert.NotNull(result.Recipe);
Assert.Equal(2, result.Recipe.Layers.Count);
Assert.NotNull(result.Recipe.MerkleRoot);
}
[Fact]
public async Task GetCompositionRecipe_WhenScanNotFound_Returns404()
{
using var secrets = new TestSurfaceSecretsScope();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.GetAsync($"{BasePath}/scan-not-found/composition-recipe");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
[Fact]
public async Task GetCompositionRecipe_WhenRecipeNotAvailable_Returns404()
{
const string imageDigest = "sha256:7777777777777777777777777777777777777777777777777777777777777777";
using var secrets = new TestSurfaceSecretsScope();
var mockService = new InMemoryLayerSbomService();
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.AddSingleton<ILayerSbomService>(mockService);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var scanId = await SubmitScanAsync(client, imageDigest);
mockService.AddScan(scanId, imageDigest, CreateTestLayers(1));
// Note: not adding composition recipe
var response = await client.GetAsync($"{BasePath}/{scanId}/composition-recipe");
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
#endregion
#region Verify Composition Recipe Tests
[Fact]
public async Task VerifyCompositionRecipe_WhenValid_ReturnsSuccess()
{
var scanId = "scan-" + Guid.NewGuid().ToString("N");
var mockService = new InMemoryLayerSbomService();
var coordinator = new StubScanCoordinator();
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
coordinator.AddScan(scanId, "sha256:image123");
mockService.SetVerificationResult(scanId, new CompositionRecipeVerificationResult
{
Valid = true,
MerkleRootMatch = true,
LayerDigestsMatch = true,
Errors = ImmutableArray<string>.Empty,
});
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<ILayerSbomService>();
services.AddSingleton<ILayerSbomService>(mockService);
services.RemoveAll<IScanCoordinator>();
services.AddSingleton<IScanCoordinator>(coordinator);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<CompositionRecipeVerificationResponseDto>();
Assert.NotNull(result);
Assert.True(result!.Valid);
Assert.True(result.MerkleRootMatch);
Assert.True(result.LayerDigestsMatch);
Assert.Null(result.Errors);
}
[Fact]
public async Task VerifyCompositionRecipe_WhenInvalid_ReturnsErrors()
{
var scanId = "scan-" + Guid.NewGuid().ToString("N");
var mockService = new InMemoryLayerSbomService();
var coordinator = new StubScanCoordinator();
mockService.AddScan(scanId, "sha256:image123", CreateTestLayers(2));
coordinator.AddScan(scanId, "sha256:image123");
mockService.SetVerificationResult(scanId, new CompositionRecipeVerificationResult
{
Valid = false,
MerkleRootMatch = false,
LayerDigestsMatch = true,
Errors = ImmutableArray.Create("Merkle root mismatch: expected sha256:abc, got sha256:def"),
});
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<ILayerSbomService>();
services.AddSingleton<ILayerSbomService>(mockService);
services.RemoveAll<IScanCoordinator>();
services.AddSingleton<IScanCoordinator>(coordinator);
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.PostAsync($"{BasePath}/{scanId}/composition-recipe/verify", null);
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
var result = await response.Content.ReadFromJsonAsync<CompositionRecipeVerificationResponseDto>();
Assert.NotNull(result);
Assert.False(result!.Valid);
Assert.False(result.MerkleRootMatch);
Assert.NotNull(result.Errors);
Assert.Single(result.Errors!);
Assert.Contains("Merkle root mismatch", result.Errors![0]);
}
[Fact]
public async Task VerifyCompositionRecipe_WhenScanNotFound_Returns404()
{
await using var factory = new ScannerApplicationFactory()
.WithOverrides(configureServices: services =>
{
services.RemoveAll<ILayerSbomService>();
services.AddSingleton<ILayerSbomService, InMemoryLayerSbomService>();
services.RemoveAll<IScanCoordinator>();
services.AddSingleton<IScanCoordinator, StubScanCoordinator>();
});
await factory.InitializeAsync();
using var client = factory.CreateClient();
var response = await client.PostAsync($"{BasePath}/scan-not-found/composition-recipe/verify", null);
Assert.Equal(HttpStatusCode.NotFound, response.StatusCode);
}
#endregion
#region Test Helpers
private static async Task<string> SubmitScanAsync(HttpClient client, string imageDigest)
{
var submitRequest = new ScanSubmitRequest
{
Image = new ScanImageDescriptor { Digest = imageDigest }
};
var submitResponse = await client.PostAsJsonAsync("/api/v1/scans", submitRequest);
Assert.Equal(HttpStatusCode.Accepted, submitResponse.StatusCode);
var submitResult = await submitResponse.Content.ReadFromJsonAsync<ScanSubmitResponse>();
Assert.NotNull(submitResult);
return submitResult!.ScanId;
}
private static LayerSummary[] CreateTestLayers(int count, string? specificDigest = null)
{
var layers = new LayerSummary[count];
for (int i = 0; i < count; i++)
{
layers[i] = CreateLayerSummary(
i == 0 && specificDigest != null ? specificDigest : $"sha256:layer{i}",
i,
10 + i * 5);
}
return layers;
}
private static LayerSummary CreateLayerSummary(string digest, int order, int componentCount)
{
return new LayerSummary
{
LayerDigest = digest,
Order = order,
HasSbom = true,
ComponentCount = componentCount,
};
}
private static byte[] CreateTestSbomBytes(string format)
{
var content = format == "spdx"
? """{"spdxVersion":"SPDX-3.0.1","format":"spdx"}"""
: """{"bomFormat":"CycloneDX","specVersion":"1.7","format":"cyclonedx"}""";
return Encoding.UTF8.GetBytes(content);
}
private static CompositionRecipeResponse CreateTestRecipe(string scanId, string imageDigest, int layerCount)
{
var layers = new CompositionRecipeLayer[layerCount];
for (int i = 0; i < layerCount; i++)
{
layers[i] = new CompositionRecipeLayer
{
Digest = $"sha256:layer{i}",
Order = i,
FragmentDigest = $"sha256:frag{i}",
SbomDigests = new LayerSbomDigests
{
CycloneDx = $"sha256:cdx{i}",
Spdx = $"sha256:spdx{i}",
},
ComponentCount = 10 + i * 5,
};
}
return new CompositionRecipeResponse
{
ScanId = scanId,
ImageDigest = imageDigest,
CreatedAt = DateTimeOffset.UtcNow.ToString("O"),
Recipe = new CompositionRecipe
{
Version = "1.0.0",
GeneratorName = "StellaOps.Scanner",
GeneratorVersion = "2026.04",
Layers = layers.ToImmutableArray(),
MerkleRoot = "sha256:merkleroot123",
AggregatedSbomDigests = new AggregatedSbomDigests
{
CycloneDx = "sha256:finalcdx",
Spdx = "sha256:finalspdx",
},
},
};
}
#endregion
}
/// <summary>
/// In-memory implementation of ILayerSbomService for testing.
/// </summary>
internal sealed class InMemoryLayerSbomService : ILayerSbomService
{
private readonly Dictionary<string, (string ImageDigest, LayerSummary[] Layers)> _scans = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<(string ScanId, string LayerDigest, string Format), byte[]> _layerSboms = new();
private readonly Dictionary<string, CompositionRecipeResponse> _recipes = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, CompositionRecipeVerificationResult> _verificationResults = new(StringComparer.OrdinalIgnoreCase);
public void AddScan(string scanId, string imageDigest, LayerSummary[] layers)
{
_scans[scanId] = (imageDigest, layers);
}
public bool HasScan(string scanId) => _scans.ContainsKey(scanId);
public (string ImageDigest, LayerSummary[] Layers)? GetScanData(string scanId)
{
if (_scans.TryGetValue(scanId, out var data))
return data;
return null;
}
public void AddLayerSbom(string scanId, string layerDigest, string format, byte[] sbomBytes)
{
_layerSboms[(scanId, layerDigest, format)] = sbomBytes;
}
public void AddCompositionRecipe(string scanId, CompositionRecipeResponse recipe)
{
_recipes[scanId] = recipe;
}
public void SetVerificationResult(string scanId, CompositionRecipeVerificationResult result)
{
_verificationResults[scanId] = result;
}
public Task<ImmutableArray<LayerSummary>> GetLayerSummariesAsync(
ScanId scanId,
CancellationToken cancellationToken = default)
{
if (!_scans.TryGetValue(scanId.Value, out var scanData))
{
return Task.FromResult(ImmutableArray<LayerSummary>.Empty);
}
return Task.FromResult(scanData.Layers.OrderBy(l => l.Order).ToImmutableArray());
}
public Task<byte[]?> GetLayerSbomAsync(
ScanId scanId,
string layerDigest,
string format,
CancellationToken cancellationToken = default)
{
if (_layerSboms.TryGetValue((scanId.Value, layerDigest, format), out var sbomBytes))
{
return Task.FromResult<byte[]?>(sbomBytes);
}
return Task.FromResult<byte[]?>(null);
}
public Task<CompositionRecipeResponse?> GetCompositionRecipeAsync(
ScanId scanId,
CancellationToken cancellationToken = default)
{
if (_recipes.TryGetValue(scanId.Value, out var recipe))
{
return Task.FromResult<CompositionRecipeResponse?>(recipe);
}
return Task.FromResult<CompositionRecipeResponse?>(null);
}
public Task<CompositionRecipeVerificationResult?> VerifyCompositionRecipeAsync(
ScanId scanId,
CancellationToken cancellationToken = default)
{
if (_verificationResults.TryGetValue(scanId.Value, out var result))
{
return Task.FromResult<CompositionRecipeVerificationResult?>(result);
}
return Task.FromResult<CompositionRecipeVerificationResult?>(null);
}
public Task StoreLayerSbomsAsync(
ScanId scanId,
string imageDigest,
LayerSbomCompositionResult result,
CancellationToken cancellationToken = default)
{
// Not implemented for tests
return Task.CompletedTask;
}
public Task<byte[]?> GetComposedSbomAsync(
ScanId scanId,
string format,
CancellationToken cancellationToken = default)
{
// Return the first matching layer SBOM for testing purposes
var key = _layerSboms.Keys.FirstOrDefault(k => k.ScanId == scanId.Value && k.Format == format);
if (key != default && _layerSboms.TryGetValue(key, out var sbom))
{
return Task.FromResult<byte[]?>(sbom);
}
return Task.FromResult<byte[]?>(null);
}
public Task<IReadOnlyList<SbomLayerFragment>?> GetLayerFragmentsAsync(
ScanId scanId,
CancellationToken cancellationToken = default)
{
if (!_scans.TryGetValue(scanId.Value, out var scanData))
{
return Task.FromResult<IReadOnlyList<SbomLayerFragment>?>(null);
}
var fragments = scanData.Layers
.OrderBy(l => l.Order)
.Select(l => new SbomLayerFragment
{
LayerDigest = l.LayerDigest,
Order = l.Order,
ComponentPurls = new List<string> { $"pkg:test/layer{l.Order}@1.0.0" }
})
.ToList();
return Task.FromResult<IReadOnlyList<SbomLayerFragment>?>(fragments);
}
}
/// <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);
}