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(); 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(); 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(); 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(); services.AddSingleton(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 _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 GetAsync(ArtifactObjectDescriptor descriptor, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(descriptor); var key = $"{descriptor.Bucket}:{descriptor.Key}"; if (!_objects.TryGetValue(key, out var bytes)) { return Task.FromResult(null); } return Task.FromResult(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; } } }