Add comprehensive Go-parity test coverage across 5 subsystems: - Accounts/Auth: isolation, import/export, auth mechanisms, permissions (82 tests) - Gateways: connection, forwarding, interest mode, config (106 tests) - Routes: connection, subscription, forwarding, config validation (78 tests) - JetStream API: stream/consumer CRUD, pub/sub, features, admin (234 tests) - JetStream Cluster: streams, consumers, failover, meta (108 tests) Total: ~608 new test annotations across 22 files (+13,844 lines) All tests pass individually; suite total: 2,283 passing, 3 skipped
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.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();
|
|
}
|
|
}
|