Add unit tests for SBOM ingestion and transformation
Some checks failed
Docs CI / lint-and-preview (push) Has been cancelled

- Implement `SbomIngestServiceCollectionExtensionsTests` to verify the SBOM ingestion pipeline exports snapshots correctly.
- Create `SbomIngestTransformerTests` to ensure the transformation produces expected nodes and edges, including deduplication of license nodes and normalization of timestamps.
- Add `SbomSnapshotExporterTests` to test the export functionality for manifest, adjacency, nodes, and edges.
- Introduce `VexOverlayTransformerTests` to validate the transformation of VEX nodes and edges.
- Set up project file for the test project with necessary dependencies and configurations.
- Include JSON fixture files for testing purposes.
This commit is contained in:
master
2025-11-04 07:49:39 +02:00
parent f72c5c513a
commit 2eb6852d34
491 changed files with 39445 additions and 3917 deletions

View File

@@ -0,0 +1,194 @@
using System.Threading;
using System.Threading.Tasks;
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using StellaOps.Graph.Indexer.Ingestion.Sbom;
using Xunit;
namespace StellaOps.Graph.Indexer.Tests;
public sealed class SbomIngestProcessorTests
{
[Fact]
public async Task ProcessAsync_writes_batch_and_records_success_metrics()
{
var snapshot = CreateSnapshot();
var transformer = new SbomIngestTransformer();
var writer = new CaptureWriter();
var metrics = new CaptureMetrics();
var snapshotExporter = new CaptureSnapshotExporter();
var processor = new SbomIngestProcessor(transformer, writer, metrics, snapshotExporter, NullLogger<SbomIngestProcessor>.Instance);
await processor.ProcessAsync(snapshot, CancellationToken.None);
writer.LastBatch.Should().NotBeNull();
metrics.LastRecord.Should().NotBeNull();
metrics.LastRecord!.Success.Should().BeTrue();
metrics.LastRecord.NodeCount.Should().Be(writer.LastBatch!.Nodes.Length);
metrics.LastRecord.EdgeCount.Should().Be(writer.LastBatch!.Edges.Length);
snapshotExporter.LastSnapshot.Should().BeSameAs(snapshot);
snapshotExporter.LastBatch.Should().BeSameAs(writer.LastBatch);
}
[Fact]
public async Task ProcessAsync_records_failure_when_writer_throws()
{
var snapshot = CreateSnapshot();
var transformer = new SbomIngestTransformer();
var writer = new CaptureWriter(shouldThrow: true);
var metrics = new CaptureMetrics();
var snapshotExporter = new CaptureSnapshotExporter();
var processor = new SbomIngestProcessor(transformer, writer, metrics, snapshotExporter, NullLogger<SbomIngestProcessor>.Instance);
var act = () => processor.ProcessAsync(snapshot, CancellationToken.None);
await act.Should().ThrowAsync<InvalidOperationException>();
metrics.LastRecord.Should().NotBeNull();
metrics.LastRecord!.Success.Should().BeFalse();
snapshotExporter.LastSnapshot.Should().BeNull();
snapshotExporter.LastBatch.Should().BeNull();
}
private static SbomSnapshot CreateSnapshot()
{
return new SbomSnapshot
{
Tenant = "tenant-alpha",
Source = "scanner.sbom.v1",
ArtifactDigest = "sha256:test-artifact",
SbomDigest = "sha256:test-sbom",
CollectedAt = DateTimeOffset.Parse("2025-10-30T12:00:00Z"),
EventOffset = 1000,
Artifact = new SbomArtifactMetadata
{
DisplayName = "registry.example.com/app:latest",
Environment = "prod",
Labels = new[] { "demo" },
OriginRegistry = "registry.example.com",
SupplyChainStage = "deploy"
},
Build = new SbomBuildMetadata
{
BuilderId = "builder://tekton/default",
BuildType = "https://slsa.dev/provenance/v1",
AttestationDigest = "sha256:attestation",
Source = "scanner.build.v1",
CollectedAt = DateTimeOffset.Parse("2025-10-30T12:00:05Z"),
EventOffset = 2000
},
Components = new[]
{
new SbomComponent
{
Purl = "pkg:nuget/Example.Primary@1.0.0",
Version = "1.0.0",
Ecosystem = "nuget",
Scope = "runtime",
License = new SbomLicense
{
Spdx = "MIT",
Name = "MIT License",
Classification = "permissive",
SourceDigest = "sha256:license001"
},
Usage = "direct",
DetectedBy = "sbom.analyzer.transformer",
LayerDigest = "sha256:layer",
EvidenceDigest = "sha256:evidence",
CollectedAt = DateTimeOffset.Parse("2025-10-30T12:00:01Z"),
EventOffset = 1201,
Source = "scanner.component.v1",
Files = new[]
{
new SbomComponentFile
{
Path = "/src/app/Program.cs",
ContentSha256 = "sha256:file",
LanguageHint = "csharp",
SizeBytes = 1024,
Scope = "build",
DetectedBy = "sbom.analyzer.transformer",
EvidenceDigest = "sha256:file-evidence",
CollectedAt = DateTimeOffset.Parse("2025-10-30T12:00:02Z"),
EventOffset = 1202,
Source = "scanner.layer.v1"
}
},
Dependencies = Array.Empty<SbomDependency>(),
SourceType = "inventory"
}
},
BaseArtifacts = new[]
{
new SbomBaseArtifact
{
ArtifactDigest = "sha256:base",
SbomDigest = "sha256:base-sbom",
DisplayName = "registry.example.com/base:2025.09",
Environment = "prod",
Labels = new[] { "base-image" },
OriginRegistry = "registry.example.com",
SupplyChainStage = "build",
CollectedAt = DateTimeOffset.Parse("2025-10-22T08:00:00Z"),
EventOffset = 800,
Source = "scanner.sbom.v1"
}
}
};
}
private sealed class CaptureWriter : IGraphDocumentWriter
{
private readonly bool _shouldThrow;
public CaptureWriter(bool shouldThrow = false)
{
_shouldThrow = shouldThrow;
}
public GraphBuildBatch? LastBatch { get; private set; }
public Task WriteAsync(GraphBuildBatch batch, CancellationToken cancellationToken)
{
LastBatch = batch;
if (_shouldThrow)
{
throw new InvalidOperationException("Simulated persistence failure");
}
return Task.CompletedTask;
}
}
private sealed class CaptureMetrics : ISbomIngestMetrics
{
public MetricRecord? LastRecord { get; private set; }
public void RecordBatch(string source, string tenant, int nodeCount, int edgeCount, TimeSpan duration, bool success)
{
LastRecord = new MetricRecord(source, tenant, nodeCount, edgeCount, duration, success);
}
}
private sealed class CaptureSnapshotExporter : ISbomSnapshotExporter
{
public SbomSnapshot? LastSnapshot { get; private set; }
public GraphBuildBatch? LastBatch { get; private set; }
public Task ExportAsync(SbomSnapshot snapshot, GraphBuildBatch batch, CancellationToken cancellationToken)
{
LastSnapshot = snapshot;
LastBatch = batch;
return Task.CompletedTask;
}
}
private sealed record MetricRecord(
string Source,
string Tenant,
int NodeCount,
int EdgeCount,
TimeSpan Duration,
bool Success);
}