- Created `StellaOps.TestKit.Tests` project for unit tests related to determinism. - Implemented `DeterminismManifestTests` to validate deterministic output for canonical bytes and strings, file read/write operations, and error handling for invalid schema versions. - Added `SbomDeterminismTests` to ensure identical inputs produce consistent SBOMs across SPDX 3.0.1 and CycloneDX 1.6/1.7 formats, including parallel execution tests. - Updated project references in `StellaOps.Integration.Determinism` to include the new determinism testing library.
131 lines
4.0 KiB
C#
131 lines
4.0 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using Xunit;
|
|
|
|
namespace StellaOps.TestKit.Assertions;
|
|
|
|
/// <summary>
|
|
/// Provides assertions for canonical JSON serialization and determinism testing.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Canonical JSON ensures:
|
|
/// - Stable key ordering (alphabetical)
|
|
/// - Consistent number formatting
|
|
/// - No whitespace variations
|
|
/// - UTF-8 encoding
|
|
/// - Deterministic output (same input → same bytes)
|
|
/// </remarks>
|
|
public static class CanonicalJsonAssert
|
|
{
|
|
/// <summary>
|
|
/// Asserts that the canonical JSON serialization of the value produces the expected SHA-256 hash.
|
|
/// </summary>
|
|
/// <param name="value">The value to serialize.</param>
|
|
/// <param name="expectedSha256Hex">The expected SHA-256 hash (lowercase hex string).</param>
|
|
public static void HasExpectedHash<T>(T value, string expectedSha256Hex)
|
|
{
|
|
string actualHash = Canonical.Json.CanonJson.Hash(value);
|
|
Assert.Equal(expectedSha256Hex.ToLowerInvariant(), actualHash);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that two values produce identical canonical JSON.
|
|
/// </summary>
|
|
public static void AreCanonicallyEqual<T>(T expected, T actual)
|
|
{
|
|
byte[] expectedBytes = Canonical.Json.CanonJson.Canonicalize(expected);
|
|
byte[] actualBytes = Canonical.Json.CanonJson.Canonicalize(actual);
|
|
|
|
Assert.Equal(expectedBytes, actualBytes);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that serializing the value multiple times produces identical bytes (determinism check).
|
|
/// </summary>
|
|
public static void IsDeterministic<T>(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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Computes the SHA-256 hash of the canonical JSON and returns it as a lowercase hex string.
|
|
/// </summary>
|
|
public static string ComputeCanonicalHash<T>(T value)
|
|
{
|
|
return Canonical.Json.CanonJson.Hash(value);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that the canonical JSON matches the expected string (useful for debugging).
|
|
/// </summary>
|
|
public static void MatchesJson<T>(T value, string expectedJson)
|
|
{
|
|
byte[] canonicalBytes = Canonical.Json.CanonJson.Canonicalize(value);
|
|
string actualJson = System.Text.Encoding.UTF8.GetString(canonicalBytes);
|
|
Assert.Equal(expectedJson, actualJson);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Asserts that the JSON contains the expected key-value pair (deep search).
|
|
/// </summary>
|
|
public static void ContainsProperty<T>(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();
|
|
}
|
|
}
|