using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Xunit;
namespace StellaOps.TestKit.Assertions;
///
/// Provides assertions for canonical JSON serialization and determinism testing.
///
///
/// Canonical JSON ensures:
/// - Stable key ordering (alphabetical)
/// - Consistent number formatting
/// - No whitespace variations
/// - UTF-8 encoding
/// - Deterministic output (same input → same bytes)
///
public static class CanonicalJsonAssert
{
///
/// Asserts that the canonical JSON serialization of the value produces the expected SHA-256 hash.
///
/// The value to serialize.
/// The expected SHA-256 hash (lowercase hex string).
public static void HasExpectedHash(T value, string expectedSha256Hex)
{
string actualHash = Canonical.Json.CanonJson.Hash(value);
Assert.Equal(expectedSha256Hex.ToLowerInvariant(), actualHash);
}
///
/// Asserts that two values produce identical canonical JSON.
///
public static void AreCanonicallyEqual(T expected, T actual)
{
byte[] expectedBytes = Canonical.Json.CanonJson.Canonicalize(expected);
byte[] actualBytes = Canonical.Json.CanonJson.Canonicalize(actual);
Assert.Equal(expectedBytes, actualBytes);
}
///
/// Asserts that serializing the value multiple times produces identical bytes (determinism check).
///
public static void IsDeterministic(T value, int iterations = 10)
{
byte[]? baseline = null;
for (int i = 0; i < iterations; i++)
{
byte[] current = Canonical.Json.CanonJson.Canonicalize(value);
if (baseline == null)
{
baseline = current;
}
else
{
Assert.Equal(baseline, current);
}
}
}
///
/// Computes the SHA-256 hash of the canonical JSON and returns it as a lowercase hex string.
///
public static string ComputeCanonicalHash(T value)
{
return Canonical.Json.CanonJson.Hash(value);
}
///
/// Asserts that the canonical JSON matches the expected string (useful for debugging).
///
public static void MatchesJson(T value, string expectedJson)
{
byte[] canonicalBytes = Canonical.Json.CanonJson.Canonicalize(value);
string actualJson = System.Text.Encoding.UTF8.GetString(canonicalBytes);
Assert.Equal(expectedJson, actualJson);
}
///
/// Asserts that the JSON contains the expected key-value pair (deep search).
///
public static void ContainsProperty(T value, string propertyPath, object expectedValue)
{
byte[] canonicalBytes = Canonical.Json.CanonJson.Canonicalize(value);
using var doc = JsonDocument.Parse(canonicalBytes);
JsonElement? element = FindPropertyByPath(doc.RootElement, propertyPath);
Assert.NotNull(element);
// Compare values
string expectedJson = JsonSerializer.Serialize(expectedValue);
string actualJson = element.Value.GetRawText();
Assert.Equal(expectedJson, actualJson);
}
private static JsonElement? FindPropertyByPath(JsonElement root, string path)
{
var parts = path.Split('.');
var current = root;
foreach (var part in parts)
{
if (current.ValueKind != JsonValueKind.Object)
{
return null;
}
if (!current.TryGetProperty(part, out var next))
{
return null;
}
current = next;
}
return current;
}
private static string ComputeSha256Hex(byte[] data)
{
byte[] hash = SHA256.HashData(data);
return Convert.ToHexString(hash).ToLowerInvariant();
}
}