215 lines
6.2 KiB
C#
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")
|
|
}
|
|
};
|
|
}
|