feat: port session 10 — Server Core Runtime, Accept Loops & Listeners
Ports server/server.go lines 2577–4782 (~1,881 Go LOC), implementing ~97 features (IDs 3051–3147) across three new partial-class files. New files: - NatsServer.Lifecycle.cs: Shutdown, WaitForShutdown, RemoveClient, SendLDMToClients, LameDuckMode, LDMClientByID, rate-limit logging, DisconnectClientByID, SendAsyncInfoToClients - NatsServer.Listeners.cs: AcceptLoop, GetServerListener, InProcessConn, AcceptConnections, GenerateInfoJson, CopyInfo, CreateClient/Ex/InProcess, StartMonitoring (HTTP/HTTPS), AddConnectURLs/RemoveConnectURLs, TlsVersion/TlsVersionFromString, GetClientConnectURLs, ResolveHostPorts, PortsInfo/PortFile/LogPorts, ReadyForListeners, GetRandomIP, AcceptError - Internal/WaitGroup.cs: Go-style WaitGroup using TaskCompletionSource Modified: - Auth/AuthTypes.cs: Account now implements INatsAccount (stub) - NatsServerTypes.cs: ServerInfo.ShallowClone(), removed duplicate RefCountedUrlSet - NatsServer.cs: _info promoted to internal for test access - Properties/AssemblyInfo.cs: InternalsVisibleTo(DynamicProxyGenAssembly2) - ServerTests.cs: 20 new session-10 unit tests (GenerateInfoJson, TlsVersion, CopyInfo, GetRandomIP — Test IDs 2895, 2906) All 565 unit tests + 1 integration test pass.
This commit is contained in:
@@ -12,11 +12,16 @@
|
||||
// limitations under the License.
|
||||
//
|
||||
// Adapted from server/server_test.go in the NATS server Go source.
|
||||
// Session 09: standalone unit tests for NatsServer helpers.
|
||||
// Session 09–10: standalone unit tests for NatsServer helpers.
|
||||
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using NSubstitute;
|
||||
using NSubstitute.ExceptionExtensions;
|
||||
using Shouldly;
|
||||
using Xunit;
|
||||
using ZB.MOM.NatsNet.Server.Auth;
|
||||
|
||||
namespace ZB.MOM.NatsNet.Server.Tests;
|
||||
|
||||
@@ -249,3 +254,215 @@ public sealed class ServerTests
|
||||
public void NeedsCompression_S2Fast_ReturnsTrue()
|
||||
=> NatsServer.NeedsCompression(CompressionMode.S2Fast).ShouldBeTrue();
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Session 10: Listeners, INFO JSON, TLS helpers, GetRandomIP
|
||||
// =============================================================================
|
||||
|
||||
/// <summary>
|
||||
/// Tests for session 10 features: GenerateInfoJson, TlsVersion helpers,
|
||||
/// CopyInfo, and GetRandomIP.
|
||||
/// </summary>
|
||||
public sealed class ServerListenersTests
|
||||
{
|
||||
// =========================================================================
|
||||
// GenerateInfoJson (feature 3069) — Test ID 2906
|
||||
// Mirrors Go TestServerJsonMarshalNestedStructsPanic (guards against
|
||||
// marshaller panics with nested/nullable structs).
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void GenerateInfoJson_MinimalInfo_ProducesInfoFrame()
|
||||
{
|
||||
var info = new ServerInfo { Id = "TEST", Version = "1.0.0", Host = "0.0.0.0", Port = 4222 };
|
||||
var bytes = NatsServer.GenerateInfoJson(info);
|
||||
var text = Encoding.UTF8.GetString(bytes);
|
||||
text.ShouldStartWith("INFO {");
|
||||
text.ShouldEndWith("}\r\n");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateInfoJson_WithConnectUrls_IncludesUrls()
|
||||
{
|
||||
var info = new ServerInfo
|
||||
{
|
||||
Id = "TEST",
|
||||
Version = "1.0.0",
|
||||
ClientConnectUrls = ["nats://127.0.0.1:4222", "nats://127.0.0.1:4223"],
|
||||
};
|
||||
var text = Encoding.UTF8.GetString(NatsServer.GenerateInfoJson(info));
|
||||
text.ShouldContain("connect_urls");
|
||||
text.ShouldContain("4222");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GenerateInfoJson_WithSubjectPermissions_DoesNotThrow()
|
||||
{
|
||||
// Mirrors Go TestServerJsonMarshalNestedStructsPanic — guards against
|
||||
// JSON marshaller failures with nested nullable structs.
|
||||
var info = new ServerInfo
|
||||
{
|
||||
Id = "TEST",
|
||||
Version = "1.0.0",
|
||||
Import = new SubjectPermission { Allow = ["foo.>"], Deny = ["bar"] },
|
||||
Export = new SubjectPermission { Allow = ["pub.>"] },
|
||||
};
|
||||
var bytes = NatsServer.GenerateInfoJson(info);
|
||||
bytes.ShouldNotBeEmpty();
|
||||
// Strip the "INFO " prefix (5 bytes) and the trailing "\r\n" (2 bytes) to get pure JSON.
|
||||
var json = Encoding.UTF8.GetString(bytes, 5, bytes.Length - 7);
|
||||
var doc = JsonDocument.Parse(json);
|
||||
doc.RootElement.GetProperty("import").ValueKind.ShouldBe(JsonValueKind.Object);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// TlsVersion helpers (features 3079–3080)
|
||||
// =========================================================================
|
||||
|
||||
[Theory]
|
||||
[InlineData(0x0301u, "1.0")]
|
||||
[InlineData(0x0302u, "1.1")]
|
||||
[InlineData(0x0303u, "1.2")]
|
||||
[InlineData(0x0304u, "1.3")]
|
||||
public void TlsVersion_KnownCodes_ReturnsVersionString(uint ver, string expected)
|
||||
=> NatsServer.TlsVersion((ushort)ver).ShouldBe(expected);
|
||||
|
||||
[Fact]
|
||||
public void TlsVersion_UnknownCode_ReturnsUnknownLabel()
|
||||
=> NatsServer.TlsVersion(0xFFFF).ShouldStartWith("Unknown");
|
||||
|
||||
[Theory]
|
||||
[InlineData("1.0", (ushort)0x0301)]
|
||||
[InlineData("1.1", (ushort)0x0302)]
|
||||
[InlineData("1.2", (ushort)0x0303)]
|
||||
[InlineData("1.3", (ushort)0x0304)]
|
||||
public void TlsVersionFromString_KnownStrings_ReturnsCode(string input, ushort expected)
|
||||
{
|
||||
var (ver, err) = NatsServer.TlsVersionFromString(input);
|
||||
err.ShouldBeNull();
|
||||
ver.ShouldBe(expected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TlsVersionFromString_UnknownString_ReturnsError()
|
||||
{
|
||||
var (_, err) = NatsServer.TlsVersionFromString("9.9");
|
||||
err.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// CopyInfo (feature 3069)
|
||||
// =========================================================================
|
||||
|
||||
[Fact]
|
||||
public void CopyInfo_DeepCopiesSlices()
|
||||
{
|
||||
var (s, err) = NatsServer.NewServer(new ServerOptions());
|
||||
err.ShouldBeNull();
|
||||
s.ShouldNotBeNull();
|
||||
s!._info.ClientConnectUrls = ["nats://127.0.0.1:4222"];
|
||||
|
||||
var copy = s.CopyInfo();
|
||||
copy.ClientConnectUrls.ShouldNotBeNull();
|
||||
copy.ClientConnectUrls!.ShouldBe(["nats://127.0.0.1:4222"]);
|
||||
// Mutating original slice shouldn't affect the copy.
|
||||
s._info.ClientConnectUrls = ["nats://10.0.0.1:4222"];
|
||||
copy.ClientConnectUrls[0].ShouldBe("nats://127.0.0.1:4222");
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// GetRandomIP (feature 3141) — Test ID 2895
|
||||
// Mirrors Go TestGetRandomIP.
|
||||
// =========================================================================
|
||||
|
||||
private static NatsServer MakeServer()
|
||||
{
|
||||
var (s, err) = NatsServer.NewServer(new ServerOptions());
|
||||
err.ShouldBeNull();
|
||||
return s!;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRandomIP_NoPort_ReturnsFormatError()
|
||||
{
|
||||
var s = MakeServer();
|
||||
var resolver = Substitute.For<INetResolver>();
|
||||
var (_, err) = await s.GetRandomIP(resolver, "noport");
|
||||
err.ShouldNotBeNull();
|
||||
err!.Message.ShouldContain("port");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRandomIP_ResolverThrows_PropagatesError()
|
||||
{
|
||||
var s = MakeServer();
|
||||
var resolver = Substitute.For<INetResolver>();
|
||||
resolver.LookupHostAsync(Arg.Any<string>(), Arg.Any<CancellationToken>())
|
||||
.ThrowsAsync(new InvalidOperationException("on purpose"));
|
||||
var (_, err) = await s.GetRandomIP(resolver, "localhost:4222");
|
||||
err.ShouldNotBeNull();
|
||||
err!.Message.ShouldContain("on purpose");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRandomIP_EmptyIps_ReturnsFallbackUrl()
|
||||
{
|
||||
var s = MakeServer();
|
||||
var resolver = Substitute.For<INetResolver>();
|
||||
resolver.LookupHostAsync("localhost", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(Array.Empty<string>()));
|
||||
var (addr, err) = await s.GetRandomIP(resolver, "localhost:4222");
|
||||
err.ShouldBeNull();
|
||||
addr.ShouldBe("localhost:4222");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRandomIP_SingleIp_ReturnsMappedAddress()
|
||||
{
|
||||
var s = MakeServer();
|
||||
var resolver = Substitute.For<INetResolver>();
|
||||
resolver.LookupHostAsync("localhost", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(new[] { "1.2.3.4" }));
|
||||
var (addr, err) = await s.GetRandomIP(resolver, "localhost:4222");
|
||||
err.ShouldBeNull();
|
||||
addr.ShouldBe("1.2.3.4:4222");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRandomIP_MultipleIps_AllSelectedWithinRange()
|
||||
{
|
||||
var s = MakeServer();
|
||||
var resolver = Substitute.For<INetResolver>();
|
||||
resolver.LookupHostAsync("localhost", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(new[] { "1.2.3.4", "2.2.3.4", "3.2.3.4" }));
|
||||
|
||||
var dist = new int[3];
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var (addr, err) = await s.GetRandomIP(resolver, "localhost:4222");
|
||||
err.ShouldBeNull();
|
||||
dist[int.Parse(addr[..1]) - 1]++;
|
||||
}
|
||||
|
||||
// Each IP should appear at least once and no single IP should dominate.
|
||||
foreach (var d in dist)
|
||||
d.ShouldBeGreaterThan(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task GetRandomIP_ExcludedIp_NeverReturned()
|
||||
{
|
||||
var s = MakeServer();
|
||||
var resolver = Substitute.For<INetResolver>();
|
||||
resolver.LookupHostAsync("localhost", Arg.Any<CancellationToken>())
|
||||
.Returns(Task.FromResult(new[] { "1.2.3.4", "2.2.3.4", "3.2.3.4" }));
|
||||
|
||||
var excluded = new HashSet<string> { "1.2.3.4:4222" };
|
||||
for (int i = 0; i < 100; i++)
|
||||
{
|
||||
var (addr, err) = await s.GetRandomIP(resolver, "localhost:4222", excluded);
|
||||
err.ShouldBeNull();
|
||||
addr.ShouldNotBe("1.2.3.4:4222");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user