save progress

This commit is contained in:
StellaOps Bot
2026-01-06 09:42:02 +02:00
parent 94d68bee8b
commit 37e11918e0
443 changed files with 85863 additions and 897 deletions

View File

@@ -0,0 +1,323 @@
// <copyright file="ReplayProofTests.cs" company="Stella Operations">
// Copyright (c) Stella Operations. Licensed under AGPL-3.0-or-later.
// </copyright>
using System.Collections.Immutable;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using FluentAssertions;
using StellaOps.Replay.Core.Models;
using Xunit;
namespace StellaOps.Replay.Core.Tests;
/// <summary>
/// Unit tests for ReplayProof model and compact string generation.
/// Sprint: SPRINT_20260105_002_001_REPLAY, Tasks RPL-011 through RPL-014.
/// </summary>
[Trait("Category", "Unit")]
public class ReplayProofTests
{
private static readonly DateTimeOffset FixedTimestamp = new(2026, 1, 5, 12, 0, 0, TimeSpan.Zero);
[Fact]
public void FromExecutionResult_CreatesValidProof()
{
// Arrange & Act
var proof = ReplayProof.FromExecutionResult(
bundleHash: "sha256:abc123",
policyVersion: "1.0.0",
verdictRoot: "sha256:def456",
verdictMatches: true,
durationMs: 150,
replayedAt: FixedTimestamp,
engineVersion: "1.0.0",
artifactDigest: "sha256:image123",
signatureVerified: true,
signatureKeyId: "key-001");
// Assert
proof.BundleHash.Should().Be("sha256:abc123");
proof.PolicyVersion.Should().Be("1.0.0");
proof.VerdictRoot.Should().Be("sha256:def456");
proof.VerdictMatches.Should().BeTrue();
proof.DurationMs.Should().Be(150);
proof.ReplayedAt.Should().Be(FixedTimestamp);
proof.EngineVersion.Should().Be("1.0.0");
proof.ArtifactDigest.Should().Be("sha256:image123");
proof.SignatureVerified.Should().BeTrue();
proof.SignatureKeyId.Should().Be("key-001");
}
[Fact]
public void ToCompactString_GeneratesCorrectFormat()
{
// Arrange
var proof = CreateTestProof();
// Act
var compact = proof.ToCompactString();
// Assert
compact.Should().StartWith("replay-proof:");
compact.Should().HaveLength("replay-proof:".Length + 64); // SHA-256 hex = 64 chars
}
[Fact]
public void ToCompactString_IsDeterministic()
{
// Arrange
var proof1 = CreateTestProof();
var proof2 = CreateTestProof();
// Act
var compact1 = proof1.ToCompactString();
var compact2 = proof2.ToCompactString();
// Assert
compact1.Should().Be(compact2, "same inputs should produce same compact proof");
}
[Fact]
public void ToCanonicalJson_SortsKeysDeterministically()
{
// Arrange
var proof = CreateTestProof();
// Act
var json = proof.ToCanonicalJson();
// Assert - Keys should appear in alphabetical order
var keys = ExtractJsonKeys(json);
keys.Should().BeInAscendingOrder(StringComparer.Ordinal);
}
[Fact]
public void ToCanonicalJson_ExcludesNullValues()
{
// Arrange
var proof = ReplayProof.FromExecutionResult(
bundleHash: "sha256:abc123",
policyVersion: "1.0.0",
verdictRoot: "sha256:def456",
verdictMatches: true,
durationMs: 150,
replayedAt: FixedTimestamp,
engineVersion: "1.0.0");
// Act
var json = proof.ToCanonicalJson();
// Assert - Should not contain null values
json.Should().NotContain("null");
json.Should().NotContain("artifactDigest"); // Not set, so excluded
json.Should().NotContain("signatureVerified"); // Not set, so excluded
json.Should().NotContain("signatureKeyId"); // Not set, so excluded
}
[Fact]
public void ToCanonicalJson_FormatsTimestampCorrectly()
{
// Arrange
var proof = CreateTestProof();
// Act
var json = proof.ToCanonicalJson();
// Assert - ISO 8601 UTC format
json.Should().Contain("2026-01-05T12:00:00.000Z");
}
[Fact]
public void ValidateCompactString_ReturnsTrueForValidProof()
{
// Arrange
var proof = CreateTestProof();
var compact = proof.ToCompactString();
var canonicalJson = proof.ToCanonicalJson();
// Act
var isValid = ReplayProof.ValidateCompactString(compact, canonicalJson);
// Assert
isValid.Should().BeTrue();
}
[Fact]
public void ValidateCompactString_ReturnsFalseForTamperedJson()
{
// Arrange
var proof = CreateTestProof();
var compact = proof.ToCompactString();
var tamperedJson = proof.ToCanonicalJson().Replace("1.0.0", "2.0.0");
// Act
var isValid = ReplayProof.ValidateCompactString(compact, tamperedJson);
// Assert
isValid.Should().BeFalse("tampered JSON should not validate");
}
[Fact]
public void ValidateCompactString_ReturnsFalseForInvalidPrefix()
{
// Arrange
var canonicalJson = CreateTestProof().ToCanonicalJson();
// Act
var isValid = ReplayProof.ValidateCompactString("invalid-proof:abc123", canonicalJson);
// Assert
isValid.Should().BeFalse("invalid prefix should not validate");
}
[Fact]
public void ValidateCompactString_ReturnsFalseForEmptyInputs()
{
// Act & Assert
ReplayProof.ValidateCompactString("", "{}").Should().BeFalse();
ReplayProof.ValidateCompactString("replay-proof:abc", "").Should().BeFalse();
ReplayProof.ValidateCompactString(null!, "{}").Should().BeFalse();
ReplayProof.ValidateCompactString("replay-proof:abc", null!).Should().BeFalse();
}
[Fact]
public void ToCanonicalJson_IncludesMetadataWhenPresent()
{
// Arrange
var proof = ReplayProof.FromExecutionResult(
bundleHash: "sha256:abc123",
policyVersion: "1.0.0",
verdictRoot: "sha256:def456",
verdictMatches: true,
durationMs: 150,
replayedAt: FixedTimestamp,
engineVersion: "1.0.0",
metadata: ImmutableDictionary<string, string>.Empty
.Add("tenant", "acme-corp")
.Add("project", "web-app"));
// Act
var json = proof.ToCanonicalJson();
// Assert
json.Should().Contain("metadata");
json.Should().Contain("tenant");
json.Should().Contain("acme-corp");
json.Should().Contain("project");
json.Should().Contain("web-app");
}
[Fact]
public void ToCanonicalJson_SortsMetadataKeys()
{
// Arrange
var proof = ReplayProof.FromExecutionResult(
bundleHash: "sha256:abc123",
policyVersion: "1.0.0",
verdictRoot: "sha256:def456",
verdictMatches: true,
durationMs: 150,
replayedAt: FixedTimestamp,
engineVersion: "1.0.0",
metadata: ImmutableDictionary<string, string>.Empty
.Add("zebra", "z-value")
.Add("alpha", "a-value")
.Add("mike", "m-value"));
// Act
var json = proof.ToCanonicalJson();
// Assert - Metadata keys should be in alphabetical order
var alphaPos = json.IndexOf("alpha", StringComparison.Ordinal);
var mikePos = json.IndexOf("mike", StringComparison.Ordinal);
var zebraPos = json.IndexOf("zebra", StringComparison.Ordinal);
alphaPos.Should().BeLessThan(mikePos);
mikePos.Should().BeLessThan(zebraPos);
}
[Fact]
public void FromExecutionResult_ThrowsOnNullRequiredParams()
{
// Act & Assert
var act1 = () => ReplayProof.FromExecutionResult(
bundleHash: null!,
policyVersion: "1.0.0",
verdictRoot: "sha256:def456",
verdictMatches: true,
durationMs: 150,
replayedAt: FixedTimestamp,
engineVersion: "1.0.0");
act1.Should().Throw<ArgumentNullException>().WithParameterName("bundleHash");
var act2 = () => ReplayProof.FromExecutionResult(
bundleHash: "sha256:abc123",
policyVersion: null!,
verdictRoot: "sha256:def456",
verdictMatches: true,
durationMs: 150,
replayedAt: FixedTimestamp,
engineVersion: "1.0.0");
act2.Should().Throw<ArgumentNullException>().WithParameterName("policyVersion");
var act3 = () => ReplayProof.FromExecutionResult(
bundleHash: "sha256:abc123",
policyVersion: "1.0.0",
verdictRoot: null!,
verdictMatches: true,
durationMs: 150,
replayedAt: FixedTimestamp,
engineVersion: "1.0.0");
act3.Should().Throw<ArgumentNullException>().WithParameterName("verdictRoot");
var act4 = () => ReplayProof.FromExecutionResult(
bundleHash: "sha256:abc123",
policyVersion: "1.0.0",
verdictRoot: "sha256:def456",
verdictMatches: true,
durationMs: 150,
replayedAt: FixedTimestamp,
engineVersion: null!);
act4.Should().Throw<ArgumentNullException>().WithParameterName("engineVersion");
}
[Fact]
public void SchemaVersion_DefaultsTo1_0_0()
{
// Arrange & Act
var proof = CreateTestProof();
// Assert
proof.SchemaVersion.Should().Be("1.0.0");
}
private static ReplayProof CreateTestProof()
{
return ReplayProof.FromExecutionResult(
bundleHash: "sha256:abc123def456",
policyVersion: "1.0.0",
verdictRoot: "sha256:verdict789",
verdictMatches: true,
durationMs: 150,
replayedAt: FixedTimestamp,
engineVersion: "1.0.0",
artifactDigest: "sha256:image123",
signatureVerified: true,
signatureKeyId: "key-001");
}
private static List<string> ExtractJsonKeys(string json)
{
var keys = new List<string>();
using var doc = JsonDocument.Parse(json);
foreach (var prop in doc.RootElement.EnumerateObject())
{
keys.Add(prop.Name);
}
return keys;
}
}