save progress
This commit is contained in:
323
src/__Libraries/StellaOps.Replay.Core.Tests/ReplayProofTests.cs
Normal file
323
src/__Libraries/StellaOps.Replay.Core.Tests/ReplayProofTests.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user