using FluentAssertions; using StellaOps.Cryptography; using StellaOps.Policy.Snapshots; using Xunit; namespace StellaOps.Policy.Tests.Snapshots; public sealed class SnapshotBuilderTests { private readonly ICryptoHash _hasher = DefaultCryptoHash.CreateForTests(); [Fact] public void Build_ValidInputs_CreatesManifest() { var builder = new SnapshotBuilder(_hasher) .WithEngine("test", "1.0", "abc123") .WithPolicy("policy-1", "sha256:xxx") .WithScoring("scoring-1", "sha256:yyy") .WithAdvisoryFeed("nvd", "2025-12-21", "sha256:zzz"); var manifest = builder.Build(); manifest.SnapshotId.Should().StartWith("ksm:sha256:"); manifest.SnapshotId.Length.Should().Be("ksm:sha256:".Length + 64); // ksm:sha256: + 64 hex chars manifest.Sources.Should().HaveCount(1); manifest.Engine.Name.Should().Be("test"); manifest.Engine.Version.Should().Be("1.0"); manifest.Engine.Commit.Should().Be("abc123"); manifest.Policy.PolicyId.Should().Be("policy-1"); manifest.Scoring.RulesId.Should().Be("scoring-1"); } [Fact] public void Build_MissingEngine_Throws() { var builder = new SnapshotBuilder(_hasher) .WithPolicy("policy-1", "sha256:xxx") .WithScoring("scoring-1", "sha256:yyy") .WithAdvisoryFeed("nvd", "2025-12-21", "sha256:zzz"); var act = () => builder.Build(); act.Should().Throw() .WithMessage("*Engine*"); } [Fact] public void Build_MissingPolicy_Throws() { var builder = new SnapshotBuilder(_hasher) .WithEngine("test", "1.0", "abc123") .WithScoring("scoring-1", "sha256:yyy") .WithAdvisoryFeed("nvd", "2025-12-21", "sha256:zzz"); var act = () => builder.Build(); act.Should().Throw() .WithMessage("*Policy*"); } [Fact] public void Build_MissingScoring_Throws() { var builder = new SnapshotBuilder(_hasher) .WithEngine("test", "1.0", "abc123") .WithPolicy("policy-1", "sha256:xxx") .WithAdvisoryFeed("nvd", "2025-12-21", "sha256:zzz"); var act = () => builder.Build(); act.Should().Throw() .WithMessage("*Scoring*"); } [Fact] public void Build_NoSources_Throws() { var builder = new SnapshotBuilder(_hasher) .WithEngine("test", "1.0", "abc123") .WithPolicy("policy-1", "sha256:xxx") .WithScoring("scoring-1", "sha256:yyy"); var act = () => builder.Build(); act.Should().Throw() .WithMessage("*source*"); } [Fact] public void Build_MultipleSources_OrderedByName() { var builder = new SnapshotBuilder(_hasher) .WithEngine("test", "1.0", "abc123") .WithPolicy("policy-1", "sha256:xxx") .WithScoring("scoring-1", "sha256:yyy") .WithAdvisoryFeed("z-source", "2025-12-21", "sha256:aaa") .WithAdvisoryFeed("a-source", "2025-12-21", "sha256:bbb") .WithAdvisoryFeed("m-source", "2025-12-21", "sha256:ccc"); var manifest = builder.Build(); manifest.Sources.Should().HaveCount(3); manifest.Sources[0].Name.Should().Be("a-source"); manifest.Sources[1].Name.Should().Be("m-source"); manifest.Sources[2].Name.Should().Be("z-source"); } [Fact] public void Build_WithPlugins_IncludesPlugins() { var builder = new SnapshotBuilder(_hasher) .WithEngine("test", "1.0", "abc123") .WithPolicy("policy-1", "sha256:xxx") .WithScoring("scoring-1", "sha256:yyy") .WithAdvisoryFeed("nvd", "2025-12-21", "sha256:zzz") .WithPlugin("reachability", "2.0", "analyzer") .WithPlugin("sbom", "1.5", "analyzer"); var manifest = builder.Build(); manifest.Plugins.Should().HaveCount(2); manifest.Plugins[0].Name.Should().Be("reachability"); manifest.Plugins[1].Name.Should().Be("sbom"); } [Fact] public void Build_WithTrust_IncludesTrust() { var builder = new SnapshotBuilder(_hasher) .WithEngine("test", "1.0", "abc123") .WithPolicy("policy-1", "sha256:xxx") .WithScoring("scoring-1", "sha256:yyy") .WithAdvisoryFeed("nvd", "2025-12-21", "sha256:zzz") .WithTrust("trust-bundle", "sha256:trust123"); var manifest = builder.Build(); manifest.Trust.Should().NotBeNull(); manifest.Trust!.BundleId.Should().Be("trust-bundle"); manifest.Trust.Digest.Should().Be("sha256:trust123"); } [Fact] public void Build_CaptureCurrentEnvironment_SetsEnvironment() { var builder = new SnapshotBuilder(_hasher) .WithEngine("test", "1.0", "abc123") .WithPolicy("policy-1", "sha256:xxx") .WithScoring("scoring-1", "sha256:yyy") .WithAdvisoryFeed("nvd", "2025-12-21", "sha256:zzz") .CaptureCurrentEnvironment(); var manifest = builder.Build(); manifest.Environment.Should().NotBeNull(); manifest.Environment!.Platform.Should().NotBeNullOrEmpty(); manifest.Environment.Locale.Should().NotBeNullOrEmpty(); } }