test(batch56): port 66 reload and auth integration tests

Port config hot-reload (44 tests), opts (1 test), account isolation
(5 tests), auth callout (5 tests), and JWT validation (11 tests) from
Go reload_test.go, opts_test.go, accounts_test.go, auth_callout_test.go,
and jwt_test.go as behavioral blackbox integration tests against the
.NET NatsServer using ReloadOptions() and the public NATS client API.
This commit is contained in:
Joseph Doherty
2026-03-01 12:21:44 -05:00
parent 41ea272c8a
commit 96ca90672f
10 changed files with 5215 additions and 0 deletions

View File

@@ -0,0 +1,294 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Mirrors Go supercluster struct and createJetStreamSuperCluster* helpers from
// server/jetstream_helpers_test.go.
using ZB.MOM.NatsNet.Server;
namespace ZB.MOM.NatsNet.Server.IntegrationTests.Helpers;
/// <summary>
/// Represents a multi-cluster super-cluster connected via NATS gateways.
/// Mirrors Go <c>supercluster</c> struct from server/jetstream_helpers_test.go.
/// </summary>
internal sealed class TestSuperCluster : IDisposable
{
// =========================================================================
// Properties
// =========================================================================
/// <summary>All clusters that form this super-cluster.</summary>
public TestCluster[] Clusters { get; }
private bool _disposed;
// =========================================================================
// Constructor
// =========================================================================
private TestSuperCluster(TestCluster[] clusters)
{
Clusters = clusters;
}
// =========================================================================
// Static factory
// =========================================================================
/// <summary>
/// Creates a JetStream super-cluster consisting of <paramref name="numClusters"/> clusters,
/// each with <paramref name="numPerCluster"/> servers, connected via gateways.
/// Cluster names are C1, C2, … Cn.
/// Mirrors Go <c>createJetStreamSuperCluster</c>.
/// </summary>
public static TestSuperCluster CreateJetStreamSuperCluster(int numPerCluster, int numClusters)
{
if (numClusters <= 1)
throw new ArgumentException("numClusters must be > 1.", nameof(numClusters));
if (numPerCluster < 1)
throw new ArgumentException("numPerCluster must be >= 1.", nameof(numPerCluster));
// Allocate gateway ports — one per server across all clusters.
var totalServers = numClusters * numPerCluster;
var gatewayPorts = new int[totalServers];
for (var i = 0; i < totalServers; i++)
gatewayPorts[i] = TestServerHelper.GetFreePort();
// Build gateway remote-entry lines for each cluster.
// Each cluster has numPerCluster gateway ports.
var gwEntries = new string[numClusters];
for (var ci = 0; ci < numClusters; ci++)
{
var clusterName = $"C{ci + 1}";
var baseIndex = ci * numPerCluster;
var urls = string.Join(
",",
Enumerable.Range(baseIndex, numPerCluster)
.Select(idx => $"nats-gw://127.0.0.1:{gatewayPorts[idx]}"));
gwEntries[ci] = string.Format(
ConfigHelper.JsGatewayEntryTemplate,
"\n\t\t\t",
clusterName,
urls);
}
var allGwConf = string.Join(string.Empty, gwEntries);
// Create each cluster with the super-cluster gateway wrapper.
var clusters = new TestCluster[numClusters];
for (var ci = 0; ci < numClusters; ci++)
{
var clusterName = $"C{ci + 1}";
var gwBaseIndex = ci * numPerCluster;
// Allocate cluster-route ports for this sub-cluster.
var clusterPorts = Enumerable.Range(0, numPerCluster)
.Select(_ => TestServerHelper.GetFreePort())
.ToArray();
var routeUrls = string.Join(
",",
clusterPorts.Select(p => $"nats-route://127.0.0.1:{p}"));
var servers = new NatsServer[numPerCluster];
var opts = new ServerOptions[numPerCluster];
for (var si = 0; si < numPerCluster; si++)
{
var serverName = $"{clusterName}-S{si + 1}";
var storeDir = TestServerHelper.CreateTempDir($"js-sc-{clusterName}-{si + 1}-");
var gwPort = gatewayPorts[gwBaseIndex + si];
// Inner cluster config (using JsClusterTemplate).
var innerConf = string.Format(
ConfigHelper.JsClusterTemplate,
serverName,
storeDir,
clusterName,
clusterPorts[si],
routeUrls);
// Wrap with super-cluster template (gateway section).
var fullConf = string.Format(
ConfigHelper.JsSuperClusterTemplate,
innerConf,
clusterName,
gwPort,
allGwConf);
var configFile = ConfigHelper.CreateConfigFile(fullConf);
var serverOpts = new ServerOptions
{
ServerName = serverName,
Host = "127.0.0.1",
Port = -1,
NoLog = true,
NoSigs = true,
JetStream = true,
StoreDir = storeDir,
ConfigFile = configFile,
Cluster = new ClusterOpts
{
Name = clusterName,
Host = "127.0.0.1",
Port = clusterPorts[si],
},
Gateway = new GatewayOpts
{
Name = clusterName,
Host = "127.0.0.1",
Port = gwPort,
Gateways = Enumerable.Range(0, numClusters)
.Where(gci => gci != ci)
.Select(gci =>
{
var remoteName = $"C{gci + 1}";
var remoteBase = gci * numPerCluster;
return new RemoteGatewayOpts
{
Name = remoteName,
Urls = Enumerable.Range(remoteBase, numPerCluster)
.Select(idx => new Uri($"nats-gw://127.0.0.1:{gatewayPorts[idx]}"))
.ToList(),
};
})
.ToList(),
},
Routes = clusterPorts
.Where((_, idx) => idx != si)
.Select(p => new Uri($"nats-route://127.0.0.1:{p}"))
.ToList(),
};
var (server, _) = TestServerHelper.RunServer(serverOpts);
servers[si] = server;
opts[si] = serverOpts;
}
clusters[ci] = TestCluster.FromServers(servers, opts, clusterName);
}
var sc = new TestSuperCluster(clusters);
sc.WaitOnLeader();
return sc;
}
// =========================================================================
// Accessors
// =========================================================================
/// <summary>
/// Finds the JetStream meta-leader across all clusters.
/// Returns null if no leader is elected.
/// Mirrors Go <c>sc.leader()</c>.
/// </summary>
public NatsServer? Leader()
{
foreach (var c in Clusters)
{
var l = c.Leader();
if (l != null) return l;
}
return null;
}
/// <summary>
/// Returns a random running server from a random cluster.
/// Mirrors Go <c>sc.randomServer()</c>.
/// </summary>
public NatsServer RandomServer()
{
var cluster = Clusters[Random.Shared.Next(Clusters.Length)];
return cluster.RandomServer();
}
/// <summary>
/// Searches all clusters for a server with the given name.
/// Mirrors Go <c>sc.serverByName</c>.
/// </summary>
public NatsServer? ServerByName(string name)
{
foreach (var c in Clusters)
{
var s = c.ServerByName(name);
if (s != null) return s;
}
return null;
}
/// <summary>
/// Returns the <see cref="TestCluster"/> with the given cluster name (e.g. "C1").
/// Mirrors Go <c>sc.clusterForName</c>.
/// </summary>
public TestCluster? ClusterForName(string name)
{
foreach (var c in Clusters)
{
if (c.Name == name) return c;
}
return null;
}
// =========================================================================
// Wait helpers
// =========================================================================
/// <summary>
/// Waits until a JetStream meta-leader is elected across all clusters.
/// Mirrors Go <c>sc.waitOnLeader()</c>.
/// </summary>
public void WaitOnLeader()
{
CheckHelper.CheckFor(TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(100), () =>
{
if (Leader() == null)
return new Exception("SuperCluster: no JetStream meta-leader elected yet.");
return null;
});
}
/// <summary>
/// Waits until the named stream has an elected leader across all clusters.
/// Mirrors Go <c>sc.waitOnStreamLeader</c>.
/// </summary>
public void WaitOnStreamLeader(string account, string stream)
{
CheckHelper.CheckFor(TimeSpan.FromSeconds(30), TimeSpan.FromMilliseconds(100), () =>
{
foreach (var c in Clusters)
{
if (c.StreamLeader(account, stream) != null) return null;
}
return new Exception(
$"SuperCluster: no leader for stream '{stream}' in account '{account}'.");
});
}
// =========================================================================
// Lifecycle
// =========================================================================
/// <inheritdoc/>
public void Dispose()
{
if (_disposed) return;
_disposed = true;
foreach (var c in Clusters)
{
try { c.Dispose(); } catch { /* best effort */ }
}
}
}