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);
}
}