112 lines
3.6 KiB
C#
112 lines
3.6 KiB
C#
using System.Collections.Immutable;
|
|
using System.Text.Json.Nodes;
|
|
using StellaOps.Graph.Indexer.Documents;
|
|
using StellaOps.Graph.Indexer.Ingestion.Sbom;
|
|
using StellaOps.Graph.Indexer.Schema;
|
|
using StellaOps.TestKit;
|
|
|
|
namespace StellaOps.Graph.Indexer.Tests;
|
|
|
|
public sealed class SbomSnapshotExporterTests
|
|
{
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public async Task ExportAsync_WritesCanonicalFilesWithStableHash()
|
|
{
|
|
var snapshot = new SbomSnapshot
|
|
{
|
|
Tenant = "tenant-c",
|
|
ArtifactDigest = "sha256:artifact-c",
|
|
SbomDigest = "sha256:sbom-c",
|
|
BaseArtifacts = Array.Empty<SbomBaseArtifact>()
|
|
};
|
|
|
|
var nodes = new[]
|
|
{
|
|
CreateArtifactNode("art", snapshot.ArtifactDigest, snapshot.SbomDigest),
|
|
CreateComponentNode("comp", "pkg:npm/test@1.0.0")
|
|
}.ToImmutableArray();
|
|
|
|
var edges = new[]
|
|
{
|
|
CreateEdge("edge-1", source: "art", target: "comp")
|
|
}.ToImmutableArray();
|
|
|
|
var batch = new GraphBuildBatch(nodes, edges);
|
|
var exporter = new SbomSnapshotExporter(new GraphSnapshotBuilder(), new FileSystemSnapshotFileWriter(_tempRoot));
|
|
|
|
await exporter.ExportAsync(snapshot, batch, CancellationToken.None);
|
|
|
|
var manifestPath = Path.Combine(_tempRoot, "manifest.json");
|
|
var manifestJson = JsonNode.Parse(await File.ReadAllTextAsync(manifestPath))!.AsObject();
|
|
|
|
// Hash in manifest should equal recomputed canonical hash.
|
|
var computed = GraphIdentity.ComputeDocumentHash(manifestJson);
|
|
Assert.Equal(computed, manifestJson["hash"]!.GetValue<string>());
|
|
|
|
// Adjacency should contain both nodes and edges, deterministic ids.
|
|
var adjacency = JsonNode.Parse(await File.ReadAllTextAsync(Path.Combine(_tempRoot, "adjacency.json")))!.AsObject();
|
|
var nodesArray = adjacency["nodes"]!.AsArray();
|
|
Assert.Equal(2, nodesArray.Count);
|
|
Assert.Equal("art", nodesArray[0]! ["node_id"]!.GetValue<string>());
|
|
|
|
// nodes.jsonl and edges.jsonl should both exist and be non-empty.
|
|
Assert.True(new FileInfo(Path.Combine(_tempRoot, "nodes.jsonl")).Length > 0);
|
|
Assert.True(new FileInfo(Path.Combine(_tempRoot, "edges.jsonl")).Length > 0);
|
|
}
|
|
|
|
public SbomSnapshotExporterTests()
|
|
{
|
|
_tempRoot = Path.Combine(Path.GetTempPath(), "graph-snapshot-tests", Guid.NewGuid().ToString("n"));
|
|
Directory.CreateDirectory(_tempRoot);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
try { Directory.Delete(_tempRoot, recursive: true); } catch { /* ignore */ }
|
|
}
|
|
|
|
private readonly string _tempRoot;
|
|
|
|
private static JsonObject CreateArtifactNode(string id, string artifactDigest, string sbomDigest)
|
|
{
|
|
var attributes = new JsonObject
|
|
{
|
|
["artifact_digest"] = artifactDigest,
|
|
["sbom_digest"] = sbomDigest
|
|
};
|
|
|
|
return new JsonObject
|
|
{
|
|
["id"] = id,
|
|
["kind"] = "artifact",
|
|
["attributes"] = attributes
|
|
};
|
|
}
|
|
|
|
private static JsonObject CreateComponentNode(string id, string purl)
|
|
{
|
|
var attributes = new JsonObject
|
|
{
|
|
["purl"] = purl
|
|
};
|
|
|
|
return new JsonObject
|
|
{
|
|
["id"] = id,
|
|
["kind"] = "component",
|
|
["attributes"] = attributes
|
|
};
|
|
}
|
|
|
|
private static JsonObject CreateEdge(string id, string source, string target)
|
|
{
|
|
return new JsonObject
|
|
{
|
|
["id"] = id,
|
|
["source"] = source,
|
|
["target"] = target
|
|
};
|
|
}
|
|
}
|