// Licensed under BUSL-1.1. Copyright (C) 2026 StellaOps Contributors. using System.Collections.Immutable; using FluentAssertions; using Xunit; namespace StellaOps.BinaryIndex.Diff.Tests.Unit; [Trait("Category", "Unit")] public sealed class PatchDiffModelTests { [Fact] public void PatchDiffResult_NoPatchDetected_CreatesCorrectResult() { // Arrange var goldenSetId = "CVE-2024-1234"; var goldenSetDigest = "sha256:abcd1234"; var binaryDigest = "sha256:same1234"; var comparedAt = DateTimeOffset.UtcNow; var duration = TimeSpan.FromMilliseconds(100); var options = DiffOptions.Default; // Act var result = PatchDiffResult.NoPatchDetected( goldenSetId, goldenSetDigest, binaryDigest, comparedAt, duration, options); // Assert result.GoldenSetId.Should().Be(goldenSetId); result.Verdict.Should().Be(PatchVerdict.NoPatchDetected); result.Confidence.Should().Be(1.0m); result.PreBinaryDigest.Should().Be(binaryDigest); result.PostBinaryDigest.Should().Be(binaryDigest); result.Evidence.Should().HaveCount(1); result.Evidence[0].Type.Should().Be(DiffEvidenceType.IdenticalBinaries); } [Theory] [InlineData(PatchVerdict.Fixed)] [InlineData(PatchVerdict.PartialFix)] [InlineData(PatchVerdict.StillVulnerable)] [InlineData(PatchVerdict.Inconclusive)] [InlineData(PatchVerdict.NoPatchDetected)] public void PatchVerdict_AllValuesAreDefined(PatchVerdict verdict) { // Assert Enum.IsDefined(verdict).Should().BeTrue(); } [Fact] public void FunctionDiffResult_FunctionRemoved_CreatesCorrectResult() { // Act var result = FunctionDiffResult.FunctionRemoved("vulnerable_func"); // Assert result.FunctionName.Should().Be("vulnerable_func"); result.PreStatus.Should().Be(FunctionStatus.Present); result.PostStatus.Should().Be(FunctionStatus.Absent); result.Verdict.Should().Be(FunctionPatchVerdict.FunctionRemoved); } [Fact] public void FunctionDiffResult_NotFound_CreatesCorrectResult() { // Act var result = FunctionDiffResult.NotFound("missing_func"); // Assert result.FunctionName.Should().Be("missing_func"); result.PreStatus.Should().Be(FunctionStatus.Absent); result.PostStatus.Should().Be(FunctionStatus.Absent); result.Verdict.Should().Be(FunctionPatchVerdict.Inconclusive); } [Fact] public void CfgDiffResult_StructureChanged_DetectsChange() { // Arrange var diff = new CfgDiffResult { PreCfgHash = "hash1", PostCfgHash = "hash2", PreBlockCount = 5, PostBlockCount = 6, PreEdgeCount = 7, PostEdgeCount = 9 }; // Assert diff.StructureChanged.Should().BeTrue(); diff.BlockCountDelta.Should().Be(1); diff.EdgeCountDelta.Should().Be(2); } [Fact] public void CfgDiffResult_NoStructureChange_WhenHashesMatch() { // Arrange var diff = new CfgDiffResult { PreCfgHash = "samehash", PostCfgHash = "samehash", PreBlockCount = 5, PostBlockCount = 5, PreEdgeCount = 7, PostEdgeCount = 7 }; // Assert diff.StructureChanged.Should().BeFalse(); diff.BlockCountDelta.Should().Be(0); diff.EdgeCountDelta.Should().Be(0); } } [Trait("Category", "Unit")] public sealed class VulnerableEdgeDiffTests { [Fact] public void Compute_AllEdgesRemoved_SetsFlag() { // Arrange var preEdges = ImmutableArray.Create("bb0->bb1", "bb1->bb2"); var postEdges = ImmutableArray.Empty; // Act var diff = VulnerableEdgeDiff.Compute(preEdges, postEdges); // Assert diff.AllVulnerableEdgesRemoved.Should().BeTrue(); diff.EdgesRemoved.Should().HaveCount(2); diff.EdgesAdded.Should().BeEmpty(); } [Fact] public void Compute_SomeEdgesRemoved_SetsFlag() { // Arrange var preEdges = ImmutableArray.Create("bb0->bb1", "bb1->bb2"); var postEdges = ImmutableArray.Create("bb0->bb1"); // Act var diff = VulnerableEdgeDiff.Compute(preEdges, postEdges); // Assert diff.AllVulnerableEdgesRemoved.Should().BeFalse(); diff.SomeVulnerableEdgesRemoved.Should().BeTrue(); diff.EdgesRemoved.Should().Contain("bb1->bb2"); } [Fact] public void Compute_NoChange_NoEdgesRemovedOrAdded() { // Arrange var edges = ImmutableArray.Create("bb0->bb1", "bb1->bb2"); // Act var diff = VulnerableEdgeDiff.Compute(edges, edges); // Assert diff.NoChange.Should().BeTrue(); diff.EdgesRemoved.Should().BeEmpty(); diff.EdgesAdded.Should().BeEmpty(); } [Fact] public void Compute_EdgesAdded_TracksNewEdges() { // Arrange var preEdges = ImmutableArray.Create("bb0->bb1"); var postEdges = ImmutableArray.Create("bb0->bb1", "bb1->bb3"); // Act var diff = VulnerableEdgeDiff.Compute(preEdges, postEdges); // Assert diff.EdgesAdded.Should().Contain("bb1->bb3"); diff.AllVulnerableEdgesRemoved.Should().BeFalse(); } [Fact] public void Empty_ReturnsEmptyDiff() { // Act var empty = VulnerableEdgeDiff.Empty; // Assert empty.EdgesInPre.Should().BeEmpty(); empty.EdgesInPost.Should().BeEmpty(); empty.EdgesRemoved.Should().BeEmpty(); empty.EdgesAdded.Should().BeEmpty(); } } [Trait("Category", "Unit")] public sealed class SinkReachabilityDiffTests { [Fact] public void Compute_AllSinksUnreachable_SetsFlag() { // Arrange var preSinks = ImmutableArray.Create("memcpy", "strcpy"); var postSinks = ImmutableArray.Empty; // Act var diff = SinkReachabilityDiff.Compute(preSinks, postSinks); // Assert diff.AllSinksUnreachable.Should().BeTrue(); diff.SinksMadeUnreachable.Should().HaveCount(2); diff.SinksStillReachable.Should().BeEmpty(); } [Fact] public void Compute_SomeSinksUnreachable_SetsFlag() { // Arrange var preSinks = ImmutableArray.Create("memcpy", "strcpy"); var postSinks = ImmutableArray.Create("memcpy"); // Act var diff = SinkReachabilityDiff.Compute(preSinks, postSinks); // Assert diff.AllSinksUnreachable.Should().BeFalse(); diff.SomeSinksUnreachable.Should().BeTrue(); diff.SinksMadeUnreachable.Should().Contain("strcpy"); diff.SinksStillReachable.Should().Contain("memcpy"); } [Fact] public void Empty_ReturnsEmptyDiff() { // Act var empty = SinkReachabilityDiff.Empty; // Assert empty.SinksReachableInPre.Should().BeEmpty(); empty.SinksReachableInPost.Should().BeEmpty(); empty.SinksMadeUnreachable.Should().BeEmpty(); empty.SinksStillReachable.Should().BeEmpty(); } }