feat: Wave 6 batch 1 — monitoring, config reload, client protocol, MQTT, leaf node tests

Port 405 new test methods across 5 subsystems for Go parity:
- Monitoring: 102 tests (varz, connz, routez, subsz, stacksz)
- Leaf Nodes: 85 tests (connection, forwarding, loop detection, subject filter, JetStream)
- MQTT Bridge: 86 tests (advanced, auth, retained messages, topic mapping, will messages)
- Client Protocol: 73 tests (connection handling, protocol violations, limits)
- Config Reload: 59 tests (hot reload, option changes, permission updates)

Total: 1,678 tests passing, 0 failures, 3 skipped
This commit is contained in:
Joseph Doherty
2026-02-23 21:40:29 -05:00
parent 921554f410
commit 9554d53bf5
19 changed files with 11040 additions and 0 deletions

View File

@@ -0,0 +1,345 @@
using Microsoft.Extensions.Logging.Abstractions;
using NATS.Client.Core;
using NATS.Server.Configuration;
namespace NATS.Server.Tests.LeafNodes;
/// <summary>
/// Tests for JetStream behavior over leaf node connections.
/// Reference: golang/nats-server/server/leafnode_test.go — TestLeafNodeJetStreamDomainMapCrossTalk, etc.
/// </summary>
public class LeafNodeJetStreamTests
{
// Go: TestLeafNodeJetStreamDomainMapCrossTalk server/leafnode_test.go:5948
[Fact]
public async Task JetStream_API_requests_reach_hub_with_JS_enabled()
{
var hubOptions = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
LeafNode = new LeafNodeOptions { Host = "127.0.0.1", Port = 0 },
JetStream = new JetStreamOptions { StoreDir = Path.Combine(Path.GetTempPath(), $"nats-js-hub-{Guid.NewGuid():N}") },
};
var hub = new NatsServer(hubOptions, NullLoggerFactory.Instance);
var hubCts = new CancellationTokenSource();
_ = hub.StartAsync(hubCts.Token);
await hub.WaitForReadyAsync();
var spokeOptions = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
LeafNode = new LeafNodeOptions
{
Host = "127.0.0.1",
Port = 0,
Remotes = [hub.LeafListen!],
},
};
var spoke = new NatsServer(spokeOptions, NullLoggerFactory.Instance);
var spokeCts = new CancellationTokenSource();
_ = spoke.StartAsync(spokeCts.Token);
await spoke.WaitForReadyAsync();
using var waitTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
while (!waitTimeout.IsCancellationRequested && (hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0))
await Task.Delay(50, waitTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
hub.Stats.JetStreamEnabled.ShouldBeTrue();
// Verify hub counts leaf
Interlocked.Read(ref hub.Stats.Leafs).ShouldBe(1);
await spokeCts.CancelAsync();
await hubCts.CancelAsync();
spoke.Dispose();
hub.Dispose();
spokeCts.Dispose();
hubCts.Dispose();
// Clean up store dir
if (Directory.Exists(hubOptions.JetStream.StoreDir))
Directory.Delete(hubOptions.JetStream.StoreDir, true);
}
// Go: TestLeafNodeJetStreamDomainMapCrossTalk server/leafnode_test.go:5948
[Fact]
public async Task JetStream_on_hub_receives_messages_published_from_leaf()
{
var storeDir = Path.Combine(Path.GetTempPath(), $"nats-js-leaf-{Guid.NewGuid():N}");
var hubOptions = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
LeafNode = new LeafNodeOptions { Host = "127.0.0.1", Port = 0 },
JetStream = new JetStreamOptions { StoreDir = storeDir },
};
var hub = new NatsServer(hubOptions, NullLoggerFactory.Instance);
var hubCts = new CancellationTokenSource();
_ = hub.StartAsync(hubCts.Token);
await hub.WaitForReadyAsync();
var spokeOptions = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
LeafNode = new LeafNodeOptions
{
Host = "127.0.0.1",
Port = 0,
Remotes = [hub.LeafListen!],
},
};
var spoke = new NatsServer(spokeOptions, NullLoggerFactory.Instance);
var spokeCts = new CancellationTokenSource();
_ = spoke.StartAsync(spokeCts.Token);
await spoke.WaitForReadyAsync();
using var waitTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
while (!waitTimeout.IsCancellationRequested && (hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0))
await Task.Delay(50, waitTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
// Subscribe on hub for a subject
await using var hubConn = new NatsConnection(new NatsOpts
{
Url = $"nats://127.0.0.1:{hub.Port}",
});
await hubConn.ConnectAsync();
await using var sub = await hubConn.SubscribeCoreAsync<string>("js.leaf.test");
await hubConn.PingAsync();
// Wait for interest propagation
using var interestTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
while (!interestTimeout.IsCancellationRequested && !spoke.HasRemoteInterest("js.leaf.test"))
await Task.Delay(50, interestTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
// Publish from spoke
await using var spokeConn = new NatsConnection(new NatsOpts
{
Url = $"nats://127.0.0.1:{spoke.Port}",
});
await spokeConn.ConnectAsync();
await spokeConn.PublishAsync("js.leaf.test", "from-leaf-to-js");
using var msgTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var msg = await sub.Msgs.ReadAsync(msgTimeout.Token);
msg.Data.ShouldBe("from-leaf-to-js");
await spokeCts.CancelAsync();
await hubCts.CancelAsync();
spoke.Dispose();
hub.Dispose();
spokeCts.Dispose();
hubCts.Dispose();
if (Directory.Exists(storeDir))
Directory.Delete(storeDir, true);
}
// Go: TestLeafNodeStreamImport server/leafnode_test.go:3441
[Fact]
public async Task Leaf_node_with_JetStream_disabled_spoke_still_forwards_messages()
{
var storeDir = Path.Combine(Path.GetTempPath(), $"nats-js-fwd-{Guid.NewGuid():N}");
var hubOptions = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
LeafNode = new LeafNodeOptions { Host = "127.0.0.1", Port = 0 },
JetStream = new JetStreamOptions { StoreDir = storeDir },
};
var hub = new NatsServer(hubOptions, NullLoggerFactory.Instance);
var hubCts = new CancellationTokenSource();
_ = hub.StartAsync(hubCts.Token);
await hub.WaitForReadyAsync();
// Spoke without JetStream
var spokeOptions = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
LeafNode = new LeafNodeOptions
{
Host = "127.0.0.1",
Port = 0,
Remotes = [hub.LeafListen!],
},
};
var spoke = new NatsServer(spokeOptions, NullLoggerFactory.Instance);
var spokeCts = new CancellationTokenSource();
_ = spoke.StartAsync(spokeCts.Token);
await spoke.WaitForReadyAsync();
using var waitTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
while (!waitTimeout.IsCancellationRequested && (hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0))
await Task.Delay(50, waitTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
hub.Stats.JetStreamEnabled.ShouldBeTrue();
spoke.Stats.JetStreamEnabled.ShouldBeFalse();
// Subscribe on hub
await using var hubConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{hub.Port}" });
await hubConn.ConnectAsync();
await using var sub = await hubConn.SubscribeCoreAsync<string>("njs.forward");
await hubConn.PingAsync();
using var interestTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
while (!interestTimeout.IsCancellationRequested && !spoke.HasRemoteInterest("njs.forward"))
await Task.Delay(50, interestTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
// Publish from spoke
await using var spokeConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{spoke.Port}" });
await spokeConn.ConnectAsync();
await spokeConn.PublishAsync("njs.forward", "no-js-spoke");
using var msgTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var msg = await sub.Msgs.ReadAsync(msgTimeout.Token);
msg.Data.ShouldBe("no-js-spoke");
await spokeCts.CancelAsync();
await hubCts.CancelAsync();
spoke.Dispose();
hub.Dispose();
spokeCts.Dispose();
hubCts.Dispose();
if (Directory.Exists(storeDir))
Directory.Delete(storeDir, true);
}
// Go: TestLeafNodeJetStreamDomainMapCrossTalk server/leafnode_test.go:5948
[Fact]
public async Task Both_hub_and_spoke_with_JetStream_enabled_connect_successfully()
{
var hubStoreDir = Path.Combine(Path.GetTempPath(), $"nats-js-hub2-{Guid.NewGuid():N}");
var spokeStoreDir = Path.Combine(Path.GetTempPath(), $"nats-js-spoke2-{Guid.NewGuid():N}");
var hubOptions = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
LeafNode = new LeafNodeOptions { Host = "127.0.0.1", Port = 0 },
JetStream = new JetStreamOptions { StoreDir = hubStoreDir },
};
var hub = new NatsServer(hubOptions, NullLoggerFactory.Instance);
var hubCts = new CancellationTokenSource();
_ = hub.StartAsync(hubCts.Token);
await hub.WaitForReadyAsync();
var spokeOptions = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
LeafNode = new LeafNodeOptions
{
Host = "127.0.0.1",
Port = 0,
Remotes = [hub.LeafListen!],
},
JetStream = new JetStreamOptions { StoreDir = spokeStoreDir },
};
var spoke = new NatsServer(spokeOptions, NullLoggerFactory.Instance);
var spokeCts = new CancellationTokenSource();
_ = spoke.StartAsync(spokeCts.Token);
await spoke.WaitForReadyAsync();
using var waitTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
while (!waitTimeout.IsCancellationRequested && (hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0))
await Task.Delay(50, waitTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
hub.Stats.JetStreamEnabled.ShouldBeTrue();
spoke.Stats.JetStreamEnabled.ShouldBeTrue();
Interlocked.Read(ref hub.Stats.Leafs).ShouldBe(1);
await spokeCts.CancelAsync();
await hubCts.CancelAsync();
spoke.Dispose();
hub.Dispose();
spokeCts.Dispose();
hubCts.Dispose();
if (Directory.Exists(hubStoreDir))
Directory.Delete(hubStoreDir, true);
if (Directory.Exists(spokeStoreDir))
Directory.Delete(spokeStoreDir, true);
}
// Go: TestLeafNodeStreamAndShadowSubs server/leafnode_test.go:6176
[Fact]
public async Task Leaf_node_message_forwarding_works_alongside_JetStream()
{
var storeDir = Path.Combine(Path.GetTempPath(), $"nats-js-combo-{Guid.NewGuid():N}");
var hubOptions = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
LeafNode = new LeafNodeOptions { Host = "127.0.0.1", Port = 0 },
JetStream = new JetStreamOptions { StoreDir = storeDir },
};
var hub = new NatsServer(hubOptions, NullLoggerFactory.Instance);
var hubCts = new CancellationTokenSource();
_ = hub.StartAsync(hubCts.Token);
await hub.WaitForReadyAsync();
var spokeOptions = new NatsOptions
{
Host = "127.0.0.1",
Port = 0,
LeafNode = new LeafNodeOptions
{
Host = "127.0.0.1",
Port = 0,
Remotes = [hub.LeafListen!],
},
};
var spoke = new NatsServer(spokeOptions, NullLoggerFactory.Instance);
var spokeCts = new CancellationTokenSource();
_ = spoke.StartAsync(spokeCts.Token);
await spoke.WaitForReadyAsync();
using var waitTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
while (!waitTimeout.IsCancellationRequested && (hub.Stats.Leafs == 0 || spoke.Stats.Leafs == 0))
await Task.Delay(50, waitTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
// Regular pub/sub should still work alongside JS
await using var leafConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{spoke.Port}" });
await leafConn.ConnectAsync();
await using var hubConn = new NatsConnection(new NatsOpts { Url = $"nats://127.0.0.1:{hub.Port}" });
await hubConn.ConnectAsync();
await using var sub = await leafConn.SubscribeCoreAsync<string>("combo.test");
await leafConn.PingAsync();
using var interestTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
while (!interestTimeout.IsCancellationRequested && !hub.HasRemoteInterest("combo.test"))
await Task.Delay(50, interestTimeout.Token).ContinueWith(_ => { }, TaskScheduler.Default);
await hubConn.PublishAsync("combo.test", "js-combo");
using var msgTimeout = new CancellationTokenSource(TimeSpan.FromSeconds(5));
var msg = await sub.Msgs.ReadAsync(msgTimeout.Token);
msg.Data.ShouldBe("js-combo");
await spokeCts.CancelAsync();
await hubCts.CancelAsync();
spoke.Dispose();
hub.Dispose();
spokeCts.Dispose();
hubCts.Dispose();
if (Directory.Exists(storeDir))
Directory.Delete(storeDir, true);
}
}