Files
git.stella-ops.org/src/Scanner/__Tests/StellaOps.Scanner.WebService.Tests/SbomUploadEndpointsTests.cs

169 lines
6.3 KiB
C#

using System.Collections.Concurrent;
using System.Net;
using System.Net.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using StellaOps.Scanner.Storage.ObjectStore;
using StellaOps.Scanner.WebService.Contracts;
using Xunit;
namespace StellaOps.Scanner.WebService.Tests;
public sealed class SbomUploadEndpointsTests
{
[Fact]
public async Task Upload_accepts_cyclonedx_fixture_and_returns_record()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = CreateFactory();
using var client = factory.CreateClient();
var request = new SbomUploadRequestDto
{
ArtifactRef = "example.com/app:1.0",
SbomBase64 = LoadFixtureBase64("sample.cdx.json"),
Source = new SbomUploadSourceDto
{
Tool = "syft",
Version = "1.0.0",
CiContext = new SbomUploadCiContextDto
{
BuildId = "build-123",
Repository = "github.com/example/app"
}
}
};
var response = await client.PostAsJsonAsync("/api/v1/sbom/upload", request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<SbomUploadResponseDto>();
Assert.NotNull(payload);
Assert.Equal("example.com/app:1.0", payload!.ArtifactRef);
Assert.Equal("cyclonedx", payload.Format);
Assert.Equal("1.6", payload.FormatVersion);
Assert.True(payload.ValidationResult.Valid);
Assert.False(string.IsNullOrWhiteSpace(payload.AnalysisJobId));
var recordResponse = await client.GetAsync($"/api/v1/sbom/uploads/{payload.SbomId}");
Assert.Equal(HttpStatusCode.OK, recordResponse.StatusCode);
var record = await recordResponse.Content.ReadFromJsonAsync<SbomUploadRecordDto>();
Assert.NotNull(record);
Assert.Equal(payload.SbomId, record!.SbomId);
Assert.Equal("example.com/app:1.0", record.ArtifactRef);
Assert.Equal("syft", record.Source?.Tool);
Assert.Equal("build-123", record.Source?.CiContext?.BuildId);
}
[Fact]
public async Task Upload_accepts_spdx_fixture_and_reports_quality_score()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = CreateFactory();
using var client = factory.CreateClient();
var request = new SbomUploadRequestDto
{
ArtifactRef = "example.com/service:2.0",
SbomBase64 = LoadFixtureBase64("sample.spdx.json")
};
var response = await client.PostAsJsonAsync("/api/v1/sbom/upload", request);
Assert.Equal(HttpStatusCode.Accepted, response.StatusCode);
var payload = await response.Content.ReadFromJsonAsync<SbomUploadResponseDto>();
Assert.NotNull(payload);
Assert.Equal("spdx", payload!.Format);
Assert.Equal("2.3", payload.FormatVersion);
Assert.True(payload.ValidationResult.Valid);
Assert.True(payload.ValidationResult.QualityScore > 0);
Assert.True(payload.ValidationResult.ComponentCount > 0);
}
[Fact]
public async Task Upload_rejects_unknown_format()
{
using var secrets = new TestSurfaceSecretsScope();
using var factory = CreateFactory();
using var client = factory.CreateClient();
var invalid = new SbomUploadRequestDto
{
ArtifactRef = "example.com/invalid:1.0",
SbomBase64 = Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes("{\"name\":\"oops\"}"))
};
var response = await client.PostAsJsonAsync("/api/v1/sbom/upload", invalid);
Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode);
}
private static ScannerApplicationFactory CreateFactory()
{
return new ScannerApplicationFactory().WithOverrides(configuration =>
{
configuration["scanner:authority:enabled"] = "false";
}, configureServices: services =>
{
services.RemoveAll<IArtifactObjectStore>();
services.AddSingleton<IArtifactObjectStore>(new InMemoryArtifactObjectStore());
});
}
private static string LoadFixtureBase64(string fileName)
{
var baseDirectory = AppContext.BaseDirectory;
var repoRoot = Path.GetFullPath(Path.Combine(baseDirectory, "..", "..", "..", "..", ".."));
var path = Path.Combine(
repoRoot,
"tests",
"AirGap",
"StellaOps.AirGap.Importer.Tests",
"Reconciliation",
"Fixtures",
fileName);
Assert.True(File.Exists(path), $"Fixture not found at {path}.");
var bytes = File.ReadAllBytes(path);
return Convert.ToBase64String(bytes);
}
private sealed class InMemoryArtifactObjectStore : IArtifactObjectStore
{
private readonly ConcurrentDictionary<string, byte[]> _objects = new(StringComparer.Ordinal);
public async Task PutAsync(ArtifactObjectDescriptor descriptor, Stream content, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(descriptor);
ArgumentNullException.ThrowIfNull(content);
using var buffer = new MemoryStream();
await content.CopyToAsync(buffer, cancellationToken).ConfigureAwait(false);
var key = $"{descriptor.Bucket}:{descriptor.Key}";
_objects[key] = buffer.ToArray();
}
public Task<Stream?> GetAsync(ArtifactObjectDescriptor descriptor, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(descriptor);
var key = $"{descriptor.Bucket}:{descriptor.Key}";
if (!_objects.TryGetValue(key, out var bytes))
{
return Task.FromResult<Stream?>(null);
}
return Task.FromResult<Stream?>(new MemoryStream(bytes, writable: false));
}
public Task DeleteAsync(ArtifactObjectDescriptor descriptor, CancellationToken cancellationToken)
{
ArgumentNullException.ThrowIfNull(descriptor);
var key = $"{descriptor.Bucket}:{descriptor.Key}";
_objects.TryRemove(key, out _);
return Task.CompletedTask;
}
}
}