169 lines
6.3 KiB
C#
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;
|
|
}
|
|
}
|
|
}
|