280 lines
11 KiB
C#
280 lines
11 KiB
C#
// Copyright 2012-2025 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.
|
||
|
||
using System.Net;
|
||
using System.Text.Json;
|
||
using Shouldly;
|
||
using ZB.MOM.NatsNet.Server;
|
||
using ZB.MOM.NatsNet.Server.Internal;
|
||
|
||
namespace ZB.MOM.NatsNet.Server.Tests.Internal;
|
||
|
||
/// <summary>
|
||
/// Tests for <see cref="ServerUtilities"/>.
|
||
/// Mirrors server/util_test.go: TestParseSize (ID 3061), TestParseSInt64 (ID 3062),
|
||
/// TestParseHostPort (ID 3063), TestURLsAreEqual (ID 3064), TestComma (ID 3065),
|
||
/// TestURLRedaction (ID 3066), TestVersionAtLeast (ID 3067).
|
||
/// Benchmarks (IDs 3068–3073) are n/a.
|
||
/// </summary>
|
||
public sealed class ServerUtilitiesTests
|
||
{
|
||
[Fact]
|
||
public void ParseSize_ShouldParseValidAndRejectInvalid()
|
||
{
|
||
// Mirror: TestParseSize
|
||
ServerUtilities.ParseSize(ReadOnlySpan<byte>.Empty).ShouldBe(-1, "nil/empty should return -1");
|
||
|
||
var n = "12345678"u8;
|
||
ServerUtilities.ParseSize(n).ShouldBe(12345678);
|
||
|
||
var bad = "12345invalid678"u8;
|
||
ServerUtilities.ParseSize(bad).ShouldBe(-1, "non-digit chars should return -1");
|
||
}
|
||
|
||
[Fact]
|
||
public void ParseInt64_ShouldParseValidAndRejectInvalid()
|
||
{
|
||
// Mirror: TestParseSInt64
|
||
ServerUtilities.ParseInt64(ReadOnlySpan<byte>.Empty).ShouldBe(-1L, "empty should return -1");
|
||
|
||
var n = "12345678"u8;
|
||
ServerUtilities.ParseInt64(n).ShouldBe(12345678L);
|
||
|
||
var bad = "12345invalid678"u8;
|
||
ServerUtilities.ParseInt64(bad).ShouldBe(-1L, "non-digit chars should return -1");
|
||
}
|
||
|
||
[Fact]
|
||
public void ParseHostPort_ShouldSplitCorrectly()
|
||
{
|
||
// Mirror: TestParseHostPort
|
||
void Check(string hostPort, int defaultPort, string expectedHost, int expectedPort, bool expectError)
|
||
{
|
||
var (host, port, err) = ServerUtilities.ParseHostPort(hostPort, defaultPort);
|
||
if (expectError)
|
||
{
|
||
err.ShouldNotBeNull($"expected error for hostPort={hostPort}");
|
||
return;
|
||
}
|
||
err.ShouldBeNull($"unexpected error for hostPort={hostPort}: {err?.Message}");
|
||
host.ShouldBe(expectedHost);
|
||
port.ShouldBe(expectedPort);
|
||
}
|
||
|
||
Check("addr:1234", 5678, "addr", 1234, false);
|
||
Check(" addr:1234 ", 5678, "addr", 1234, false);
|
||
Check(" addr : 1234 ", 5678, "addr", 1234, false);
|
||
Check("addr", 5678, "addr", 5678, false); // no port → default
|
||
Check(" addr ", 5678, "addr", 5678, false);
|
||
Check("addr:-1", 5678, "addr", 5678, false); // -1 → default
|
||
Check(" addr:-1 ", 5678, "addr", 5678, false);
|
||
Check(" addr : -1 ", 5678, "addr", 5678, false);
|
||
Check("addr:0", 5678, "addr", 5678, false); // 0 → default
|
||
Check(" addr:0 ", 5678, "addr", 5678, false);
|
||
Check(" addr : 0 ", 5678, "addr", 5678, false);
|
||
Check("addr:addr", 0, "", 0, true); // non-numeric port
|
||
Check("addr:::1234", 0, "", 0, true); // ambiguous colons
|
||
Check("", 0, "", 0, true); // empty
|
||
}
|
||
|
||
[Fact]
|
||
public void UrlsAreEqual_ShouldCompareCorrectly()
|
||
{
|
||
// Mirror: TestURLsAreEqual
|
||
void Check(string u1Str, string u2Str, bool expectedSame)
|
||
{
|
||
var u1 = new Uri(u1Str);
|
||
var u2 = new Uri(u2Str);
|
||
ServerUtilities.UrlsAreEqual(u1, u2).ShouldBe(expectedSame,
|
||
$"expected {u1Str} and {u2Str} to be {(expectedSame ? "equal" : "different")}");
|
||
}
|
||
|
||
Check("nats://localhost:4222", "nats://localhost:4222", true);
|
||
Check("nats://ivan:pwd@localhost:4222", "nats://ivan:pwd@localhost:4222", true);
|
||
Check("nats://ivan@localhost:4222", "nats://ivan@localhost:4222", true);
|
||
Check("nats://ivan:@localhost:4222", "nats://ivan:@localhost:4222", true);
|
||
Check("nats://host1:4222", "nats://host2:4222", false);
|
||
}
|
||
|
||
[Fact]
|
||
public void Comma_ShouldFormatWithThousandSeparators()
|
||
{
|
||
// Mirror: TestComma
|
||
var cases = new (long input, string expected)[]
|
||
{
|
||
(0, "0"),
|
||
(10, "10"),
|
||
(100, "100"),
|
||
(1_000, "1,000"),
|
||
(10_000, "10,000"),
|
||
(100_000, "100,000"),
|
||
(10_000_000, "10,000,000"),
|
||
(10_100_000, "10,100,000"),
|
||
(10_010_000, "10,010,000"),
|
||
(10_001_000, "10,001,000"),
|
||
(123_456_789, "123,456,789"),
|
||
(9_223_372_036_854_775_807L, "9,223,372,036,854,775,807"), // long.MaxValue
|
||
(long.MinValue, "-9,223,372,036,854,775,808"),
|
||
(-123_456_789, "-123,456,789"),
|
||
(-10_100_000, "-10,100,000"),
|
||
(-10_010_000, "-10,010,000"),
|
||
(-10_001_000, "-10,001,000"),
|
||
(-10_000_000, "-10,000,000"),
|
||
(-100_000, "-100,000"),
|
||
(-10_000, "-10,000"),
|
||
(-1_000, "-1,000"),
|
||
(-100, "-100"),
|
||
(-10, "-10"),
|
||
};
|
||
|
||
foreach (var (input, expected) in cases)
|
||
ServerUtilities.Comma(input).ShouldBe(expected, $"Comma({input})");
|
||
}
|
||
|
||
[Fact]
|
||
public void UrlRedaction_ShouldReplacePasswords()
|
||
{
|
||
// Mirror: TestURLRedaction
|
||
var cases = new (string full, string safe)[]
|
||
{
|
||
("nats://foo:bar@example.org", "nats://foo:xxxxx@example.org"),
|
||
("nats://foo@example.org", "nats://foo@example.org"),
|
||
("nats://example.org", "nats://example.org"),
|
||
("nats://example.org/foo?bar=1", "nats://example.org/foo?bar=1"),
|
||
};
|
||
|
||
var listFull = new Uri[cases.Length];
|
||
var listSafe = new Uri[cases.Length];
|
||
|
||
for (var i = 0; i < cases.Length; i++)
|
||
{
|
||
ServerUtilities.RedactUrlString(cases[i].full).ShouldBe(cases[i].safe,
|
||
$"RedactUrlString[{i}]");
|
||
listFull[i] = new Uri(cases[i].full);
|
||
listSafe[i] = new Uri(cases[i].safe);
|
||
}
|
||
|
||
var results = ServerUtilities.RedactUrlList(listFull);
|
||
for (var i = 0; i < results.Length; i++)
|
||
results[i].ToString().ShouldBe(listSafe[i].ToString(), $"RedactUrlList[{i}]");
|
||
}
|
||
|
||
[Fact]
|
||
public void VersionAtLeast_ShouldReturnCorrectResult()
|
||
{
|
||
// Mirror: TestVersionAtLeast
|
||
var cases = new (string version, int major, int minor, int update, bool result)[]
|
||
{
|
||
("2.0.0-beta", 1, 9, 9, true),
|
||
("2.0.0", 1, 99, 9, true),
|
||
("2.2.0", 2, 1, 9, true),
|
||
("2.2.2", 2, 2, 2, true),
|
||
("2.2.2", 2, 2, 3, false),
|
||
("2.2.2", 2, 3, 2, false),
|
||
("2.2.2", 3, 2, 2, false),
|
||
("2.22.2", 3, 0, 0, false),
|
||
("2.2.22", 2, 3, 0, false),
|
||
("bad.version",1, 2, 3, false),
|
||
};
|
||
|
||
foreach (var (version, major, minor, update, expected) in cases)
|
||
{
|
||
ServerUtilities.VersionAtLeast(version, major, minor, update)
|
||
.ShouldBe(expected,
|
||
$"VersionAtLeast({version}, {major}, {minor}, {update})");
|
||
}
|
||
}
|
||
|
||
[Fact]
|
||
public void RefCountedUrlSet_Wrappers_ShouldTrackRefCounts()
|
||
{
|
||
var set = new RefCountedUrlSet();
|
||
ServerUtilities.AddUrl(set, "nats://a:4222").ShouldBeTrue();
|
||
ServerUtilities.AddUrl(set, "nats://a:4222").ShouldBeFalse();
|
||
ServerUtilities.AddUrl(set, "nats://b:4222").ShouldBeTrue();
|
||
|
||
ServerUtilities.RemoveUrl(set, "nats://a:4222").ShouldBeFalse();
|
||
ServerUtilities.RemoveUrl(set, "nats://a:4222").ShouldBeTrue();
|
||
|
||
var urls = ServerUtilities.GetAsStringSlice(set);
|
||
urls.Length.ShouldBe(1);
|
||
urls[0].ShouldBe("nats://b:4222");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task NatsDialTimeout_ShouldConnectWithinTimeout()
|
||
{
|
||
using var listener = new System.Net.Sockets.TcpListener(IPAddress.Loopback, 0);
|
||
listener.Start();
|
||
var port = ((IPEndPoint)listener.LocalEndpoint).Port;
|
||
var acceptTask = listener.AcceptTcpClientAsync();
|
||
|
||
using var client = await ServerUtilities.NatsDialTimeout(
|
||
"tcp",
|
||
$"127.0.0.1:{port}",
|
||
TimeSpan.FromSeconds(2));
|
||
|
||
client.Connected.ShouldBeTrue();
|
||
using var accepted = await acceptTask;
|
||
accepted.Connected.ShouldBeTrue();
|
||
}
|
||
|
||
[Fact]
|
||
public void GenerateInfoJSON_ShouldEmitInfoLineWithCRLF()
|
||
{
|
||
var info = new ServerInfo
|
||
{
|
||
Id = "S1",
|
||
Name = "n1",
|
||
Host = "127.0.0.1",
|
||
Port = 4222,
|
||
Version = "2.0.0",
|
||
Proto = 1,
|
||
GoVersion = "go1.23",
|
||
};
|
||
|
||
var bytes = ServerUtilities.GenerateInfoJSON(info);
|
||
var line = System.Text.Encoding.UTF8.GetString(bytes);
|
||
line.ShouldStartWith("INFO ");
|
||
line.ShouldEndWith("\r\n");
|
||
|
||
var json = line["INFO ".Length..^2];
|
||
var payload = JsonSerializer.Deserialize<ServerInfo>(json);
|
||
payload.ShouldNotBeNull();
|
||
payload!.Id.ShouldBe("S1");
|
||
}
|
||
|
||
[Fact]
|
||
public async Task ParallelTaskQueue_ShouldExecuteQueuedActions()
|
||
{
|
||
var writer = ServerUtilities.ParallelTaskQueue(maxParallelism: 2);
|
||
var ran = 0;
|
||
var tcs = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
|
||
|
||
for (var i = 0; i < 4; i++)
|
||
{
|
||
var accepted = writer.TryWrite(() =>
|
||
{
|
||
if (Interlocked.Increment(ref ran) == 4)
|
||
tcs.TrySetResult();
|
||
});
|
||
accepted.ShouldBeTrue();
|
||
}
|
||
|
||
writer.TryComplete().ShouldBeTrue();
|
||
var finished = await Task.WhenAny(tcs.Task, Task.Delay(TimeSpan.FromSeconds(2)));
|
||
finished.ShouldBe(tcs.Task);
|
||
ran.ShouldBe(4);
|
||
}
|
||
}
|