using Microsoft.Extensions.Logging.Abstractions; using NATS.Server.Configuration; using NATS.Server.Gateways; using NATS.Server.Monitoring; namespace NATS.Server.Tests.Gateways; /// /// Gateway configuration validation, options parsing, monitoring endpoint, /// and server lifecycle tests. /// Ported from golang/nats-server/server/gateway_test.go. /// public class GatewayConfigTests { // ── GatewayOptions Defaults ───────────────────────────────────────── // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public void Default_gateway_options_have_correct_defaults() { var options = new GatewayOptions(); options.Name.ShouldBeNull(); options.Host.ShouldBe("0.0.0.0"); options.Port.ShouldBe(0); options.Remotes.ShouldNotBeNull(); options.Remotes.Count.ShouldBe(0); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public void Gateway_options_name_can_be_set() { var options = new GatewayOptions { Name = "CLUSTER-A" }; options.Name.ShouldBe("CLUSTER-A"); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public void Gateway_options_host_can_be_set() { var options = new GatewayOptions { Host = "192.168.1.1" }; options.Host.ShouldBe("192.168.1.1"); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public void Gateway_options_port_can_be_set() { var options = new GatewayOptions { Port = 7222 }; options.Port.ShouldBe(7222); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public void Gateway_options_remotes_can_be_set() { var options = new GatewayOptions { Remotes = ["127.0.0.1:7222", "127.0.0.1:7223"], }; options.Remotes.Count.ShouldBe(2); } // ── NatsOptions Gateway Configuration ─────────────────────────────── // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public void NatsOptions_gateway_is_null_by_default() { var opts = new NatsOptions(); opts.Gateway.ShouldBeNull(); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public void NatsOptions_gateway_can_be_assigned() { var opts = new NatsOptions { Gateway = new GatewayOptions { Name = "TestGW", Host = "127.0.0.1", Port = 7222, }, }; opts.Gateway.ShouldNotBeNull(); opts.Gateway.Name.ShouldBe("TestGW"); } // ── Config File Parsing ───────────────────────────────────────────── // Go: TestGatewayWithListenToAny server/gateway_test.go:834 [Fact] public void Config_processor_parses_gateway_name() { var config = """ gateway { name: "MY-GATEWAY" } """; var opts = ConfigProcessor.ProcessConfig(config); opts.Gateway.ShouldNotBeNull(); opts.Gateway!.Name.ShouldBe("MY-GATEWAY"); } // Go: TestGatewayWithListenToAny server/gateway_test.go:834 [Fact] public void Config_processor_parses_gateway_listen() { var config = """ gateway { name: "GW" listen: "127.0.0.1:7222" } """; var opts = ConfigProcessor.ProcessConfig(config); opts.Gateway.ShouldNotBeNull(); opts.Gateway!.Host.ShouldBe("127.0.0.1"); opts.Gateway!.Port.ShouldBe(7222); } // Go: TestGatewayWithListenToAny server/gateway_test.go:834 [Fact] public void Config_processor_parses_gateway_listen_any() { var config = """ gateway { name: "GW" listen: "0.0.0.0:7333" } """; var opts = ConfigProcessor.ProcessConfig(config); opts.Gateway.ShouldNotBeNull(); opts.Gateway!.Host.ShouldBe("0.0.0.0"); opts.Gateway!.Port.ShouldBe(7333); } // Go: TestGatewayWithListenToAny server/gateway_test.go:834 [Fact] public void Config_processor_gateway_without_name_leaves_null() { var config = """ gateway { listen: "127.0.0.1:7222" } """; var opts = ConfigProcessor.ProcessConfig(config); opts.Gateway.ShouldNotBeNull(); opts.Gateway!.Name.ShouldBeNull(); } // Go: TestGatewayWithListenToAny server/gateway_test.go:834 [Fact] public void Config_processor_no_gateway_section_leaves_null() { var config = """ port: 4222 """; var opts = ConfigProcessor.ProcessConfig(config); opts.Gateway.ShouldBeNull(); } // ── Server Lifecycle with Gateway ─────────────────────────────────── // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public async Task Server_starts_with_gateway_configured() { var opts = new NatsOptions { Host = "127.0.0.1", Port = 0, Gateway = new GatewayOptions { Name = "LIFECYCLE", Host = "127.0.0.1", Port = 0, }, }; var server = new NatsServer(opts, NullLoggerFactory.Instance); var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); server.GatewayListen.ShouldNotBeNull(); server.GatewayListen.ShouldContain("127.0.0.1:"); await cts.CancelAsync(); server.Dispose(); cts.Dispose(); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public async Task Server_gateway_listen_uses_ephemeral_port() { var opts = new NatsOptions { Host = "127.0.0.1", Port = 0, Gateway = new GatewayOptions { Name = "EPHEMERAL", Host = "127.0.0.1", Port = 0, }, }; var server = new NatsServer(opts, NullLoggerFactory.Instance); var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); // The gateway listen should have a non-zero port var parts = server.GatewayListen!.Split(':'); int.Parse(parts[1]).ShouldBeGreaterThan(0); await cts.CancelAsync(); server.Dispose(); cts.Dispose(); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public async Task Server_without_gateway_has_null_gateway_listen() { var opts = new NatsOptions { Host = "127.0.0.1", Port = 0, }; var server = new NatsServer(opts, NullLoggerFactory.Instance); var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); server.GatewayListen.ShouldBeNull(); await cts.CancelAsync(); server.Dispose(); cts.Dispose(); } // Go: TestGatewayNoPanicOnStartupWithMonitoring server/gateway_test.go:6903 [Fact] public async Task Server_starts_with_both_gateway_and_monitoring() { var opts = new NatsOptions { Host = "127.0.0.1", Port = 0, MonitorPort = 0, Gateway = new GatewayOptions { Name = "MON-GW", Host = "127.0.0.1", Port = 0, }, }; var server = new NatsServer(opts, NullLoggerFactory.Instance); var cts = new CancellationTokenSource(); _ = server.StartAsync(cts.Token); await server.WaitForReadyAsync(); server.GatewayListen.ShouldNotBeNull(); await cts.CancelAsync(); server.Dispose(); cts.Dispose(); } // ── GatewayManager Unit Tests ─────────────────────────────────────── // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public async Task Gateway_manager_starts_and_listens() { var options = new GatewayOptions { Name = "UNIT", Host = "127.0.0.1", Port = 0, }; var stats = new ServerStats(); var manager = new GatewayManager( options, stats, "SERVER-1", _ => { }, _ => { }, NullLogger.Instance); await manager.StartAsync(CancellationToken.None); manager.ListenEndpoint.ShouldContain("127.0.0.1:"); await manager.DisposeAsync(); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public async Task Gateway_manager_ephemeral_port_resolves() { var options = new GatewayOptions { Name = "UNIT", Host = "127.0.0.1", Port = 0, }; var manager = new GatewayManager( options, new ServerStats(), "S1", _ => { }, _ => { }, NullLogger.Instance); await manager.StartAsync(CancellationToken.None); // Port should have been resolved options.Port.ShouldBeGreaterThan(0); await manager.DisposeAsync(); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public async Task Gateway_manager_dispose_decrements_stats() { var options = new GatewayOptions { Name = "STATS", Host = "127.0.0.1", Port = 0, }; var stats = new ServerStats(); var manager = new GatewayManager( options, stats, "S1", _ => { }, _ => { }, NullLogger.Instance); await manager.StartAsync(CancellationToken.None); await manager.DisposeAsync(); stats.Gateways.ShouldBe(0); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public async Task Gateway_manager_forward_without_connections_does_not_throw() { var options = new GatewayOptions { Name = "EMPTY", Host = "127.0.0.1", Port = 0, }; var manager = new GatewayManager( options, new ServerStats(), "S1", _ => { }, _ => { }, NullLogger.Instance); // ForwardMessageAsync without any connections should not throw await manager.ForwardMessageAsync("$G", "test", null, new byte[] { 1 }, CancellationToken.None); manager.ForwardedJetStreamClusterMessages.ShouldBe(0); } // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public async Task Gateway_manager_propagate_without_connections_does_not_throw() { var options = new GatewayOptions { Name = "EMPTY", Host = "127.0.0.1", Port = 0, }; var manager = new GatewayManager( options, new ServerStats(), "S1", _ => { }, _ => { }, NullLogger.Instance); // These should not throw even without connections manager.PropagateLocalSubscription("$G", "test.>", null); manager.PropagateLocalUnsubscription("$G", "test.>", null); } // ── GatewayzHandler ───────────────────────────────────────────────── // Go: TestGatewayNoPanicOnStartupWithMonitoring server/gateway_test.go:6903 [Fact] public async Task Gatewayz_handler_returns_gateway_count() { await using var fixture = await GatewayConfigFixture.StartAsync(); var handler = new GatewayzHandler(fixture.Local); var result = handler.Build(); result.ShouldNotBeNull(); } // Go: TestGatewayNoPanicOnStartupWithMonitoring server/gateway_test.go:6903 [Fact] public async Task Gatewayz_handler_reflects_active_connections() { await using var fixture = await GatewayConfigFixture.StartAsync(); fixture.Local.Stats.Gateways.ShouldBeGreaterThan(0); } // ── Duplicate Remote Deduplication ─────────────────────────────────── // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public async Task Duplicate_remotes_are_deduplicated() { var localOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, Gateway = new GatewayOptions { Name = "LOCAL", Host = "127.0.0.1", Port = 0, }, }; var local = new NatsServer(localOptions, NullLoggerFactory.Instance); var localCts = new CancellationTokenSource(); _ = local.StartAsync(localCts.Token); await local.WaitForReadyAsync(); // Create remote with duplicate entries var remoteOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, Gateway = new GatewayOptions { Name = "REMOTE", Host = "127.0.0.1", Port = 0, Remotes = [local.GatewayListen!, local.GatewayListen!], }, }; var remote = new NatsServer(remoteOptions, NullLoggerFactory.Instance); var remoteCts = new CancellationTokenSource(); _ = remote.StartAsync(remoteCts.Token); await remote.WaitForReadyAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && (local.Stats.Gateways == 0 || remote.Stats.Gateways == 0)) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); // Should have exactly 1 gateway connection, not 2 // (remote deduplicates identical endpoints) local.Stats.Gateways.ShouldBeGreaterThan(0); remote.Stats.Gateways.ShouldBeGreaterThan(0); await localCts.CancelAsync(); await remoteCts.CancelAsync(); local.Dispose(); remote.Dispose(); localCts.Dispose(); remoteCts.Dispose(); } // ── ServerStats Gateway Fields ────────────────────────────────────── // Go: TestGatewayBasic server/gateway_test.go:399 [Fact] public void ServerStats_gateway_fields_initialized_to_zero() { var stats = new ServerStats(); stats.Gateways.ShouldBe(0); stats.SlowConsumerGateways.ShouldBe(0); stats.StaleConnectionGateways.ShouldBe(0); } // Go: TestGatewaySlowConsumer server/gateway_test.go:7003 [Fact] public void ServerStats_gateway_counter_atomic() { var stats = new ServerStats(); Interlocked.Increment(ref stats.Gateways); Interlocked.Increment(ref stats.Gateways); stats.Gateways.ShouldBe(2); Interlocked.Decrement(ref stats.Gateways); stats.Gateways.ShouldBe(1); } } /// /// Shared fixture for config tests. /// internal sealed class GatewayConfigFixture : IAsyncDisposable { private readonly CancellationTokenSource _localCts; private readonly CancellationTokenSource _remoteCts; private GatewayConfigFixture(NatsServer local, NatsServer remote, CancellationTokenSource localCts, CancellationTokenSource remoteCts) { Local = local; Remote = remote; _localCts = localCts; _remoteCts = remoteCts; } public NatsServer Local { get; } public NatsServer Remote { get; } public static async Task StartAsync() { var localOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, Gateway = new GatewayOptions { Name = "LOCAL", Host = "127.0.0.1", Port = 0, }, }; var local = new NatsServer(localOptions, NullLoggerFactory.Instance); var localCts = new CancellationTokenSource(); _ = local.StartAsync(localCts.Token); await local.WaitForReadyAsync(); var remoteOptions = new NatsOptions { Host = "127.0.0.1", Port = 0, Gateway = new GatewayOptions { Name = "REMOTE", Host = "127.0.0.1", Port = 0, Remotes = [local.GatewayListen!], }, }; var remote = new NatsServer(remoteOptions, NullLoggerFactory.Instance); var remoteCts = new CancellationTokenSource(); _ = remote.StartAsync(remoteCts.Token); await remote.WaitForReadyAsync(); using var timeout = new CancellationTokenSource(TimeSpan.FromSeconds(5)); while (!timeout.IsCancellationRequested && (local.Stats.Gateways == 0 || remote.Stats.Gateways == 0)) await Task.Delay(50, timeout.Token).ContinueWith(_ => { }, TaskScheduler.Default); return new GatewayConfigFixture(local, remote, localCts, remoteCts); } public async ValueTask DisposeAsync() { await _localCts.CancelAsync(); await _remoteCts.CancelAsync(); Local.Dispose(); Remote.Dispose(); _localCts.Dispose(); _remoteCts.Dispose(); } }