Files
git.stella-ops.org/src/__Libraries/__Tests/StellaOps.ReachGraph.Tests/DigestComputerTests.cs

215 lines
6.2 KiB
C#

// Licensed to StellaOps under the BUSL-1.1 license.
using StellaOps.ReachGraph.Hashing;
using StellaOps.ReachGraph.Schema;
using StellaOps.ReachGraph.Serialization;
using Xunit;
namespace StellaOps.ReachGraph.Tests;
public class DigestComputerTests
{
private readonly CanonicalReachGraphSerializer _serializer = new();
private readonly ReachGraphDigestComputer _digestComputer;
public DigestComputerTests()
{
_digestComputer = new ReachGraphDigestComputer(_serializer);
}
[Fact]
public void ComputeDigest_WithSameInput_ProducesSameDigest()
{
// Arrange
var graph = CreateSampleGraph();
// Act
var digest1 = _digestComputer.ComputeDigest(graph);
var digest2 = _digestComputer.ComputeDigest(graph);
// Assert
Assert.Equal(digest1, digest2);
}
[Fact]
public void ComputeDigest_ReturnsBlake3Format()
{
// Arrange
var graph = CreateSampleGraph();
// Act
var digest = _digestComputer.ComputeDigest(graph);
// Assert
Assert.StartsWith("blake3:", digest);
Assert.Equal(71, digest.Length); // "blake3:" (7) + 64 hex chars
}
[Fact]
public void ComputeDigest_ExcludesSignatures()
{
// Arrange
var unsigned = CreateSampleGraph();
var signed = unsigned with
{
Signatures = [new ReachGraphSignature("key-1", "sig-base64")]
};
// Act
var digestUnsigned = _digestComputer.ComputeDigest(unsigned);
var digestSigned = _digestComputer.ComputeDigest(signed);
// Assert - signatures should not affect digest
Assert.Equal(digestUnsigned, digestSigned);
}
[Fact]
public void ComputeDigest_DifferentInputs_ProduceDifferentDigests()
{
// Arrange
var graph1 = CreateSampleGraph();
var graph2 = graph1 with
{
Artifact = new ReachGraphArtifact("different-app", "sha256:different", ["linux/amd64"])
};
// Act
var digest1 = _digestComputer.ComputeDigest(graph1);
var digest2 = _digestComputer.ComputeDigest(graph2);
// Assert
Assert.NotEqual(digest1, digest2);
}
[Fact]
public void VerifyDigest_ValidDigest_ReturnsTrue()
{
// Arrange
var graph = CreateSampleGraph();
var digest = _digestComputer.ComputeDigest(graph);
// Act
var result = _digestComputer.VerifyDigest(graph, digest);
// Assert
Assert.True(result);
}
[Fact]
public void VerifyDigest_InvalidDigest_ReturnsFalse()
{
// Arrange
var graph = CreateSampleGraph();
var wrongDigest = "blake3:0000000000000000000000000000000000000000000000000000000000000000";
// Act
var result = _digestComputer.VerifyDigest(graph, wrongDigest);
// Assert
Assert.False(result);
}
[Fact]
public void IsValidBlake3Digest_ValidFormat_ReturnsTrue()
{
// Arrange
var validDigest = "blake3:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
// Act
var result = ReachGraphDigestComputer.IsValidBlake3Digest(validDigest);
// Assert
Assert.True(result);
}
[Theory]
[InlineData("sha256:abcdef")] // Wrong algorithm
[InlineData("blake3:short")] // Too short
[InlineData("blake3:ZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZZ")] // Invalid hex
[InlineData("")] // Empty
[InlineData("blake3")] // No colon
public void IsValidBlake3Digest_InvalidFormat_ReturnsFalse(string digest)
{
// Act
var result = ReachGraphDigestComputer.IsValidBlake3Digest(digest);
// Assert
Assert.False(result);
}
[Fact]
public void ParseDigest_ValidFormat_ReturnsComponents()
{
// Arrange
var digest = "blake3:abc123def456";
// Act
var result = ReachGraphDigestComputer.ParseDigest(digest);
// Assert
Assert.NotNull(result);
Assert.Equal("blake3", result.Value.Algorithm);
Assert.Equal("abc123def456", result.Value.Hash);
}
[Theory]
[InlineData("")]
[InlineData("nocolon")]
[InlineData(":noleft")]
[InlineData("noright:")]
public void ParseDigest_InvalidFormat_ReturnsNull(string digest)
{
// Act
var result = ReachGraphDigestComputer.ParseDigest(digest);
// Assert
Assert.Null(result);
}
[Fact]
public void ComputeDigest_IsDeterministic_AcrossNodeOrdering()
{
// Arrange - nodes in different order
var graph1 = CreateSampleGraph() with
{
Nodes =
[
new ReachGraphNode { Id = "sha256:aaa", Kind = ReachGraphNodeKind.Function, Ref = "a()" },
new ReachGraphNode { Id = "sha256:bbb", Kind = ReachGraphNodeKind.Function, Ref = "b()" }
]
};
var graph2 = CreateSampleGraph() with
{
Nodes =
[
new ReachGraphNode { Id = "sha256:bbb", Kind = ReachGraphNodeKind.Function, Ref = "b()" },
new ReachGraphNode { Id = "sha256:aaa", Kind = ReachGraphNodeKind.Function, Ref = "a()" }
]
};
// Act
var digest1 = _digestComputer.ComputeDigest(graph1);
var digest2 = _digestComputer.ComputeDigest(graph2);
// Assert - canonical serialization should produce same digest regardless of input order
Assert.Equal(digest1, digest2);
}
private static ReachGraphMinimal CreateSampleGraph() => new()
{
Artifact = new ReachGraphArtifact("test-app", "sha256:abc123", ["linux/amd64"]),
Scope = new ReachGraphScope(["/app/main"], ["prod"]),
Nodes =
[
new ReachGraphNode { Id = "sha256:001", Kind = ReachGraphNodeKind.Function, Ref = "main()" }
],
Edges = [],
Provenance = new ReachGraphProvenance
{
Inputs = new ReachGraphInputs { Sbom = "sha256:sbom123" },
ComputedAt = new DateTimeOffset(2025, 12, 27, 10, 0, 0, TimeSpan.Zero),
Analyzer = new ReachGraphAnalyzer("test", "1.0.0", "sha256:toolchain")
}
};
}