// Licensed under AGPL-3.0-or-later. 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 DiffEvidenceTests { [Fact] public void FunctionRemoved_CreatesCorrectEvidence() { // Act var evidence = DiffEvidence.FunctionRemoved("vuln_func"); // Assert evidence.Type.Should().Be(DiffEvidenceType.FunctionRemoved); evidence.FunctionName.Should().Be("vuln_func"); evidence.Weight.Should().Be(0.9m); evidence.Description.Should().Contain("vuln_func"); } [Fact] public void FunctionRenamed_CreatesCorrectEvidence() { // Act var evidence = DiffEvidence.FunctionRenamed("old_name", "new_name", 0.85m); // Assert evidence.Type.Should().Be(DiffEvidenceType.FunctionRenamed); evidence.FunctionName.Should().Be("old_name"); evidence.Data["OldName"].Should().Be("old_name"); evidence.Data["NewName"].Should().Be("new_name"); evidence.Data["Similarity"].Should().Be("0.850"); } [Fact] public void CfgStructureChanged_CreatesCorrectEvidence() { // Act var evidence = DiffEvidence.CfgStructureChanged("func", "hash1", "hash2"); // Assert evidence.Type.Should().Be(DiffEvidenceType.CfgStructureChanged); evidence.FunctionName.Should().Be("func"); evidence.Data["PreHash"].Should().Be("hash1"); evidence.Data["PostHash"].Should().Be("hash2"); evidence.Weight.Should().Be(0.5m); } [Fact] public void VulnerableEdgeRemoved_CreatesCorrectEvidence() { // Arrange var edges = ImmutableArray.Create("bb0->bb1", "bb1->bb2"); // Act var evidence = DiffEvidence.VulnerableEdgeRemoved("func", edges); // Assert evidence.Type.Should().Be(DiffEvidenceType.VulnerableEdgeRemoved); evidence.Weight.Should().Be(1.0m); evidence.Data["EdgeCount"].Should().Be("2"); evidence.Data["EdgesRemoved"].Should().Contain("bb0->bb1"); } [Fact] public void SinkMadeUnreachable_CreatesCorrectEvidence() { // Arrange var sinks = ImmutableArray.Create("memcpy", "strcpy"); // Act var evidence = DiffEvidence.SinkMadeUnreachable("func", sinks); // Assert evidence.Type.Should().Be(DiffEvidenceType.SinkMadeUnreachable); evidence.Weight.Should().Be(0.95m); evidence.Data["SinkCount"].Should().Be("2"); } [Fact] public void TaintGateAdded_CreatesCorrectEvidence() { // Act var evidence = DiffEvidence.TaintGateAdded("func", "BoundCheck", "len < bufsize"); // Assert evidence.Type.Should().Be(DiffEvidenceType.TaintGateAdded); evidence.Data["GateType"].Should().Be("BoundCheck"); evidence.Data["Condition"].Should().Be("len < bufsize"); evidence.Weight.Should().Be(0.85m); } [Fact] public void SemanticDivergence_CreatesCorrectEvidence() { // Act var evidence = DiffEvidence.SemanticDivergence("func", 0.45m); // Assert evidence.Type.Should().Be(DiffEvidenceType.SemanticDivergence); evidence.Data["Similarity"].Should().Be("0.450"); evidence.Weight.Should().Be(0.6m); } [Fact] public void IdenticalBinaries_CreatesCorrectEvidence() { // Act var evidence = DiffEvidence.IdenticalBinaries("sha256:abc123"); // Assert evidence.Type.Should().Be(DiffEvidenceType.IdenticalBinaries); evidence.Data["Digest"].Should().Be("sha256:abc123"); evidence.Weight.Should().Be(1.0m); } [Theory] [InlineData(DiffEvidenceType.FunctionRemoved)] [InlineData(DiffEvidenceType.FunctionRenamed)] [InlineData(DiffEvidenceType.CfgStructureChanged)] [InlineData(DiffEvidenceType.VulnerableEdgeRemoved)] [InlineData(DiffEvidenceType.VulnerableBlockModified)] [InlineData(DiffEvidenceType.SinkMadeUnreachable)] [InlineData(DiffEvidenceType.TaintGateAdded)] [InlineData(DiffEvidenceType.ConstantChanged)] [InlineData(DiffEvidenceType.SemanticDivergence)] [InlineData(DiffEvidenceType.IdenticalBinaries)] public void DiffEvidenceType_AllValuesAreDefined(DiffEvidenceType type) { // Assert Enum.IsDefined(type).Should().BeTrue(); } } [Trait("Category", "Unit")] public sealed class DiffOptionsTests { [Fact] public void Default_HasSensibleDefaults() { // Act var options = DiffOptions.Default; // Assert options.IncludeSemanticAnalysis.Should().BeFalse(); options.IncludeReachabilityAnalysis.Should().BeTrue(); options.SemanticThreshold.Should().Be(0.85m); options.FixedConfidenceThreshold.Should().Be(0.80m); options.DetectRenames.Should().BeTrue(); options.FunctionTimeout.Should().Be(TimeSpan.FromSeconds(30)); options.TotalTimeout.Should().Be(TimeSpan.FromMinutes(10)); } [Fact] public void DiffOptions_CanBeCustomized() { // Act var options = new DiffOptions { IncludeSemanticAnalysis = true, SemanticThreshold = 0.95m, DetectRenames = false }; // Assert options.IncludeSemanticAnalysis.Should().BeTrue(); options.SemanticThreshold.Should().Be(0.95m); options.DetectRenames.Should().BeFalse(); } } [Trait("Category", "Unit")] public sealed class DiffMetadataTests { [Fact] public void CurrentEngineVersion_IsSet() { // Assert DiffMetadata.CurrentEngineVersion.Should().NotBeNullOrEmpty(); DiffMetadata.CurrentEngineVersion.Should().Be("1.0.0"); } [Fact] public void DiffMetadata_StoresAllProperties() { // Arrange var comparedAt = DateTimeOffset.UtcNow; var duration = TimeSpan.FromSeconds(5); var options = DiffOptions.Default; // Act var metadata = new DiffMetadata { ComparedAt = comparedAt, EngineVersion = DiffMetadata.CurrentEngineVersion, Duration = duration, Options = options }; // Assert metadata.ComparedAt.Should().Be(comparedAt); metadata.EngineVersion.Should().Be("1.0.0"); metadata.Duration.Should().Be(duration); metadata.Options.Should().Be(options); } } [Trait("Category", "Unit")] public sealed class SingleBinaryCheckResultTests { [Fact] public void NotVulnerable_CreatesCorrectResult() { // Arrange var binaryDigest = "sha256:abc123"; var goldenSetId = "CVE-2024-1234"; var checkedAt = DateTimeOffset.UtcNow; var duration = TimeSpan.FromMilliseconds(50); // Act var result = SingleBinaryCheckResult.NotVulnerable( binaryDigest, goldenSetId, checkedAt, duration); // Assert result.IsVulnerable.Should().BeFalse(); result.Confidence.Should().Be(0.9m); result.BinaryDigest.Should().Be(binaryDigest); result.GoldenSetId.Should().Be(goldenSetId); result.FunctionResults.Should().BeEmpty(); } } [Trait("Category", "Unit")] public sealed class FunctionRenameTests { [Fact] public void FunctionRename_StoresAllProperties() { // Act var rename = new FunctionRename { OriginalName = "old_func", NewName = "new_func", Confidence = 0.92m, Similarity = 0.92m }; // Assert rename.OriginalName.Should().Be("old_func"); rename.NewName.Should().Be("new_func"); rename.Confidence.Should().Be(0.92m); rename.Similarity.Should().Be(0.92m); } } [Trait("Category", "Unit")] public sealed class RenameDetectionOptionsTests { [Fact] public void Default_HasSensibleDefaults() { // Act var options = RenameDetectionOptions.Default; // Assert options.MinSimilarity.Should().Be(0.7m); options.UseCfgHash.Should().BeTrue(); options.UseBlockHashes.Should().BeTrue(); options.UseStringRefs.Should().BeTrue(); } }