using Microsoft.Extensions.Configuration; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Runtime.Tests.Historian; /// /// #603 + review R1 m3: the two "is this node's LocalDb replicated" predicates — permissive for table /// registration (), strict for the alarm drain gate /// (). /// public sealed class LocalDbReplicationPolicyTests { private const string Peer = "LocalDb:Replication:PeerAddress"; private const string Key = "LocalDb:Replication:ApiKey"; private const string Port = "LocalDb:SyncListenPort"; private static IConfiguration Config(params (string Key, string? Value)[] pairs) => new ConfigurationBuilder() .AddInMemoryCollection(pairs.Select(p => new KeyValuePair(p.Key, p.Value))) .Build(); /// Registration: PeerAddress OR ApiKey counts; the listen port alone and blank values do not. [Theory] [InlineData(Peer, "http://peer:9001", true)] [InlineData(Key, "k", true)] [InlineData(Port, "9001", false)] [InlineData(Key, " ", false)] public void CapturesChanges_follows_the_PeerAddress_or_ApiKey_rule(string key, string value, bool expected) { LocalDbReplicationPolicy.CapturesChanges(Config((key, value))).ShouldBe(expected); } /// Drain gate: ApiKey AND (PeerAddress OR SyncListenPort > 0). [Theory] [InlineData("http://peer:9001", "k", null, true)] // dialling half of a pair [InlineData(null, "k", "9001", true)] // listening half of a pair [InlineData("http://peer:9001", "k", "9001", true)] // both [InlineData(null, "k", null, false)] // key, no path: shares nothing (review R1 m3) [InlineData(null, "k", "0", false)] // port 0 = no listener [InlineData(null, "k", "nope", false)] // unparseable port = no listener [InlineData("http://peer:9001", null, "9001", false)] // paths, no key: both auth directions fail closed [InlineData(null, " ", "9001", false)] // blank key public void SharesRowsWithPeer_needs_a_key_and_a_sync_path(string? peer, string? key, string? port, bool expected) { LocalDbReplicationPolicy.SharesRowsWithPeer(Config((Peer, peer), (Key, key), (Port, port))).ShouldBe(expected); } /// Every node the gate treats as sharing rows also captures — never a gated node with no capture. [Theory] [InlineData("http://peer:9001", "k", null)] [InlineData(null, "k", "9001")] public void A_node_sharing_rows_always_captures(string? peer, string? key, string? port) { var config = Config((Peer, peer), (Key, key), (Port, port)); LocalDbReplicationPolicy.SharesRowsWithPeer(config).ShouldBeTrue(); LocalDbReplicationPolicy.CapturesChanges(config).ShouldBeTrue(); } /// No replication keys at all is an unpaired node under both predicates. [Fact] public void Both_predicates_are_false_for_an_empty_configuration() { LocalDbReplicationPolicy.CapturesChanges(Config()).ShouldBeFalse(); LocalDbReplicationPolicy.SharesRowsWithPeer(Config()).ShouldBeFalse(); } }