// ============================================================================= // SourcePrecedenceLatticePropertyTests.cs // Property-based tests for lattice properties // Part of Task T25: Write property-based tests // ============================================================================= using StellaOps.AirGap.Importer.Reconciliation; namespace StellaOps.AirGap.Importer.Tests.Reconciliation; /// /// Property-based tests verifying lattice algebraic properties. /// A lattice must satisfy: associativity, commutativity, idempotence, and absorption. /// public sealed class SourcePrecedenceLatticePropertyTests { private static readonly SourcePrecedence[] AllPrecedences = [ SourcePrecedence.Unknown, SourcePrecedence.ThirdParty, SourcePrecedence.Maintainer, SourcePrecedence.Vendor ]; #region Lattice Algebraic Properties /// /// Property: Join is commutative - Join(a, b) = Join(b, a) /// [Fact] public void Join_IsCommutative() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { var joinAB = SourcePrecedenceLattice.Join(a, b); var joinBA = SourcePrecedenceLattice.Join(b, a); Assert.Equal(joinAB, joinBA); } } } /// /// Property: Meet is commutative - Meet(a, b) = Meet(b, a) /// [Fact] public void Meet_IsCommutative() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { var meetAB = SourcePrecedenceLattice.Meet(a, b); var meetBA = SourcePrecedenceLattice.Meet(b, a); Assert.Equal(meetAB, meetBA); } } } /// /// Property: Join is associative - Join(Join(a, b), c) = Join(a, Join(b, c)) /// [Fact] public void Join_IsAssociative() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { foreach (var c in AllPrecedences) { var left = SourcePrecedenceLattice.Join(SourcePrecedenceLattice.Join(a, b), c); var right = SourcePrecedenceLattice.Join(a, SourcePrecedenceLattice.Join(b, c)); Assert.Equal(left, right); } } } } /// /// Property: Meet is associative - Meet(Meet(a, b), c) = Meet(a, Meet(b, c)) /// [Fact] public void Meet_IsAssociative() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { foreach (var c in AllPrecedences) { var left = SourcePrecedenceLattice.Meet(SourcePrecedenceLattice.Meet(a, b), c); var right = SourcePrecedenceLattice.Meet(a, SourcePrecedenceLattice.Meet(b, c)); Assert.Equal(left, right); } } } } /// /// Property: Join is idempotent - Join(a, a) = a /// [Fact] public void Join_IsIdempotent() { foreach (var a in AllPrecedences) { var result = SourcePrecedenceLattice.Join(a, a); Assert.Equal(a, result); } } /// /// Property: Meet is idempotent - Meet(a, a) = a /// [Fact] public void Meet_IsIdempotent() { foreach (var a in AllPrecedences) { var result = SourcePrecedenceLattice.Meet(a, a); Assert.Equal(a, result); } } /// /// Property: Absorption law 1 - Join(a, Meet(a, b)) = a /// [Fact] public void Absorption_JoinMeet_ReturnsFirst() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { var meet = SourcePrecedenceLattice.Meet(a, b); var result = SourcePrecedenceLattice.Join(a, meet); Assert.Equal(a, result); } } } /// /// Property: Absorption law 2 - Meet(a, Join(a, b)) = a /// [Fact] public void Absorption_MeetJoin_ReturnsFirst() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { var join = SourcePrecedenceLattice.Join(a, b); var result = SourcePrecedenceLattice.Meet(a, join); Assert.Equal(a, result); } } } #endregion #region Ordering Properties /// /// Property: Compare is antisymmetric - if Compare(a,b) > 0 then Compare(b,a) < 0 /// [Fact] public void Compare_IsAntisymmetric() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { var compareAB = SourcePrecedenceLattice.Compare(a, b); var compareBA = SourcePrecedenceLattice.Compare(b, a); if (compareAB > 0) { Assert.True(compareBA < 0); } else if (compareAB < 0) { Assert.True(compareBA > 0); } else { Assert.Equal(0, compareBA); } } } } /// /// Property: Compare is transitive - if Compare(a,b) > 0 and Compare(b,c) > 0 then Compare(a,c) > 0 /// [Fact] public void Compare_IsTransitive() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { foreach (var c in AllPrecedences) { var ab = SourcePrecedenceLattice.Compare(a, b); var bc = SourcePrecedenceLattice.Compare(b, c); var ac = SourcePrecedenceLattice.Compare(a, c); if (ab > 0 && bc > 0) { Assert.True(ac > 0); } if (ab < 0 && bc < 0) { Assert.True(ac < 0); } } } } } /// /// Property: Compare is reflexive - Compare(a, a) = 0 /// [Fact] public void Compare_IsReflexive() { foreach (var a in AllPrecedences) { Assert.Equal(0, SourcePrecedenceLattice.Compare(a, a)); } } #endregion #region Join/Meet Bound Properties /// /// Property: Join returns an upper bound - Join(a, b) >= a AND Join(a, b) >= b /// [Fact] public void Join_ReturnsUpperBound() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { var join = SourcePrecedenceLattice.Join(a, b); Assert.True(SourcePrecedenceLattice.Compare(join, a) >= 0); Assert.True(SourcePrecedenceLattice.Compare(join, b) >= 0); } } } /// /// Property: Meet returns a lower bound - Meet(a, b) <= a AND Meet(a, b) <= b /// [Fact] public void Meet_ReturnsLowerBound() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { var meet = SourcePrecedenceLattice.Meet(a, b); Assert.True(SourcePrecedenceLattice.Compare(meet, a) <= 0); Assert.True(SourcePrecedenceLattice.Compare(meet, b) <= 0); } } } /// /// Property: Join is least upper bound - for all c, if c >= a and c >= b then c >= Join(a,b) /// [Fact] public void Join_IsLeastUpperBound() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { var join = SourcePrecedenceLattice.Join(a, b); foreach (var c in AllPrecedences) { var cGeA = SourcePrecedenceLattice.Compare(c, a) >= 0; var cGeB = SourcePrecedenceLattice.Compare(c, b) >= 0; if (cGeA && cGeB) { Assert.True(SourcePrecedenceLattice.Compare(c, join) >= 0); } } } } } /// /// Property: Meet is greatest lower bound - for all c, if c <= a and c <= b then c <= Meet(a,b) /// [Fact] public void Meet_IsGreatestLowerBound() { foreach (var a in AllPrecedences) { foreach (var b in AllPrecedences) { var meet = SourcePrecedenceLattice.Meet(a, b); foreach (var c in AllPrecedences) { var cLeA = SourcePrecedenceLattice.Compare(c, a) <= 0; var cLeB = SourcePrecedenceLattice.Compare(c, b) <= 0; if (cLeA && cLeB) { Assert.True(SourcePrecedenceLattice.Compare(c, meet) <= 0); } } } } } #endregion #region Bounded Lattice Properties /// /// Property: Unknown is the bottom element - Join(Unknown, a) = a /// [Fact] public void Unknown_IsBottomElement() { foreach (var a in AllPrecedences) { var result = SourcePrecedenceLattice.Join(SourcePrecedence.Unknown, a); Assert.Equal(a, result); } } /// /// Property: Vendor is the top element - Meet(Vendor, a) = a /// [Fact] public void Vendor_IsTopElement() { foreach (var a in AllPrecedences) { var result = SourcePrecedenceLattice.Meet(SourcePrecedence.Vendor, a); Assert.Equal(a, result); } } #endregion #region Merge Determinism /// /// Property: Merge is deterministic - same inputs always produce same output /// [Fact] public void Merge_IsDeterministic() { var lattice = new SourcePrecedenceLattice(); var timestamp = new DateTimeOffset(2025, 12, 4, 12, 0, 0, TimeSpan.Zero); var statements = new[] { CreateStatement("CVE-2024-001", "product-1", VexStatus.Affected, SourcePrecedence.ThirdParty, timestamp), CreateStatement("CVE-2024-001", "product-1", VexStatus.NotAffected, SourcePrecedence.Vendor, timestamp), CreateStatement("CVE-2024-001", "product-1", VexStatus.Fixed, SourcePrecedence.Maintainer, timestamp) }; // Run merge 100 times and verify same result var firstResult = lattice.Merge(statements); for (int i = 0; i < 100; i++) { var result = lattice.Merge(statements); Assert.Equal(firstResult.Status, result.Status); Assert.Equal(firstResult.Source, result.Source); Assert.Equal(firstResult.VulnerabilityId, result.VulnerabilityId); } } /// /// Property: Higher precedence always wins in merge /// [Fact] public void Merge_HigherPrecedenceWins() { var lattice = new SourcePrecedenceLattice(); var timestamp = new DateTimeOffset(2025, 12, 4, 12, 0, 0, TimeSpan.Zero); // Vendor should win over ThirdParty var vendorStatement = CreateStatement("CVE-2024-001", "product-1", VexStatus.NotAffected, SourcePrecedence.Vendor, timestamp); var thirdPartyStatement = CreateStatement("CVE-2024-001", "product-1", VexStatus.Affected, SourcePrecedence.ThirdParty, timestamp); var result = lattice.Merge(vendorStatement, thirdPartyStatement); Assert.Equal(SourcePrecedence.Vendor, result.Source); Assert.Equal(VexStatus.NotAffected, result.Status); } /// /// Property: More recent timestamp wins when precedence is equal /// [Fact] public void Merge_MoreRecentTimestampWins_WhenPrecedenceEqual() { var lattice = new SourcePrecedenceLattice(); var olderTimestamp = new DateTimeOffset(2025, 12, 1, 12, 0, 0, TimeSpan.Zero); var newerTimestamp = new DateTimeOffset(2025, 12, 4, 12, 0, 0, TimeSpan.Zero); var olderStatement = CreateStatement("CVE-2024-001", "product-1", VexStatus.Affected, SourcePrecedence.Maintainer, olderTimestamp); var newerStatement = CreateStatement("CVE-2024-001", "product-1", VexStatus.Fixed, SourcePrecedence.Maintainer, newerTimestamp); var result = lattice.Merge(olderStatement, newerStatement); Assert.Equal(VexStatus.Fixed, result.Status); Assert.Equal(newerTimestamp, result.Timestamp); } private static VexStatement CreateStatement( string vulnId, string productId, VexStatus status, SourcePrecedence source, DateTimeOffset? timestamp) { return new VexStatement { VulnerabilityId = vulnId, ProductId = productId, Status = status, Source = source, Timestamp = timestamp }; } #endregion }