using Microsoft.Extensions.Options; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests; /// /// Every failure this validator catches otherwise surfaces as an absence — a deployment /// that never arrives, an alarm ack that does nothing, a node whose ack is dropped without a /// trace. Absences are the hardest class of fault to attribute in the field, so the whole point /// of these tests is that the host refuses to start instead. /// public class MeshTransportOptionsValidatorTests { private static ValidateOptionsResult Validate(MeshTransportOptions o) => new MeshTransportOptionsValidator().Validate(MeshTransportOptions.SectionName, o); [Fact] public void Default_options_are_valid() { // The default is Dps — the pre-Phase-2 transport. A node that never sets this section must // behave exactly as it did before, which is the whole premise of the dark switch. Validate(new MeshTransportOptions()).Succeeded.ShouldBeTrue(); } [Fact] public void Unknown_mode_fails() { var result = Validate(new MeshTransportOptions { Mode = "grpc" }); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain("grpc"); } [Fact] public void Mode_matching_is_case_insensitive() { Validate(new MeshTransportOptions { Mode = "clusterclient", CentralContactPoints = ["akka.tcp://otopcua@central-1:4053"], }).Succeeded.ShouldBeTrue(); } [Fact] public void Non_positive_contact_refresh_fails() { var result = Validate(new MeshTransportOptions { ContactRefreshSeconds = 0 }); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain(nameof(MeshTransportOptions.ContactRefreshSeconds)); } [Fact] public void ClusterClient_mode_requires_central_contact_points() { var result = Validate(new MeshTransportOptions { Mode = MeshTransportOptions.ModeClusterClient, CentralContactPoints = [], }); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain(nameof(MeshTransportOptions.CentralContactPoints)); } [Fact] public void Dps_mode_does_not_require_central_contact_points() { // An admin-only node has no reason to carry them, and requiring them under the default mode // would make the section mandatory everywhere for a feature that is switched off. Validate(new MeshTransportOptions { Mode = MeshTransportOptions.ModeDps, CentralContactPoints = [], }).Succeeded.ShouldBeTrue(); } [Fact] public void A_contact_point_that_is_not_an_akka_address_fails() { var result = Validate(new MeshTransportOptions { Mode = MeshTransportOptions.ModeClusterClient, CentralContactPoints = ["central-1:4053"], }); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain("akka.tcp://"); } [Fact] public void A_contact_point_carrying_an_actor_path_suffix_fails() { // The sister project's shipped site template listed a full receptionist path, and separately // listed the site's OWN remoting port as the second contact — "a permanent failure in the // initial-contact rotation". Both are invisible until a command goes missing. var result = Validate(new MeshTransportOptions { Mode = MeshTransportOptions.ModeClusterClient, CentralContactPoints = ["akka.tcp://otopcua@central-1:4053/system/receptionist"], }); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain("/system/receptionist"); } [Fact] public void One_bad_contact_point_among_good_ones_still_fails() { var result = Validate(new MeshTransportOptions { Mode = MeshTransportOptions.ModeClusterClient, CentralContactPoints = [ "akka.tcp://otopcua@central-1:4053", "central-2:4053", ], }); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain("central-2:4053"); } [Fact] public void Well_formed_ClusterClient_options_succeed() { Validate(new MeshTransportOptions { Mode = MeshTransportOptions.ModeClusterClient, CentralContactPoints = [ "akka.tcp://otopcua@central-1:4053", "akka.tcp://otopcua@central-2:4053", ], }).Succeeded.ShouldBeTrue(); } }