Refactor code structure for improved readability and maintainability; removed redundant code blocks and optimized function calls.
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
using System.Collections.Immutable;
|
||||
using StellaOps.Graph.Indexer.Schema;
|
||||
|
||||
namespace StellaOps.Graph.Indexer.Tests;
|
||||
|
||||
public sealed class GraphIdentityTests
|
||||
{
|
||||
[Fact]
|
||||
public void ComputeNodeId_IsDeterministic_WhenTupleOrderChanges()
|
||||
{
|
||||
var tupleA = ImmutableDictionary<string, string>.Empty
|
||||
.Add("purl", "pkg:npm/test@1.0.0")
|
||||
.Add("reason", "declared")
|
||||
.Add("scope", "runtime");
|
||||
|
||||
var tupleB = ImmutableDictionary<string, string>.Empty
|
||||
.Add("scope", "runtime")
|
||||
.Add("reason", "declared")
|
||||
.Add("purl", "pkg:npm/test@1.0.0");
|
||||
|
||||
var idA = GraphIdentity.ComputeNodeId("Tenant-A", "Component", tupleA);
|
||||
var idB = GraphIdentity.ComputeNodeId("tenant-a", "component", tupleB);
|
||||
|
||||
Assert.Equal(idA, idB);
|
||||
Assert.StartsWith("gn:tenant-a:component:", idA);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ComputeEdgeId_IsCaseInsensitiveExceptFingerprintFields()
|
||||
{
|
||||
var tupleLower = ImmutableDictionary<string, string>.Empty
|
||||
.Add("digest", "sha256:ABC")
|
||||
.Add("source", "sbom");
|
||||
|
||||
var tupleUpper = ImmutableDictionary<string, string>.Empty
|
||||
.Add("digest", "sha256:abc")
|
||||
.Add("source", "SBOM");
|
||||
|
||||
var edgeA = GraphIdentity.ComputeEdgeId("TENANT", "depends_on", tupleLower);
|
||||
var edgeB = GraphIdentity.ComputeEdgeId("tenant", "DEPENDS_ON", tupleUpper);
|
||||
|
||||
// digest key is case-sensitive by design; different casing produces different id when value changes.
|
||||
Assert.NotEqual(edgeA, edgeB);
|
||||
Assert.StartsWith("ge:tenant:DEPENDS_ON:", edgeA);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.Json.Nodes;
|
||||
using StellaOps.Graph.Indexer.Documents;
|
||||
using StellaOps.Graph.Indexer.Ingestion.Sbom;
|
||||
|
||||
namespace StellaOps.Graph.Indexer.Tests;
|
||||
|
||||
public sealed class GraphSnapshotBuilderTests
|
||||
{
|
||||
[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());
|
||||
}
|
||||
|
||||
[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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
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;
|
||||
|
||||
namespace StellaOps.Graph.Indexer.Tests;
|
||||
|
||||
public sealed class SbomSnapshotExporterTests
|
||||
{
|
||||
[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
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="../../StellaOps.Graph.Indexer/StellaOps.Graph.Indexer.csproj" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user