sprints work

This commit is contained in:
StellaOps Bot
2025-12-24 21:46:08 +02:00
parent 43e2af88f6
commit b9f71fc7e9
161 changed files with 29566 additions and 527 deletions

View File

@@ -0,0 +1,243 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging.Abstractions;
using Moq;
using StellaOps.Attestor.Envelope;
using StellaOps.Attestor.GraphRoot.Models;
using Xunit;
namespace StellaOps.Attestor.GraphRoot.Tests;
public class GraphRootAttestorTests
{
private readonly Mock<IMerkleRootComputer> _merkleComputerMock;
private readonly EnvelopeSignatureService _signatureService;
private readonly GraphRootAttestor _attestor;
private readonly EnvelopeKey _testKey;
public GraphRootAttestorTests()
{
_merkleComputerMock = new Mock<IMerkleRootComputer>();
_merkleComputerMock.Setup(m => m.Algorithm).Returns("sha256");
_merkleComputerMock
.Setup(m => m.ComputeRoot(It.IsAny<IReadOnlyList<ReadOnlyMemory<byte>>>()))
.Returns(new byte[32]); // 32-byte hash
// Create a real test key for signing (need both private and public for Ed25519)
var privateKey = new byte[64]; // Ed25519 expanded private key is 64 bytes
var publicKey = new byte[32];
Random.Shared.NextBytes(privateKey);
Random.Shared.NextBytes(publicKey);
_testKey = EnvelopeKey.CreateEd25519Signer(privateKey, publicKey, "test-key-id");
_signatureService = new EnvelopeSignatureService();
_attestor = new GraphRootAttestor(
_merkleComputerMock.Object,
_signatureService,
_ => _testKey,
NullLogger<GraphRootAttestor>.Instance);
}
[Fact]
public async Task AttestAsync_ValidRequest_ReturnsResult()
{
// Arrange
var request = CreateValidRequest();
// Act
var result = await _attestor.AttestAsync(request);
// Assert
Assert.NotNull(result);
Assert.NotNull(result.Envelope);
Assert.StartsWith("sha256:", result.RootHash);
Assert.Equal(3, result.NodeCount);
Assert.Equal(2, result.EdgeCount);
}
[Fact]
public async Task AttestAsync_SortsNodeIds()
{
// Arrange
var request = new GraphRootAttestationRequest
{
GraphType = GraphType.DependencyGraph,
NodeIds = new[] { "z-node", "a-node", "m-node" },
EdgeIds = Array.Empty<string>(),
PolicyDigest = "sha256:p",
FeedsDigest = "sha256:f",
ToolchainDigest = "sha256:t",
ParamsDigest = "sha256:pr",
ArtifactDigest = "sha256:a"
};
IReadOnlyList<ReadOnlyMemory<byte>>? capturedLeaves = null;
_merkleComputerMock
.Setup(m => m.ComputeRoot(It.IsAny<IReadOnlyList<ReadOnlyMemory<byte>>>()))
.Callback<IReadOnlyList<ReadOnlyMemory<byte>>>(leaves => capturedLeaves = leaves)
.Returns(new byte[32]);
// Act
await _attestor.AttestAsync(request);
// Assert
Assert.NotNull(capturedLeaves);
// First three leaves should be node IDs in sorted order
var firstNodeId = System.Text.Encoding.UTF8.GetString(capturedLeaves[0].Span);
var secondNodeId = System.Text.Encoding.UTF8.GetString(capturedLeaves[1].Span);
var thirdNodeId = System.Text.Encoding.UTF8.GetString(capturedLeaves[2].Span);
Assert.Equal("a-node", firstNodeId);
Assert.Equal("m-node", secondNodeId);
Assert.Equal("z-node", thirdNodeId);
}
[Fact]
public async Task AttestAsync_SortsEdgeIds()
{
// Arrange
var request = new GraphRootAttestationRequest
{
GraphType = GraphType.DependencyGraph,
NodeIds = Array.Empty<string>(),
EdgeIds = new[] { "z-edge", "a-edge" },
PolicyDigest = "sha256:p",
FeedsDigest = "sha256:f",
ToolchainDigest = "sha256:t",
ParamsDigest = "sha256:pr",
ArtifactDigest = "sha256:a"
};
IReadOnlyList<ReadOnlyMemory<byte>>? capturedLeaves = null;
_merkleComputerMock
.Setup(m => m.ComputeRoot(It.IsAny<IReadOnlyList<ReadOnlyMemory<byte>>>()))
.Callback<IReadOnlyList<ReadOnlyMemory<byte>>>(leaves => capturedLeaves = leaves)
.Returns(new byte[32]);
// Act
await _attestor.AttestAsync(request);
// Assert
Assert.NotNull(capturedLeaves);
// First two leaves should be edge IDs in sorted order
var firstEdgeId = System.Text.Encoding.UTF8.GetString(capturedLeaves[0].Span);
var secondEdgeId = System.Text.Encoding.UTF8.GetString(capturedLeaves[1].Span);
Assert.Equal("a-edge", firstEdgeId);
Assert.Equal("z-edge", secondEdgeId);
}
[Fact]
public async Task AttestAsync_IncludesInputDigestsInLeaves()
{
// Arrange
var request = new GraphRootAttestationRequest
{
GraphType = GraphType.DependencyGraph,
NodeIds = Array.Empty<string>(),
EdgeIds = Array.Empty<string>(),
PolicyDigest = "sha256:policy",
FeedsDigest = "sha256:feeds",
ToolchainDigest = "sha256:toolchain",
ParamsDigest = "sha256:params",
ArtifactDigest = "sha256:artifact"
};
IReadOnlyList<ReadOnlyMemory<byte>>? capturedLeaves = null;
_merkleComputerMock
.Setup(m => m.ComputeRoot(It.IsAny<IReadOnlyList<ReadOnlyMemory<byte>>>()))
.Callback<IReadOnlyList<ReadOnlyMemory<byte>>>(leaves => capturedLeaves = leaves)
.Returns(new byte[32]);
// Act
await _attestor.AttestAsync(request);
// Assert
Assert.NotNull(capturedLeaves);
Assert.Equal(4, capturedLeaves.Count); // Just the 4 input digests
var digestStrings = capturedLeaves.Select(l => System.Text.Encoding.UTF8.GetString(l.Span)).ToList();
Assert.Contains("sha256:policy", digestStrings);
Assert.Contains("sha256:feeds", digestStrings);
Assert.Contains("sha256:toolchain", digestStrings);
Assert.Contains("sha256:params", digestStrings);
}
[Fact]
public async Task AttestAsync_NullRequest_ThrowsArgumentNullException()
{
// Act & Assert
await Assert.ThrowsAsync<ArgumentNullException>(() => _attestor.AttestAsync(null!));
}
[Fact]
public async Task AttestAsync_KeyResolverReturnsNull_ThrowsInvalidOperationException()
{
// Arrange
var attestorWithNullKey = new GraphRootAttestor(
_merkleComputerMock.Object,
_signatureService,
_ => null,
NullLogger<GraphRootAttestor>.Instance);
var request = CreateValidRequest();
// Act & Assert
var ex = await Assert.ThrowsAsync<InvalidOperationException>(() => attestorWithNullKey.AttestAsync(request));
Assert.Contains("Unable to resolve signing key", ex.Message);
}
[Fact]
public async Task AttestAsync_CancellationRequested_ThrowsOperationCanceledException()
{
// Arrange
var request = CreateValidRequest();
var cts = new CancellationTokenSource();
cts.Cancel();
// Act & Assert
await Assert.ThrowsAsync<OperationCanceledException>(() => _attestor.AttestAsync(request, cts.Token));
}
[Fact]
public async Task AttestAsync_ReturnsCorrectGraphType()
{
// Arrange
var request = new GraphRootAttestationRequest
{
GraphType = GraphType.ReachabilityGraph,
NodeIds = new[] { "n1" },
EdgeIds = Array.Empty<string>(),
PolicyDigest = "sha256:p",
FeedsDigest = "sha256:f",
ToolchainDigest = "sha256:t",
ParamsDigest = "sha256:pr",
ArtifactDigest = "sha256:a"
};
// Act
var result = await _attestor.AttestAsync(request);
// Assert
var attestation = JsonSerializer.Deserialize<GraphRootAttestation>(result.Envelope.Payload.Span);
Assert.NotNull(attestation);
Assert.Equal("ReachabilityGraph", attestation.Predicate.GraphType);
}
private static GraphRootAttestationRequest CreateValidRequest()
{
return new GraphRootAttestationRequest
{
GraphType = GraphType.DependencyGraph,
NodeIds = new[] { "node-1", "node-2", "node-3" },
EdgeIds = new[] { "edge-1", "edge-2" },
PolicyDigest = "sha256:policy123",
FeedsDigest = "sha256:feeds456",
ToolchainDigest = "sha256:tools789",
ParamsDigest = "sha256:params012",
ArtifactDigest = "sha256:artifact345"
};
}
}

View File

@@ -0,0 +1,226 @@
using System;
using System.Collections.Generic;
using StellaOps.Attestor.GraphRoot.Models;
using Xunit;
namespace StellaOps.Attestor.GraphRoot.Tests;
public class GraphRootModelsTests
{
[Fact]
public void GraphRootAttestationRequest_RequiredProperties_Set()
{
// Arrange & Act
var request = new GraphRootAttestationRequest
{
GraphType = GraphType.DependencyGraph,
NodeIds = new[] { "node-1", "node-2" },
EdgeIds = new[] { "edge-1" },
PolicyDigest = "sha256:abc123",
FeedsDigest = "sha256:def456",
ToolchainDigest = "sha256:ghi789",
ParamsDigest = "sha256:jkl012",
ArtifactDigest = "sha256:artifact123"
};
// Assert
Assert.Equal(GraphType.DependencyGraph, request.GraphType);
Assert.Equal(2, request.NodeIds.Count);
Assert.Single(request.EdgeIds);
Assert.Equal("sha256:abc123", request.PolicyDigest);
Assert.False(request.PublishToRekor);
Assert.Null(request.SigningKeyId);
Assert.Empty(request.EvidenceIds);
}
[Fact]
public void GraphRootAttestationRequest_OptionalProperties_HaveDefaults()
{
// Arrange & Act
var request = new GraphRootAttestationRequest
{
GraphType = GraphType.CallGraph,
NodeIds = Array.Empty<string>(),
EdgeIds = Array.Empty<string>(),
PolicyDigest = "sha256:p",
FeedsDigest = "sha256:f",
ToolchainDigest = "sha256:t",
ParamsDigest = "sha256:pr",
ArtifactDigest = "sha256:a"
};
// Assert
Assert.False(request.PublishToRekor);
Assert.Null(request.SigningKeyId);
Assert.Empty(request.EvidenceIds);
}
[Fact]
public void GraphRootPredicate_RequiredProperties_Set()
{
// Arrange & Act
var predicate = new GraphRootPredicate
{
GraphType = "DependencyGraph",
RootHash = "sha256:abc123",
NodeCount = 10,
EdgeCount = 15,
NodeIds = new[] { "n1", "n2" },
EdgeIds = new[] { "e1" },
Inputs = new GraphInputDigests
{
PolicyDigest = "sha256:p",
FeedsDigest = "sha256:f",
ToolchainDigest = "sha256:t",
ParamsDigest = "sha256:pr"
},
CanonVersion = "stella:canon:v1",
ComputedAt = DateTimeOffset.UtcNow,
ComputedBy = "test",
ComputedByVersion = "1.0.0"
};
// Assert
Assert.Equal("DependencyGraph", predicate.GraphType);
Assert.Equal("sha256:abc123", predicate.RootHash);
Assert.Equal("sha256", predicate.RootAlgorithm);
Assert.Equal(10, predicate.NodeCount);
Assert.Equal(15, predicate.EdgeCount);
}
[Fact]
public void GraphRootAttestation_HasCorrectDefaults()
{
// Arrange & Act
var attestation = new GraphRootAttestation
{
Subject = new[]
{
new GraphRootSubject
{
Name = "sha256:root",
Digest = new Dictionary<string, string> { ["sha256"] = "root" }
}
},
Predicate = new GraphRootPredicate
{
GraphType = "Test",
RootHash = "sha256:root",
NodeCount = 1,
EdgeCount = 0,
NodeIds = Array.Empty<string>(),
EdgeIds = Array.Empty<string>(),
Inputs = new GraphInputDigests
{
PolicyDigest = "sha256:p",
FeedsDigest = "sha256:f",
ToolchainDigest = "sha256:t",
ParamsDigest = "sha256:pr"
},
CanonVersion = "v1",
ComputedAt = DateTimeOffset.UtcNow,
ComputedBy = "test",
ComputedByVersion = "1.0"
}
};
// Assert
Assert.Equal("https://in-toto.io/Statement/v1", attestation.Type);
Assert.Equal(GraphRootPredicateTypes.GraphRootV1, attestation.PredicateType);
}
[Fact]
public void GraphRootPredicateTypes_HasCorrectValue()
{
Assert.Equal("https://stella-ops.org/attestation/graph-root/v1", GraphRootPredicateTypes.GraphRootV1);
}
[Fact]
public void GraphRootVerificationResult_ValidResult()
{
// Arrange & Act
var result = new GraphRootVerificationResult
{
IsValid = true,
ExpectedRoot = "sha256:abc",
ComputedRoot = "sha256:abc",
NodeCount = 5,
EdgeCount = 3
};
// Assert
Assert.True(result.IsValid);
Assert.Null(result.FailureReason);
Assert.Equal("sha256:abc", result.ExpectedRoot);
Assert.Equal(5, result.NodeCount);
}
[Fact]
public void GraphRootVerificationResult_InvalidResult_HasReason()
{
// Arrange & Act
var result = new GraphRootVerificationResult
{
IsValid = false,
FailureReason = "Root mismatch",
ExpectedRoot = "sha256:abc",
ComputedRoot = "sha256:xyz"
};
// Assert
Assert.False(result.IsValid);
Assert.Equal("Root mismatch", result.FailureReason);
Assert.NotEqual(result.ExpectedRoot, result.ComputedRoot);
}
[Fact]
public void GraphNodeData_RequiredProperty()
{
// Arrange & Act
var node = new GraphNodeData
{
NodeId = "node-123",
Content = "optional content"
};
// Assert
Assert.Equal("node-123", node.NodeId);
Assert.Equal("optional content", node.Content);
}
[Fact]
public void GraphEdgeData_AllProperties()
{
// Arrange & Act
var edge = new GraphEdgeData
{
EdgeId = "edge-1",
SourceNodeId = "source-node",
TargetNodeId = "target-node"
};
// Assert
Assert.Equal("edge-1", edge.EdgeId);
Assert.Equal("source-node", edge.SourceNodeId);
Assert.Equal("target-node", edge.TargetNodeId);
}
[Fact]
public void GraphInputDigests_AllDigests()
{
// Arrange & Act
var digests = new GraphInputDigests
{
PolicyDigest = "sha256:policy",
FeedsDigest = "sha256:feeds",
ToolchainDigest = "sha256:toolchain",
ParamsDigest = "sha256:params"
};
// Assert
Assert.Equal("sha256:policy", digests.PolicyDigest);
Assert.Equal("sha256:feeds", digests.FeedsDigest);
Assert.Equal("sha256:toolchain", digests.ToolchainDigest);
Assert.Equal("sha256:params", digests.ParamsDigest);
}
}

View File

@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
using Xunit;
namespace StellaOps.Attestor.GraphRoot.Tests;
public class Sha256MerkleRootComputerTests
{
private readonly Sha256MerkleRootComputer _computer = new();
[Fact]
public void Algorithm_ReturnsSha256()
{
Assert.Equal("sha256", _computer.Algorithm);
}
[Fact]
public void ComputeRoot_SingleLeaf_ReturnsHash()
{
// Arrange
var leaf = "test-node-1"u8.ToArray();
var leaves = new List<ReadOnlyMemory<byte>> { leaf };
// Act
var root = _computer.ComputeRoot(leaves);
// Assert
Assert.NotNull(root);
Assert.Equal(32, root.Length); // SHA-256 produces 32 bytes
}
[Fact]
public void ComputeRoot_TwoLeaves_CombinesCorrectly()
{
// Arrange
var leaf1 = "node-1"u8.ToArray();
var leaf2 = "node-2"u8.ToArray();
var leaves = new List<ReadOnlyMemory<byte>> { leaf1, leaf2 };
// Act
var root = _computer.ComputeRoot(leaves);
// Assert
Assert.NotNull(root);
Assert.Equal(32, root.Length);
}
[Fact]
public void ComputeRoot_OddLeaves_DuplicatesLast()
{
// Arrange
var leaves = new List<ReadOnlyMemory<byte>>
{
"node-1"u8.ToArray(),
"node-2"u8.ToArray(),
"node-3"u8.ToArray()
};
// Act
var root = _computer.ComputeRoot(leaves);
// Assert
Assert.NotNull(root);
Assert.Equal(32, root.Length);
}
[Fact]
public void ComputeRoot_Deterministic_SameInputSameOutput()
{
// Arrange
var leaves = new List<ReadOnlyMemory<byte>>
{
"node-a"u8.ToArray(),
"node-b"u8.ToArray(),
"edge-1"u8.ToArray(),
"edge-2"u8.ToArray()
};
// Act
var root1 = _computer.ComputeRoot(leaves);
var root2 = _computer.ComputeRoot(leaves);
// Assert
Assert.Equal(root1, root2);
}
[Fact]
public void ComputeRoot_DifferentInputs_DifferentOutputs()
{
// Arrange
var leaves1 = new List<ReadOnlyMemory<byte>> { "node-1"u8.ToArray() };
var leaves2 = new List<ReadOnlyMemory<byte>> { "node-2"u8.ToArray() };
// Act
var root1 = _computer.ComputeRoot(leaves1);
var root2 = _computer.ComputeRoot(leaves2);
// Assert
Assert.NotEqual(root1, root2);
}
[Fact]
public void ComputeRoot_OrderMatters()
{
// Arrange
var leavesAB = new List<ReadOnlyMemory<byte>>
{
"node-a"u8.ToArray(),
"node-b"u8.ToArray()
};
var leavesBA = new List<ReadOnlyMemory<byte>>
{
"node-b"u8.ToArray(),
"node-a"u8.ToArray()
};
// Act
var rootAB = _computer.ComputeRoot(leavesAB);
var rootBA = _computer.ComputeRoot(leavesBA);
// Assert - order should matter for Merkle trees
Assert.NotEqual(rootAB, rootBA);
}
[Fact]
public void ComputeRoot_EmptyList_ThrowsArgumentException()
{
// Arrange
var leaves = new List<ReadOnlyMemory<byte>>();
// Act & Assert
Assert.Throws<ArgumentException>(() => _computer.ComputeRoot(leaves));
}
[Fact]
public void ComputeRoot_NullInput_ThrowsArgumentNullException()
{
// Act & Assert
Assert.Throws<ArgumentNullException>(() => _computer.ComputeRoot(null!));
}
[Fact]
public void ComputeRoot_LargeTree_HandlesCorrectly()
{
// Arrange - create 100 leaves
var leaves = new List<ReadOnlyMemory<byte>>();
for (var i = 0; i < 100; i++)
{
leaves.Add(System.Text.Encoding.UTF8.GetBytes($"node-{i:D4}"));
}
// Act
var root = _computer.ComputeRoot(leaves);
// Assert
Assert.NotNull(root);
Assert.Equal(32, root.Length);
}
[Fact]
public void ComputeRoot_PowerOfTwo_HandlesCorrectly()
{
// Arrange - 8 leaves (power of 2)
var leaves = new List<ReadOnlyMemory<byte>>();
for (var i = 0; i < 8; i++)
{
leaves.Add(System.Text.Encoding.UTF8.GetBytes($"node-{i}"));
}
// Act
var root = _computer.ComputeRoot(leaves);
// Assert
Assert.NotNull(root);
Assert.Equal(32, root.Length);
}
}

View File

@@ -0,0 +1,30 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>StellaOps.Attestor.GraphRoot.Tests</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="Moq" Version="4.20.72" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.1">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
<PackageReference Include="coverlet.collector" Version="6.0.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
<PrivateAssets>all</PrivateAssets>
</PackageReference>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\StellaOps.Attestor.GraphRoot\StellaOps.Attestor.GraphRoot.csproj" />
</ItemGroup>
</Project>