using System.Linq; using System.Security.Cryptography; using System.Text; using System.Text.Json; using Xunit; using StellaOps.TestKit; namespace StellaOps.EvidenceLocker.Tests; /// /// Golden fixture tests for evidence bundle integrity verification. /// These tests verify that checksum/hash computation logic works correctly. /// public sealed class GoldenFixturesTests { private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web); [Trait("Category", TestCategories.Unit)] [Fact] public void SealedBundle_ComputedHashMatchesRoot() { // Arrange - Create a minimal bundle structure var entries = new[] { new { canonicalPath = "artifacts/sbom.json", sha256 = "a5b8e9c4f3d2e1b0a7c6d5e4f3c2b1a0e9d8c7b6a5f4e3d2c1b0a9f8e7d6c5b4" }, new { canonicalPath = "artifacts/provenance.json", sha256 = "b6c9d0e5f4a3b2c1d0e9f8a7b6c5d4e3f2a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7" } }; // Act - Compute the merkle root by hashing entries var entryHashes = entries.Select(e => e.sha256).OrderBy(h => h).ToArray(); var concatenated = string.Join("", entryHashes); var merkleRoot = Convert.ToHexString(SHA256.HashData(Encoding.UTF8.GetBytes(concatenated))).ToLowerInvariant(); // Assert - Should be able to verify the root was computed from entries Assert.NotEmpty(merkleRoot); Assert.Equal(64, merkleRoot.Length); // SHA256 produces 64 hex chars } [Trait("Category", TestCategories.Unit)] [Fact] public void PortableBundle_RedactionRemovesTenantInfo() { // Arrange - Create a bundle with tenant info var originalBundle = JsonSerializer.Serialize(new { bundleId = "test-bundle", tenantId = "secret-tenant-123", tenantName = "Acme Corp", data = new { value = "public" } }, JsonOptions); // Act - Simulate redaction by removing tenant fields using var doc = JsonDocument.Parse(originalBundle); var redactedData = new Dictionary { ["bundleId"] = doc.RootElement.GetProperty("bundleId").GetString(), ["data"] = new { value = "public" } }; var redactedBundle = JsonSerializer.Serialize(redactedData, JsonOptions); // Assert - Redacted bundle should not contain tenant info Assert.Contains("bundleId", redactedBundle); Assert.DoesNotContain("tenant", redactedBundle, StringComparison.OrdinalIgnoreCase); } [Trait("Category", TestCategories.Unit)] [Fact] public void ReplayRecord_DigestMatchesContent() { // Arrange - Create sample replay record var replayContent = "{\"eventId\":\"evt-001\",\"timestamp\":\"2026-01-22T12:00:00Z\",\"action\":\"promote\"}\n" + "{\"eventId\":\"evt-002\",\"timestamp\":\"2026-01-22T12:01:00Z\",\"action\":\"approve\"}\n"; var contentBytes = Encoding.UTF8.GetBytes(replayContent); // Act - Compute digest var computedHash = "sha256:" + Convert.ToHexString(SHA256.HashData(contentBytes)).ToLowerInvariant(); // Assert - Digest should match expected format Assert.StartsWith("sha256:", computedHash); Assert.Equal(71, computedHash.Length); // "sha256:" (7) + 64 hex chars // Verify digest is deterministic var recomputedHash = "sha256:" + Convert.ToHexString(SHA256.HashData(contentBytes)).ToLowerInvariant(); Assert.Equal(computedHash, recomputedHash); } }