using System.Security.Cryptography; using System.Text; using FluentAssertions; using StellaOps.AirGap.Importer.Reconciliation; namespace StellaOps.AirGap.Importer.Tests.Reconciliation; public sealed class EvidenceDirectoryDiscoveryTests { [Fact] public void Discover_ReturnsDeterministicRelativePathsAndHashes() { var root = Path.Combine(Path.GetTempPath(), "stellaops-evidence-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(root); try { WriteUtf8(Path.Combine(root, "sboms", "a.cdx.json"), "{\"bom\":1}"); WriteUtf8(Path.Combine(root, "attestations", "z.intoto.jsonl.dsig"), "dsse"); WriteUtf8(Path.Combine(root, "vex", "v.openvex.json"), "{\"vex\":true}"); var discovered = EvidenceDirectoryDiscovery.Discover(root); discovered.Should().HaveCount(3); discovered.Select(d => d.RelativePath).Should().Equal( "attestations/z.intoto.jsonl.dsig", "sboms/a.cdx.json", "vex/v.openvex.json"); discovered[0].Kind.Should().Be(EvidenceFileKind.Attestation); discovered[1].Kind.Should().Be(EvidenceFileKind.Sbom); discovered[2].Kind.Should().Be(EvidenceFileKind.Vex); discovered[0].ContentSha256.Should().Be(HashUtf8("dsse")); discovered[1].ContentSha256.Should().Be(HashUtf8("{\"bom\":1}")); discovered[2].ContentSha256.Should().Be(HashUtf8("{\"vex\":true}")); } finally { Directory.Delete(root, recursive: true); } } [Fact] public void Discover_WhenDirectoryMissing_Throws() { var missing = Path.Combine(Path.GetTempPath(), "stellaops-missing-" + Guid.NewGuid().ToString("N")); Action act = () => EvidenceDirectoryDiscovery.Discover(missing); act.Should().Throw(); } private static void WriteUtf8(string path, string content) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); File.WriteAllText(path, content, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false)); } private static string HashUtf8(string content) { using var sha256 = SHA256.Create(); var bytes = Encoding.UTF8.GetBytes(content); var hash = sha256.ComputeHash(bytes); return "sha256:" + Convert.ToHexString(hash).ToLowerInvariant(); } }