Repair release investigation workspace contracts

This commit is contained in:
master
2026-03-09 23:19:42 +02:00
parent 3ecafc49a3
commit 359fafa9da
20 changed files with 1806 additions and 284 deletions

View File

@@ -0,0 +1,153 @@
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
using FluentAssertions;
using Microsoft.AspNetCore.Mvc.Testing;
using StellaOps.SbomService.Models;
using StellaOps.SbomService.Services;
using StellaOps.TestKit;
using Xunit;
namespace StellaOps.SbomService.Tests;
public sealed class ChangeTraceCompatibilityEndpointsTests : IClassFixture<WebApplicationFactory<StellaOps.SbomService.Program>>
{
private const string TenantId = "github.com/acme/change-trace";
private readonly WebApplicationFactory<StellaOps.SbomService.Program> _factory;
public ChangeTraceCompatibilityEndpointsTests(WebApplicationFactory<StellaOps.SbomService.Program> factory)
{
_factory = factory.WithWebHostBuilder(_ => { });
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Build_endpoint_returns_deterministic_change_trace_for_uploaded_artifacts()
{
var client = CreateAuthenticatedClient();
var firstUpload = await UploadAsync(client, "1.0.0");
var secondUpload = await UploadAsync(client, "1.1.0");
var response = await client.PostAsJsonAsync("/api/change-traces/build", new ChangeTraceBuildRequest
{
TenantId = TenantId,
FromDigest = firstUpload.Digest,
ToDigest = secondUpload.Digest,
IncludeByteDiff = false,
});
response.StatusCode.Should().Be(HttpStatusCode.OK);
var payload = await response.Content.ReadFromJsonAsync<ChangeTraceDocument>();
payload.Should().NotBeNull();
payload!.TraceId.Should().NotBeNullOrWhiteSpace();
payload.Subject.FromDigest.Should().Be(firstUpload.Digest);
payload.Subject.ToDigest.Should().Be(secondUpload.Digest);
payload.Deltas.Should().ContainSingle();
payload.Deltas[0].ChangeType.Should().BeOneOf("upgraded", "patched");
payload.Summary.ChangedPackages.Should().Be(1);
payload.Commitment.Should().NotBeNull();
payload.Commitment!.Sha256.Should().StartWith("sha256:");
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Get_endpoint_rehydrates_trace_from_trace_id()
{
var client = CreateAuthenticatedClient();
var firstUpload = await UploadAsync(client, "2.0.0");
var secondUpload = await UploadAsync(client, "2.0.1");
var buildResponse = await client.PostAsJsonAsync("/api/change-traces/build", new ChangeTraceBuildRequest
{
TenantId = TenantId,
FromDigest = firstUpload.Digest,
ToDigest = secondUpload.Digest,
IncludeByteDiff = false,
});
buildResponse.EnsureSuccessStatusCode();
var builtTrace = await buildResponse.Content.ReadFromJsonAsync<ChangeTraceDocument>();
builtTrace.Should().NotBeNull();
var getResponse = await client.GetAsync($"/api/change-traces/{Uri.EscapeDataString(builtTrace!.TraceId)}");
getResponse.StatusCode.Should().Be(HttpStatusCode.OK);
var rehydratedTrace = await getResponse.Content.ReadFromJsonAsync<ChangeTraceDocument>();
rehydratedTrace.Should().NotBeNull();
rehydratedTrace!.TraceId.Should().Be(builtTrace.TraceId);
rehydratedTrace.Subject.FromDigest.Should().Be(firstUpload.Digest);
rehydratedTrace.Subject.ToDigest.Should().Be(secondUpload.Digest);
rehydratedTrace.Summary.ChangedPackages.Should().Be(1);
}
[Trait("Category", TestCategories.Unit)]
[Fact]
public async Task Get_endpoint_rejects_invalid_trace_id()
{
var client = CreateAuthenticatedClient();
var response = await client.GetAsync("/api/change-traces/not-a-valid-trace-id");
response.StatusCode.Should().Be(HttpStatusCode.BadRequest);
var payload = await response.Content.ReadFromJsonAsync<JsonElement>();
payload.GetProperty("error").GetString().Should().Be("invalid traceId");
}
private async Task<SbomUploadResponse> UploadAsync(HttpClient client, string version)
{
var response = await client.PostAsJsonAsync(
"/sbom/upload",
new SbomUploadRequest
{
ArtifactRef = "acme/change-trace:demo",
Sbom = JsonDocument.Parse($$"""
{
"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"
}
]
}
]
}
""").RootElement.Clone(),
Source = new SbomUploadSource
{
Tool = "syft",
Version = "1.0.0",
CiContext = new SbomUploadCiContext
{
BuildId = $"build-{version}",
Repository = TenantId,
},
},
});
response.EnsureSuccessStatusCode();
var payload = await response.Content.ReadFromJsonAsync<SbomUploadResponse>();
payload.Should().NotBeNull();
return payload!;
}
private HttpClient CreateAuthenticatedClient()
{
var client = _factory.CreateClient();
client.DefaultRequestHeaders.Add("X-Tenant-Id", TenantId);
client.DefaultRequestHeaders.Add("X-User-Id", "change-trace-test");
return client;
}
}