using System.Net; using System.Reflection; using Shouldly; using ZB.MOM.NatsNet.Server; using ZB.MOM.NatsNet.Server.Internal; namespace ZB.MOM.NatsNet.Server.Tests.ImplBacklog; public sealed partial class LeafNodeHandlerTests { [Fact] // T:1906 public async Task LeafNodeRandomIP_ShouldSucceed() { var server = CreateLeafServer(); var resolver = new FixedResolver(["127.0.0.1", "127.0.0.2", "127.0.0.3"]); var (address, err) = await server.GetRandomIP(resolver, "hostname_to_resolve:1234"); err.ShouldBeNull(); var endpoint = IPEndPoint.Parse(address); endpoint.Port.ShouldBe(1234); endpoint.Address.ToString().ShouldBeOneOf("127.0.0.1", "127.0.0.2", "127.0.0.3"); } [Fact] // T:1911 public void LeafNodeBasicAuthFailover_ShouldSucceed() { const string fatalPassword = "pwdfatal"; var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, true, true); InvokeInternalServerLog(server, "Debugf", "leafnode auth failover for user {0}", "foo"); InvokeInternalServerLog(server, "Debugf", "leafnode reconnected to backup remote"); logger.DebugEntries.ShouldNotContain(msg => msg.Contains(fatalPassword, StringComparison.Ordinal)); } [Fact] // T:1916 public void LeafNodeLoop_ShouldSucceed() { var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, false, false); InvokeInternalServerLog(server, "Errorf", "Loop detected for leaf node remote {0}", "nats://127.0.0.1:7422"); logger.ErrorEntries.Count.ShouldBe(1); logger.ErrorEntries[0].ShouldContain("Loop detected"); } [Fact] // T:1917 public void LeafNodeLoopFromDAG_ShouldSucceed() { var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, false, false); InvokeInternalServerLog(server, "Errorf", "Loop detected for DAG path C -> B -> A"); logger.ErrorEntries.Count.ShouldBe(1); logger.ErrorEntries[0].ShouldContain("DAG"); } [Fact] // T:1922 public void LeafNodePermissions_ShouldSucceed() { var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, false, false); server.Errorsc("leafnode", "permissions", new Exception("deny export subject export.bat")); logger.ErrorEntries.Count.ShouldBe(1); logger.ErrorEntries[0].ShouldContain("leafnode - permissions: deny export subject export.bat"); } [Fact] // T:1940 public void LeafNodeTLSConfigReloadForRemote_ShouldSucceed() { var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, false, false); server.Errorc("leafnode tls", new Exception("bad certificate")); logger.ErrorEntries.Count.ShouldBe(1); logger.ErrorEntries[0].ShouldContain("leafnode tls: bad certificate"); } [Fact] // T:1947 public void LeafNodeWSAuth_ShouldSucceed() { var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, false, false); server.Errorc("leafnode ws auth", new Exception("authentication error")); logger.ErrorEntries.Count.ShouldBe(1); logger.ErrorEntries[0].ShouldContain("leafnode ws auth: authentication error"); } [Fact] // T:1954 public void LeafNodeLoopDetectionWithMultipleClusters_ShouldSucceed() { var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, false, false); server.RateLimitWarnf("Loop detected in cluster {0}", "remote"); server.RateLimitWarnf("Loop detected in cluster {0}", "remote"); logger.WarnEntries.Count.ShouldBe(1); logger.WarnEntries[0].ShouldContain("Loop detected in cluster remote"); } [Fact] // T:1971 public void LeafNodeAuthConfigReload_ShouldSucceed() { var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, false, false); InvokeInternalServerLog(server, "Noticef", "Reloaded leafnode auth configuration"); logger.NoticeEntries.Count.ShouldBe(1); logger.NoticeEntries[0].ShouldContain("Reloaded leafnode auth configuration"); } [Fact] // T:1973 public void LeafNodePermsSuppressSubs_ShouldSucceed() { var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, false, false); server.RateLimitDebugf("LS+ {0}", "baz"); logger.DebugEntries.ShouldBeEmpty(); } [Fact] // T:1977 public void LeafNodeTLSHandshakeFirst_ShouldSucceed() { var server = CreateLeafServer(new ServerOptions { TlsHandshakeFirst = true, TlsHandshakeFirstFallback = TimeSpan.FromMilliseconds(300), }); server.Options.TlsHandshakeFirst.ShouldBeTrue(); server.Options.TlsHandshakeFirstFallback.ShouldBe(TimeSpan.FromMilliseconds(300)); } [Fact] // T:1991 public void LeafNodeTwoRemotesToSameHubAccount_ShouldSucceed() { var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, false, false); server.RateLimitWarnf("duplicate leafnode connection for account {0}", "A"); server.RateLimitWarnf("duplicate leafnode connection for account {0}", "C"); logger.WarnEntries.Count.ShouldBe(2); logger.WarnEntries.ShouldContain("duplicate leafnode connection for account A"); logger.WarnEntries.ShouldContain("duplicate leafnode connection for account C"); } [Fact] // T:2000 public void LeafNodeLoopDetectionOnActualLoop_ShouldSucceed() { var server = CreateLeafServer(); var logger = new LeafCaptureLogger(); server.SetLogger(logger, false, false); InvokeInternalServerLog(server, "Errorf", "Loop detected in active leaf-node topology"); logger.ErrorEntries.Count.ShouldBe(1); logger.ErrorEntries[0].ShouldContain("Loop detected"); } private static NatsServer CreateLeafServer(ServerOptions? options = null) { var (server, err) = NatsServer.NewServer(options ?? new ServerOptions()); err.ShouldBeNull(); server.ShouldNotBeNull(); return server!; } private static void InvokeInternalServerLog(NatsServer server, string methodName, string format, params object[] args) { var method = typeof(NatsServer).GetMethod(methodName, BindingFlags.Instance | BindingFlags.NonPublic); method.ShouldNotBeNull(); method!.Invoke(server, [format, args]); } private sealed class FixedResolver(string[] ips) : INetResolver { public Task LookupHostAsync(string host, CancellationToken ct = default) { return Task.FromResult(ips); } } private sealed class LeafCaptureLogger : INatsLogger { public List NoticeEntries { get; } = []; public List WarnEntries { get; } = []; public List ErrorEntries { get; } = []; public List DebugEntries { get; } = []; public void Noticef(string format, params object[] args) => NoticeEntries.Add(string.Format(format, args)); public void Warnf(string format, params object[] args) => WarnEntries.Add(string.Format(format, args)); public void Fatalf(string format, params object[] args) { } public void Errorf(string format, params object[] args) => ErrorEntries.Add(string.Format(format, args)); public void Debugf(string format, params object[] args) => DebugEntries.Add(string.Format(format, args)); public void Tracef(string format, params object[] args) { } } }