135 lines
4.7 KiB
C#
135 lines
4.7 KiB
C#
using System.Collections.Immutable;
|
|
using System.Text.Json.Nodes;
|
|
using StellaOps.Graph.Indexer.Documents;
|
|
using StellaOps.Graph.Indexer.Ingestion.Sbom;
|
|
|
|
using StellaOps.TestKit;
|
|
namespace StellaOps.Graph.Indexer.Tests;
|
|
|
|
public sealed class GraphSnapshotBuilderTests
|
|
{
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Build_ProducesDeterministicAdjacencyOrdering()
|
|
{
|
|
var snapshot = new SbomSnapshot
|
|
{
|
|
Tenant = "tenant-a",
|
|
ArtifactDigest = "sha256:artifact",
|
|
SbomDigest = "sha256:sbom",
|
|
BaseArtifacts = Array.Empty<SbomBaseArtifact>()
|
|
};
|
|
|
|
// Nodes intentionally provided out of order to verify deterministic sorting.
|
|
var nodes = new[]
|
|
{
|
|
CreateComponentNode("node-b", "pkg:type/b@2.0"),
|
|
CreateArtifactNode("node-artifact", snapshot.ArtifactDigest, snapshot.SbomDigest),
|
|
CreateComponentNode("node-a", "pkg:type/a@1.0")
|
|
}.ToImmutableArray();
|
|
|
|
// Edges also out of order; adjacency should normalize ordering.
|
|
var edges = new[]
|
|
{
|
|
CreateEdge("edge-b", source: "node-artifact", target: "node-b"),
|
|
CreateEdge("edge-a", source: "node-artifact", target: "node-a")
|
|
}.ToImmutableArray();
|
|
|
|
var batch = new GraphBuildBatch(nodes, edges);
|
|
var builder = new GraphSnapshotBuilder();
|
|
|
|
var result = builder.Build(snapshot, batch, generatedAt: DateTimeOffset.Parse("2025-11-18T00:00:00Z"));
|
|
|
|
// Node ordering is lexicographic by node id.
|
|
var nodeIds = result.Adjacency.Nodes.Select(n => n.NodeId).ToArray();
|
|
Assert.Equal(new[] { "node-a", "node-artifact", "node-b" }, nodeIds);
|
|
|
|
// Outgoing edges are sorted per-node.
|
|
var artifactNode = result.Adjacency.Nodes.Single(n => n.NodeId == "node-artifact");
|
|
Assert.Equal(new[] { "edge-a", "edge-b" }, artifactNode.OutgoingEdges.ToArray());
|
|
|
|
// Incoming edges preserved deterministically on targets.
|
|
Assert.Equal(new[] { "edge-a" }, result.Adjacency.Nodes.Single(n => n.NodeId == "node-a").IncomingEdges.ToArray());
|
|
Assert.Equal(new[] { "edge-b" }, result.Adjacency.Nodes.Single(n => n.NodeId == "node-b").IncomingEdges.ToArray());
|
|
}
|
|
|
|
[Trait("Category", TestCategories.Unit)]
|
|
[Fact]
|
|
public void Build_ComputesStableManifestHash_ForShuffledInputs()
|
|
{
|
|
var snapshot = new SbomSnapshot
|
|
{
|
|
Tenant = "tenant-b",
|
|
ArtifactDigest = "sha256:artifact-b",
|
|
SbomDigest = "sha256:sbom-b",
|
|
BaseArtifacts = Array.Empty<SbomBaseArtifact>()
|
|
};
|
|
|
|
var nodesA = new[]
|
|
{
|
|
CreateArtifactNode("art", snapshot.ArtifactDigest, snapshot.SbomDigest),
|
|
CreateComponentNode("comp-1", "pkg:nuget/one@1.0.0"),
|
|
CreateComponentNode("comp-2", "pkg:nuget/two@2.0.0")
|
|
}.ToImmutableArray();
|
|
|
|
var edgesA = new[]
|
|
{
|
|
CreateEdge("e2", source: "art", target: "comp-2"),
|
|
CreateEdge("e1", source: "art", target: "comp-1")
|
|
}.ToImmutableArray();
|
|
|
|
var builder = new GraphSnapshotBuilder();
|
|
var t = DateTimeOffset.Parse("2025-11-18T01:23:45Z");
|
|
var baseline = builder.Build(snapshot, new GraphBuildBatch(nodesA, edgesA), t);
|
|
|
|
// Shuffle nodes/edges and ensure hash remains identical.
|
|
var nodesB = nodesA.Reverse().ToImmutableArray();
|
|
var edgesB = edgesA.Reverse().ToImmutableArray();
|
|
var shuffled = builder.Build(snapshot, new GraphBuildBatch(nodesB, edgesB), t);
|
|
|
|
Assert.Equal(baseline.Manifest.Hash, shuffled.Manifest.Hash);
|
|
Assert.Equal(baseline.Adjacency.Nodes.Select(n => n.NodeId), shuffled.Adjacency.Nodes.Select(n => n.NodeId));
|
|
}
|
|
|
|
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
|
|
};
|
|
}
|
|
}
|