namespace ZB.MOM.WW.ScadaBridge.ClusterInfrastructure.Tests; /// /// for the opt-in Akka.Remote TLS block /// (ScadaBridge:Cluster:RemotingTls, ScadaBridge#64). Disabled must check nothing — so the /// default configuration can never be blocked by it — and enabled must refuse the shapes that would /// boot into a pair that silently never joins. /// public class ClusterRemotingTlsValidatorTests { private static ClusterOptions Options(bool tlsSeeds, ClusterRemotingTlsOptions tls) { var scheme = tlsSeeds ? "akka.ssl.tcp" : "akka.tcp"; return new ClusterOptions { SeedNodes = new List { $"{scheme}://scadabridge@node1:8081", $"{scheme}://scadabridge@node2:8081" }, SplitBrainResolverStrategy = "auto-down", RemotingTls = tls, }; } private static ClusterRemotingTlsOptions PathTls() => new() { Enabled = true, CertificatePath = "/certs/node1.pfx", CertificatePassword = "pw", }; /// Verifies TLS is off by default. [Fact] public void RemotingTls_is_off_by_default() { Assert.False(new ClusterOptions().RemotingTls.Enabled); } /// /// Verifies a disabled block carrying garbage (both sources, a bad store location) is not checked: /// with TLS off the fields are inert, and validating them would let an unused block break a boot. /// [Fact] public void Disabled_block_is_not_validated() { var tls = new ClusterRemotingTlsOptions { Enabled = false, CertificatePath = "/x.pfx", CertificateThumbprint = "ABC", StoreLocation = "nowhere", }; var result = new ClusterOptionsValidator().Validate(null, Options(tlsSeeds: false, tls)); Assert.True(result.Succeeded, result.FailureMessage); } /// Verifies a file-certificate TLS config with akka.ssl.tcp seeds is valid. [Fact] public void Enabled_with_path_and_tls_seeds_is_valid() { var result = new ClusterOptionsValidator().Validate(null, Options(tlsSeeds: true, PathTls())); Assert.True(result.Succeeded, result.FailureMessage); } /// Verifies a store-thumbprint TLS config with akka.ssl.tcp seeds is valid. [Fact] public void Enabled_with_thumbprint_and_tls_seeds_is_valid() { var tls = new ClusterRemotingTlsOptions { Enabled = true, CertificateThumbprint = "45df32e258c92a7abf6c112e54912ab15bbb9eb0" }; var result = new ClusterOptionsValidator().Validate(null, Options(tlsSeeds: true, tls)); Assert.True(result.Succeeded, result.FailureMessage); } /// /// Verifies TLS with akka.tcp seeds is refused: Akka.Remote advertises akka.ssl.tcp once SSL is on, /// so those seeds name an address nobody answers and the join retries forever. /// [Fact] public void Enabled_with_plain_tcp_seeds_fails() { var result = new ClusterOptionsValidator().Validate(null, Options(tlsSeeds: false, PathTls())); Assert.True(result.Failed); Assert.Contains("akka.ssl.tcp://", result.FailureMessage); Assert.Contains("akka.tcp://scadabridge@node1:8081", result.FailureMessage); } /// Verifies exactly one certificate source is required. /// The certificate path, or null. /// The thumbprint, or null. [Theory] [InlineData(null, null)] [InlineData("/certs/node1.pfx", "45df32e258c92a7abf6c112e54912ab15bbb9eb0")] public void Enabled_requires_exactly_one_certificate_source(string? path, string? thumbprint) { var tls = new ClusterRemotingTlsOptions { Enabled = true, CertificatePath = path, CertificateThumbprint = thumbprint }; var result = new ClusterOptionsValidator().Validate(null, Options(tlsSeeds: true, tls)); Assert.True(result.Failed); Assert.Contains("exactly one certificate source", result.FailureMessage); } /// Verifies an unknown store location is refused for a thumbprint certificate. [Fact] public void Enabled_thumbprint_rejects_unknown_store_location() { var tls = new ClusterRemotingTlsOptions { Enabled = true, CertificateThumbprint = "ABC", StoreLocation = "LocalMachine" }; var result = new ClusterOptionsValidator().Validate(null, Options(tlsSeeds: true, tls)); Assert.True(result.Failed); Assert.Contains("StoreLocation", result.FailureMessage); } }