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