Move 25 gateway-related test files from NATS.Server.Tests into a dedicated NATS.Server.Gateways.Tests project. Update namespaces, replace private ReadUntilAsync with SocketTestHelper from TestUtilities, inline TestServerFactory usage, add InternalsVisibleTo, and register the project in the solution file. All 261 tests pass.
581 lines
18 KiB
C#
581 lines
18 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using NATS.Server.Configuration;
|
|
using NATS.Server.Gateways;
|
|
using NATS.Server.Monitoring;
|
|
|
|
namespace NATS.Server.Gateways.Tests.Gateways;
|
|
|
|
/// <summary>
|
|
/// Gateway configuration validation, options parsing, monitoring endpoint,
|
|
/// and server lifecycle tests.
|
|
/// Ported from golang/nats-server/server/gateway_test.go.
|
|
/// </summary>
|
|
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<GatewayManager>.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<GatewayManager>.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<GatewayManager>.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<GatewayManager>.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<GatewayManager>.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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Shared fixture for config tests.
|
|
/// </summary>
|
|
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<GatewayConfigFixture> 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();
|
|
}
|
|
}
|