Files
git.stella-ops.org/src/__Libraries/StellaOps.TestKit/Assertions/CanonicalJsonAssert.cs
master bc4318ef97 Add tests for SBOM generation determinism across multiple formats
- 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.
2025-12-23 18:56:12 +02:00

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();
}
}