using ZB.MOM.WW.ScadaBridge.Commons.Types.Audit; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Maintenance; namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Maintenance; /// /// Pure verdict table of (ScadaBridge#107 item 1) for the /// branches the SQL Server tests do not reach: a link carried by a tombstone, the partition /// anchor itself, and malformed input. /// public sealed class AuditChainHeadComparerTests { private static readonly byte[] H1 = Enumerable.Repeat((byte)1, 32).ToArray(); private static readonly byte[] H2 = Enumerable.Repeat((byte)2, 32).ToArray(); private static AuditChainHeadRecord Exported(long seq, byte[] hash) => new( 1, "2026-05", seq, AuditChainHeadComparer.ToHex(hash), null, null, seq, 0, DateTime.UtcNow, DateTime.UtcNow, "central-a"); private static AuditChainHeadSnapshot Head(long seq, byte[] hash, long? anchor = null, byte[]? anchorHash = null) => new(new DateTime(2026, 5, 1, 0, 0, 0, DateTimeKind.Utc), seq, hash, anchor, anchorHash, seq, 0, DateTime.UtcNow); private static Func> Link(byte[]? row, byte[]? tombstone) => _ => Task.FromResult(new AuditChainHeadComparer.StoredLink(row, tombstone)); private static Func> NoLookup => _ => throw new InvalidOperationException("no link lookup expected"); /// The exported link was later purged per channel: the tombstone carries the hash. /// A task that represents the asynchronous operation. [Fact] public async Task GrownChain_LinkOnATombstone_IsExtended_OrMismatch() { Assert.Equal(AuditChainHeadStatus.Extended, (await AuditChainHeadComparer.CompareAsync(Exported(3, H1), Head(9, H2), Link(null, H1))).Status); Assert.Equal(AuditChainHeadStatus.Mismatch, (await AuditChainHeadComparer.CompareAsync(Exported(3, H1), Head(9, H2), Link(null, H2))).Status); } /// The exported head is the partition-purge anchor: compared with the anchor's hash, no lookup. /// A task that represents the asynchronous operation. [Fact] public async Task GrownChain_ExportedHeadIsTheAnchor_ComparesTheAnchorHash() { Assert.Equal(AuditChainHeadStatus.Extended, (await AuditChainHeadComparer.CompareAsync(Exported(4, H1), Head(9, H2, 4, H1), NoLookup)).Status); Assert.Equal(AuditChainHeadStatus.Mismatch, (await AuditChainHeadComparer.CompareAsync(Exported(4, H1), Head(9, H2, 4, H2), NoLookup)).Status); Assert.Equal(AuditChainHeadStatus.Purged, (await AuditChainHeadComparer.CompareAsync(Exported(2, H1), Head(9, H2, 4, H2), NoLookup)).Status); } /// A record whose hash is not 32 bytes of hex can never match. /// The exported hash text. /// A task that represents the asynchronous operation. [Theory] [InlineData("abc")] [InlineData("zz0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f")] public async Task MalformedExportedHash_IsMismatch(string hash) { var record = Exported(3, H1) with { LastHash = hash }; Assert.Equal(AuditChainHeadStatus.Mismatch, (await AuditChainHeadComparer.CompareAsync(record, Head(3, H1), NoLookup)).Status); } /// Only the tamper verdicts count as failures. /// The verdict. /// Whether it is a failure. [Theory] [InlineData(AuditChainHeadStatus.Matches, false)] [InlineData(AuditChainHeadStatus.Extended, false)] [InlineData(AuditChainHeadStatus.Purged, false)] [InlineData(AuditChainHeadStatus.Mismatch, true)] [InlineData(AuditChainHeadStatus.Regressed, true)] [InlineData(AuditChainHeadStatus.MissingHead, true)] [InlineData(AuditChainHeadStatus.MissingLink, true)] [InlineData(AuditChainHeadStatus.CountMismatch, true)] public void IsFailure_IsTrueOnlyForTamperVerdicts(AuditChainHeadStatus status, bool failure) { Assert.Equal(failure, new AuditChainHeadCheck("2026-05", 1, DateTime.UtcNow, null, status, 1, "").IsFailure); } }