using Microsoft.Extensions.Options; namespace ScadaLink.ClusterInfrastructure.Tests; /// /// CI-004: Tests that rejects the /// catastrophic misconfigurations the design doc warns against — a /// MinNrOfMembers other than 1, an unsupported split-brain strategy, /// empty seed nodes, and timings where the heartbeat is not below the /// failure-detection threshold. /// public class ClusterOptionsValidatorTests { private static ClusterOptions ValidOptions() => new() { SeedNodes = new List { "akka.tcp://scadalink@node1:8081", "akka.tcp://scadalink@node2:8081" }, SplitBrainResolverStrategy = "keep-oldest", StableAfter = TimeSpan.FromSeconds(15), HeartbeatInterval = TimeSpan.FromSeconds(2), FailureDetectionThreshold = TimeSpan.FromSeconds(10), MinNrOfMembers = 1, DownIfAlone = true }; [Fact] public void DefaultOptions_AreValid() { var result = new ClusterOptionsValidator().Validate(null, ValidOptions()); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void MinNrOfMembers_NotOne_FailsValidation() { var options = ValidOptions(); options.MinNrOfMembers = 2; var result = new ClusterOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains("MinNrOfMembers", result.FailureMessage); } [Theory] [InlineData("keep-majority")] [InlineData("static-quorum")] [InlineData("nonsense")] public void UnsupportedSplitBrainStrategy_FailsValidation(string strategy) { var options = ValidOptions(); options.SplitBrainResolverStrategy = strategy; var result = new ClusterOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains("SplitBrainResolverStrategy", result.FailureMessage); } [Fact] public void EmptySeedNodes_FailsValidation() { var options = ValidOptions(); options.SeedNodes = new List(); var result = new ClusterOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains("SeedNodes", result.FailureMessage); } [Fact] public void HeartbeatNotBelowFailureThreshold_FailsValidation() { var options = ValidOptions(); options.HeartbeatInterval = TimeSpan.FromSeconds(10); options.FailureDetectionThreshold = TimeSpan.FromSeconds(10); var result = new ClusterOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains("HeartbeatInterval", result.FailureMessage); } [Fact] public void NonPositiveStableAfter_FailsValidation() { var options = ValidOptions(); options.StableAfter = TimeSpan.Zero; var result = new ClusterOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains("StableAfter", result.FailureMessage); } [Fact] public void DownIfAloneFalse_FailsValidation() { var options = ValidOptions(); options.DownIfAlone = false; var result = new ClusterOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains("DownIfAlone", result.FailureMessage); } [Fact] public void Validate_AccumulatesAllFailures() { var options = new ClusterOptions { SeedNodes = new List(), SplitBrainResolverStrategy = "keep-majority", MinNrOfMembers = 2, StableAfter = TimeSpan.Zero, HeartbeatInterval = TimeSpan.FromSeconds(20), FailureDetectionThreshold = TimeSpan.FromSeconds(10) }; var result = new ClusterOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains("SeedNodes", result.FailureMessage); Assert.Contains("SplitBrainResolverStrategy", result.FailureMessage); Assert.Contains("MinNrOfMembers", result.FailureMessage); Assert.Contains("StableAfter", result.FailureMessage); Assert.Contains("HeartbeatInterval", result.FailureMessage); } }