using System.Net; using System.Net.Http.Json; using System.Text.Json; using FluentAssertions; using Microsoft.AspNetCore.Mvc.Testing; using StellaOps.SbomService.Models; using Xunit; using StellaOps.TestKit; namespace StellaOps.SbomService.Tests; public sealed class SbomLedgerEndpointsTests : IClassFixture> { private readonly WebApplicationFactory _factory; public SbomLedgerEndpointsTests(WebApplicationFactory factory) { _factory = factory.WithWebHostBuilder(_ => { }); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task Upload_accepts_cyclonedx_and_returns_analysis_job() { var client = _factory.CreateClient(); var request = CreateUploadRequest("acme/app:1.0", CycloneDxSample()); var response = await client.PostAsJsonAsync("/sbom/upload", request); response.StatusCode.Should().Be(HttpStatusCode.Accepted); var payload = await response.Content.ReadFromJsonAsync(); payload.Should().NotBeNull(); payload!.ArtifactRef.Should().Be("acme/app:1.0"); payload.ValidationResult.Valid.Should().BeTrue(); payload.ValidationResult.ComponentCount.Should().Be(1); payload.AnalysisJobId.Should().NotBeNullOrWhiteSpace(); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task Upload_accepts_spdx_and_records_history() { var client = _factory.CreateClient(); var artifact = "acme/worker:2.0"; var first = await client.PostAsJsonAsync("/sbom/upload", CreateUploadRequest(artifact, SpdxSample("4.17.21"))); first.StatusCode.Should().Be(HttpStatusCode.Accepted); var firstPayload = await first.Content.ReadFromJsonAsync(); firstPayload.Should().NotBeNull(); var second = await client.PostAsJsonAsync("/sbom/upload", CreateUploadRequest(artifact, SpdxSample("4.17.22"))); second.StatusCode.Should().Be(HttpStatusCode.Accepted); var secondPayload = await second.Content.ReadFromJsonAsync(); secondPayload.Should().NotBeNull(); var history = await client.GetAsync($"/sbom/ledger/history?artifact={Uri.EscapeDataString(artifact)}&limit=5"); history.StatusCode.Should().Be(HttpStatusCode.OK); var historyPayload = await history.Content.ReadFromJsonAsync(); historyPayload.Should().NotBeNull(); historyPayload!.Versions.Should().HaveCount(2); var diff = await client.GetAsync($"/sbom/ledger/diff?before={firstPayload!.SbomId}&after={secondPayload!.SbomId}"); diff.StatusCode.Should().Be(HttpStatusCode.OK); var diffPayload = await diff.Content.ReadFromJsonAsync(); diffPayload.Should().NotBeNull(); diffPayload!.Summary.VersionChangedCount.Should().Be(1); } [Trait("Category", TestCategories.Unit)] [Fact] public async Task Lineage_includes_build_edges_for_shared_build_id() { var client = _factory.CreateClient(); var artifact = "acme/build:1.0"; var first = await client.PostAsJsonAsync("/sbom/upload", CreateUploadRequest(artifact, SpdxSample("1.0.0"))); first.StatusCode.Should().Be(HttpStatusCode.Accepted); var second = await client.PostAsJsonAsync("/sbom/upload", CreateUploadRequest(artifact, SpdxSample("1.1.0"))); second.StatusCode.Should().Be(HttpStatusCode.Accepted); var lineage = await client.GetAsync($"/sbom/ledger/lineage?artifact={Uri.EscapeDataString(artifact)}"); lineage.StatusCode.Should().Be(HttpStatusCode.OK); var payload = await lineage.Content.ReadFromJsonAsync(); payload.Should().NotBeNull(); payload!.Edges.Should().Contain(e => e.Relationship == SbomLineageRelationships.Build); } private static SbomUploadRequest CreateUploadRequest(string artifactRef, string sbomJson) { using var document = JsonDocument.Parse(sbomJson); using StellaOps.TestKit; return new SbomUploadRequest { ArtifactRef = artifactRef, Sbom = document.RootElement.Clone(), Source = new SbomUploadSource { Tool = "syft", Version = "1.0.0", CiContext = new SbomUploadCiContext { BuildId = "build-01", Repository = "github.com/acme/app" } } }; } private static string CycloneDxSample() { return """ { "bomFormat": "CycloneDX", "specVersion": "1.6", "version": 1, "components": [ { "type": "library", "name": "lodash", "version": "4.17.21", "purl": "pkg:npm/lodash@4.17.21", "licenses": [ { "license": { "id": "MIT" } } ] } ] } """; } private static string SpdxSample(string version) { return $$""" { "spdxVersion": "SPDX-2.3", "SPDXID": "SPDXRef-DOCUMENT", "name": "sample", "dataLicense": "CC0-1.0", "packages": [ { "SPDXID": "SPDXRef-Package-lodash", "name": "lodash", "versionInfo": "{{version}}", "licenseDeclared": "MIT", "externalRefs": [ { "referenceType": "purl", "referenceLocator": "pkg:npm/lodash@{{version}}", "referenceCategory": "PACKAGE-MANAGER" } ] } ] } """; } }