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.
295 lines
11 KiB
C#
295 lines
11 KiB
C#
// 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 */ }
|
|
}
|
|
}
|
|
}
|