refactor: rename remaining tests to NATS.Server.Core.Tests

- Rename tests/NATS.Server.Tests -> tests/NATS.Server.Core.Tests
- Update solution file, InternalsVisibleTo, and csproj references
- Remove JETSTREAM_INTEGRATION_MATRIX and NATS.NKeys from csproj (moved to JetStream.Tests and Auth.Tests)
- Update all namespaces from NATS.Server.Tests.* to NATS.Server.Core.Tests.*
- Replace private GetFreePort/ReadUntilAsync helpers with TestUtilities calls
- Fix stale namespace in Transport.Tests/NetworkingGoParityTests.cs
This commit is contained in:
Joseph Doherty
2026-03-12 16:14:02 -04:00
parent 78b4bc2486
commit 7fbffffd05
114 changed files with 576 additions and 1121 deletions

View File

@@ -0,0 +1,386 @@
// Tests for TLS certificate hot reload (E9).
// Verifies that TlsCertificateProvider supports atomic cert swapping
// and that ConfigReloader.ReloadTlsCertificate integrates correctly.
// Reference: golang/nats-server/server/reload_test.go — TestConfigReloadRotateTLS (line 392).
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using NATS.Server.Configuration;
using NATS.Server.Tls;
namespace NATS.Server.Core.Tests.Configuration;
public class TlsReloadTests
{
/// <summary>
/// Generates a self-signed X509Certificate2 for testing.
/// </summary>
private static X509Certificate2 GenerateSelfSignedCert(string cn = "test")
{
using var rsa = RSA.Create(2048);
var req = new CertificateRequest($"CN={cn}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));
// Export and re-import to ensure the cert has the private key bound
return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pkcs12), null);
}
[Fact]
public void CertificateProvider_GetCurrentCertificate_ReturnsInitialCert()
{
// Go parity: TestConfigReloadRotateTLS — initial cert is usable
var cert = GenerateSelfSignedCert("initial");
using var provider = new TlsCertificateProvider(cert);
var current = provider.GetCurrentCertificate();
current.ShouldNotBeNull();
current.Subject.ShouldContain("initial");
}
[Fact]
public void CertificateProvider_SwapCertificate_ReturnsOldCert()
{
// Go parity: TestConfigReloadRotateTLS — cert rotation returns old cert
var cert1 = GenerateSelfSignedCert("cert1");
var cert2 = GenerateSelfSignedCert("cert2");
using var provider = new TlsCertificateProvider(cert1);
var old = provider.SwapCertificate(cert2);
old.ShouldNotBeNull();
old.Subject.ShouldContain("cert1");
old.Dispose();
var current = provider.GetCurrentCertificate();
current.ShouldNotBeNull();
current.Subject.ShouldContain("cert2");
}
[Fact]
public void CertificateProvider_SwapCertificate_IncrementsVersion()
{
// Go parity: TestConfigReloadRotateTLS — version tracking for reload detection
var cert1 = GenerateSelfSignedCert("v1");
var cert2 = GenerateSelfSignedCert("v2");
using var provider = new TlsCertificateProvider(cert1);
var v0 = provider.Version;
v0.ShouldBe(0);
provider.SwapCertificate(cert2)?.Dispose();
provider.Version.ShouldBe(1);
}
[Fact]
public void CertificateProvider_MultipleSwa_NewConnectionsGetLatest()
{
// Go parity: TestConfigReloadRotateTLS — multiple rotations, each new
// handshake gets the latest certificate
var cert1 = GenerateSelfSignedCert("round1");
var cert2 = GenerateSelfSignedCert("round2");
var cert3 = GenerateSelfSignedCert("round3");
using var provider = new TlsCertificateProvider(cert1);
provider.GetCurrentCertificate()!.Subject.ShouldContain("round1");
provider.SwapCertificate(cert2)?.Dispose();
provider.GetCurrentCertificate()!.Subject.ShouldContain("round2");
provider.SwapCertificate(cert3)?.Dispose();
provider.GetCurrentCertificate()!.Subject.ShouldContain("round3");
provider.Version.ShouldBe(2);
}
[Fact]
public async Task CertificateProvider_ConcurrentAccess_IsThreadSafe()
{
// Go parity: TestConfigReloadRotateTLS — cert swap must be safe under
// concurrent connection accept
var cert1 = GenerateSelfSignedCert("concurrent1");
using var provider = new TlsCertificateProvider(cert1);
var tasks = new Task[50];
for (int i = 0; i < tasks.Length; i++)
{
var idx = i;
tasks[i] = Task.Run(() =>
{
if (idx % 2 == 0)
{
// Readers — simulate new connections getting current cert
var c = provider.GetCurrentCertificate();
c.ShouldNotBeNull();
}
else
{
// Writers — simulate reload
var newCert = GenerateSelfSignedCert($"swap-{idx}");
provider.SwapCertificate(newCert)?.Dispose();
}
});
}
await Task.WhenAll(tasks);
// After all swaps, the provider should still return a valid cert
provider.GetCurrentCertificate().ShouldNotBeNull();
}
[Fact]
public void ReloadTlsCertificate_NullProvider_ReturnsFalse()
{
// Edge case: server running without TLS
var opts = new NatsOptions();
var result = ConfigReloader.ReloadTlsCertificate(opts, null);
result.ShouldBeFalse();
}
[Fact]
public void ReloadTlsCertificate_NoTlsConfig_ReturnsFalse()
{
// Edge case: provider exists but options don't have TLS paths
var cert = GenerateSelfSignedCert("no-tls");
using var provider = new TlsCertificateProvider(cert);
var opts = new NatsOptions(); // HasTls is false (no TlsCert/TlsKey)
var result = ConfigReloader.ReloadTlsCertificate(opts, provider);
result.ShouldBeFalse();
}
[Fact]
public void ReloadTlsCertificate_WithCertFiles_SwapsCertAndSslOptions()
{
// Go parity: TestConfigReloadRotateTLS — full reload with cert files.
// Write a self-signed cert to temp files and verify the provider loads it.
var tempDir = Path.Combine(Path.GetTempPath(), $"nats-tls-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(tempDir);
try
{
var certPath = Path.Combine(tempDir, "cert.pem");
var keyPath = Path.Combine(tempDir, "key.pem");
WriteSelfSignedCertFiles(certPath, keyPath, "reload-test");
// Create provider with initial cert
var initialCert = GenerateSelfSignedCert("initial");
using var provider = new TlsCertificateProvider(initialCert);
var opts = new NatsOptions { TlsCert = certPath, TlsKey = keyPath };
var result = ConfigReloader.ReloadTlsCertificate(opts, provider);
result.ShouldBeTrue();
provider.Version.ShouldBeGreaterThan(0);
provider.GetCurrentCertificate().ShouldNotBeNull();
provider.GetCurrentSslOptions().ShouldNotBeNull();
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void ConfigDiff_DetectsTlsChanges()
{
// Go parity: TestConfigReloadEnableTLS, TestConfigReloadDisableTLS
// Verify that diff detects TLS option changes and flags them
var oldOpts = new NatsOptions { TlsCert = "/old/cert.pem", TlsKey = "/old/key.pem" };
var newOpts = new NatsOptions { TlsCert = "/new/cert.pem", TlsKey = "/new/key.pem" };
var changes = ConfigReloader.Diff(oldOpts, newOpts);
changes.Count.ShouldBeGreaterThan(0);
changes.ShouldContain(c => c.IsTlsChange && c.Name == "TlsCert");
changes.ShouldContain(c => c.IsTlsChange && c.Name == "TlsKey");
}
[Fact]
public void ConfigDiff_TlsVerifyChange_IsTlsChange()
{
// Go parity: TestConfigReloadRotateTLS — enabling client verification
var oldOpts = new NatsOptions { TlsVerify = false };
var newOpts = new NatsOptions { TlsVerify = true };
var changes = ConfigReloader.Diff(oldOpts, newOpts);
changes.ShouldContain(c => c.IsTlsChange && c.Name == "TlsVerify");
}
[Fact]
public void ConfigApplyResult_ReportsTlsChanges()
{
// Verify ApplyDiff flags TLS changes correctly
var changes = new List<IConfigChange>
{
new ConfigChange("TlsCert", isTlsChange: true),
new ConfigChange("TlsKey", isTlsChange: true),
};
var oldOpts = new NatsOptions();
var newOpts = new NatsOptions();
var result = ConfigReloader.ApplyDiff(changes, oldOpts, newOpts);
result.HasTlsChanges.ShouldBeTrue();
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>
private static void WriteSelfSignedCertFiles(string certPath, string keyPath, string cn)
{
using var rsa = RSA.Create(2048);
var req = new CertificateRequest($"CN={cn}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
var cert = req.CreateSelfSigned(DateTimeOffset.UtcNow, DateTimeOffset.UtcNow.AddDays(1));
File.WriteAllText(certPath, cert.ExportCertificatePem());
File.WriteAllText(keyPath, rsa.ExportRSAPrivateKeyPem());
}
}