163 lines
5.7 KiB
C#
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"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}
|
|
""";
|
|
}
|
|
}
|