Files
git.stella-ops.org/src/SbomService/StellaOps.SbomService.Tests/SbomLedgerEndpointsTests.cs

163 lines
5.7 KiB
C#

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<WebApplicationFactory<Program>>
{
private readonly WebApplicationFactory<Program> _factory;
public SbomLedgerEndpointsTests(WebApplicationFactory<Program> 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<SbomUploadResponse>();
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<SbomUploadResponse>();
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<SbomUploadResponse>();
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<SbomVersionHistoryResult>();
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<SbomDiffResult>();
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<SbomLineageResult>();
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"
}
]
}
]
}
""";
}
}