feat: add TLS certificate hot-reload for new connections (Gap 14.3)

Add ReloadTlsCertificates(oldOpts, newOpts) returning TlsReloadResult for
path-based cert comparison and validation during config hot-reload. Add 10
targeted tests covering no-change, path detection, missing file, null transitions,
and success cases.
This commit is contained in:
Joseph Doherty
2026-02-25 11:48:11 -05:00
parent 074ff6b287
commit 5116aed491
2 changed files with 236 additions and 1 deletions

View File

@@ -224,6 +224,153 @@ public class TlsReloadTests
result.ChangeCount.ShouldBe(2);
}
// ─── ReloadTlsCertificates (TlsReloadResult) tests ─────────────────────────
[Fact]
public void No_cert_change_returns_no_change()
{
// Go parity: tlsConfigReload — identical cert path means no reload needed
var oldOpts = new NatsOptions { TlsCert = "/same/cert.pem" };
var newOpts = new NatsOptions { TlsCert = "/same/cert.pem" };
var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);
result.CertificateChanged.ShouldBeFalse();
result.CertificateLoaded.ShouldBeFalse();
result.Error.ShouldBeNull();
}
[Fact]
public void Cert_path_changed_detected()
{
// Go parity: tlsConfigReload — different cert path triggers reload
var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
var newOpts = new NatsOptions { TlsCert = "/new/cert.pem" };
var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);
result.CertificateChanged.ShouldBeTrue();
}
[Fact]
public void Cert_path_set_returns_path()
{
// Verify CertificatePath reflects the new cert path when changed
var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
var newOpts = new NatsOptions { TlsCert = "/new/cert.pem" };
var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);
result.CertificatePath.ShouldBe("/new/cert.pem");
}
[Fact]
public void Missing_cert_file_returns_error()
{
// Go parity: tlsConfigReload — non-existent cert path returns an error
var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
var newOpts = new NatsOptions { TlsCert = "/nonexistent/path/cert.pem" };
var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);
result.Error.ShouldNotBeNullOrWhiteSpace();
result.Error!.ShouldContain("/nonexistent/path/cert.pem");
result.CertificateLoaded.ShouldBeFalse();
}
[Fact]
public void Null_to_null_no_change()
{
// Both TlsCert null — no TLS configured on either side, no change
var oldOpts = new NatsOptions { TlsCert = null };
var newOpts = new NatsOptions { TlsCert = null };
var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);
result.CertificateChanged.ShouldBeFalse();
}
[Fact]
public void Null_to_value_detected()
{
// Go parity: TestConfigReloadEnableTLS — enabling TLS is detected as a change
var oldOpts = new NatsOptions { TlsCert = null };
var newOpts = new NatsOptions { TlsCert = "/new/cert.pem" };
var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);
result.CertificateChanged.ShouldBeTrue();
}
[Fact]
public void Value_to_null_detected()
{
// Go parity: TestConfigReloadDisableTLS — disabling TLS is a change, loads successfully
var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
var newOpts = new NatsOptions { TlsCert = null };
var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);
result.CertificateChanged.ShouldBeTrue();
result.CertificateLoaded.ShouldBeTrue();
result.Error.ShouldBeNull();
}
[Fact]
public void Valid_cert_path_loaded_true()
{
// Go parity: tlsConfigReload — file exists, so CertificateLoaded is true
var tempFile = Path.GetTempFileName();
try
{
var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
var newOpts = new NatsOptions { TlsCert = tempFile };
var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);
result.CertificateChanged.ShouldBeTrue();
result.CertificateLoaded.ShouldBeTrue();
result.Error.ShouldBeNull();
}
finally
{
File.Delete(tempFile);
}
}
[Fact]
public void Error_null_on_success()
{
// Successful reload (file exists) must have Error as null
var tempFile = Path.GetTempFileName();
try
{
var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem" };
var newOpts = new NatsOptions { TlsCert = tempFile };
var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);
result.Error.ShouldBeNull();
}
finally
{
File.Delete(tempFile);
}
}
[Fact]
public void Same_empty_strings_no_change()
{
// Both TlsCert are empty string — treat as equal, no change
var oldOpts = new NatsOptions { TlsCert = string.Empty };
var newOpts = new NatsOptions { TlsCert = string.Empty };
var result = ConfigReloader.ReloadTlsCertificates(oldOpts, newOpts);
result.CertificateChanged.ShouldBeFalse();
}
/// <summary>
/// Helper to write a self-signed certificate to PEM files.
/// </summary>