Files

280 lines
11 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 30683073) 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);
}
}