Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/ClientTests.cs
Joseph Doherty 11b387e442 feat: port session 08 — Client Connection & PROXY Protocol
- ClientConnection: full connection lifecycle, string/identity helpers,
  SplitSubjectQueue, KindString, MsgParts, SetHeader, message header
  manipulation (GenHeader, RemoveHeader, SliceHeader, GetHeader)
- ClientTypes: ClientConnectionType, ClientProtocol, ClientFlags,
  ReadCacheFlags, ClosedState, PmrFlags, DenyType, ClientOptions,
  ClientInfo, NbPool, RouteTarget, ClientKindHelpers
- NatsMessageHeaders: complete header utility class (GenHeader,
  RemoveHeaderIfPrefixPresent, RemoveHeaderIfPresent, SliceHeader,
  GetHeader, SetHeader, GetHeaderKeyIndex)
- ProxyProtocol: PROXY protocol v1/v2 parser (ReadV1Header,
  ParseV2Header, ReadProxyProtoHeader sync entry point)
- ServerErrors: add ErrAuthorization sentinel
- Tests: 32 standalone unit tests (proxy protocol: IDs 159-168,
  171-178, 180-181; client: IDs 200-201, 247-256)
- DB: 195 features → complete (387-581); 32 tests → complete;
  81 server-dependent tests → n/a

Features: 667 complete, 274 unit tests complete (17.2% overall)
2026-02-26 13:50:38 -05:00

321 lines
14 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.
//
// Adapted from server/client_test.go in the NATS server Go source.
using System.Text;
using Shouldly;
using Xunit;
using ZB.MOM.NatsNet.Server.Internal;
using ZB.MOM.NatsNet.Server.Protocol;
namespace ZB.MOM.NatsNet.Server.Tests;
/// <summary>
/// Standalone unit tests for <see cref="ClientConnection"/> helper functions.
/// Adapted from server/client_test.go.
/// </summary>
public sealed class ClientTests
{
// =========================================================================
// TestSplitSubjectQueue — Test ID 200
// =========================================================================
[Theory]
[InlineData("foo", "foo", null, false)]
[InlineData("foo bar", "foo", "bar", false)]
[InlineData(" foo bar ", "foo", "bar", false)]
[InlineData("foo bar", "foo", "bar", false)]
[InlineData("foo bar fizz", null, null, true)]
public void SplitSubjectQueue_TableDriven(
string sq, string? wantSubject, string? wantQueue, bool wantErr)
{
if (wantErr)
{
Should.Throw<Exception>(() => ClientConnection.SplitSubjectQueue(sq));
}
else
{
var (subject, queue) = ClientConnection.SplitSubjectQueue(sq);
subject.ShouldBe(wantSubject is null ? null : Encoding.ASCII.GetBytes(wantSubject));
queue.ShouldBe(wantQueue is null ? null : Encoding.ASCII.GetBytes(wantQueue));
}
}
// =========================================================================
// TestTypeString — Test ID 201
// =========================================================================
[Theory]
[InlineData(ClientKind.Client, "Client")]
[InlineData(ClientKind.Router, "Router")]
[InlineData(ClientKind.Gateway, "Gateway")]
[InlineData(ClientKind.Leaf, "Leafnode")]
[InlineData(ClientKind.JetStream, "JetStream")]
[InlineData(ClientKind.Account, "Account")]
[InlineData(ClientKind.System, "System")]
[InlineData((ClientKind)(-1), "Unknown Type")]
public void KindString_ReturnsExpectedString(ClientKind kind, string expected)
{
var c = new ClientConnection(kind);
c.KindString().ShouldBe(expected);
}
}
/// <summary>
/// Standalone unit tests for <see cref="NatsMessageHeaders"/> functions.
/// Adapted from server/client_test.go (header utility tests).
/// </summary>
public sealed class NatsMessageHeadersTests
{
// =========================================================================
// TestRemoveHeaderIfPrefixPresent — Test ID 247
// =========================================================================
[Fact]
public void RemoveHeaderIfPrefixPresent_RemovesMatchingHeaders()
{
byte[]? hdr = null;
hdr = NatsMessageHeaders.GenHeader(hdr, "a", "1");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedStream, "my-stream");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSeq, "22");
hdr = NatsMessageHeaders.GenHeader(hdr, "b", "2");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastMsgId, "1");
hdr = NatsMessageHeaders.GenHeader(hdr, "c", "3");
hdr = NatsMessageHeaders.RemoveHeaderIfPrefixPresent(hdr!, "Nats-Expected-");
var expected = Encoding.ASCII.GetBytes("NATS/1.0\r\na: 1\r\nb: 2\r\nc: 3\r\n\r\n");
hdr.ShouldBe(expected);
}
// =========================================================================
// TestSliceHeader — Test ID 248
// =========================================================================
[Fact]
public void SliceHeader_ReturnsCorrectSlice()
{
byte[]? hdr = null;
hdr = NatsMessageHeaders.GenHeader(hdr, "a", "1");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedStream, "my-stream");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSeq, "22");
hdr = NatsMessageHeaders.GenHeader(hdr, "b", "2");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastMsgId, "1");
hdr = NatsMessageHeaders.GenHeader(hdr, "c", "3");
var sliced = NatsMessageHeaders.SliceHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);
var copied = NatsMessageHeaders.GetHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);
sliced.ShouldNotBeNull();
sliced!.Value.Length.ShouldBe(2); // "24" is 2 bytes
copied.ShouldNotBeNull();
sliced.Value.ToArray().ShouldBe(copied!);
}
// =========================================================================
// TestSliceHeaderOrderingPrefix — Test ID 249
// =========================================================================
[Fact]
public void SliceHeader_OrderingPrefix_LongerHeaderDoesNotPreemptShorter()
{
byte[]? hdr = null;
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
var sliced = NatsMessageHeaders.SliceHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);
var copied = NatsMessageHeaders.GetHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, hdr!);
sliced.ShouldNotBeNull();
sliced!.Value.Length.ShouldBe(2);
copied.ShouldNotBeNull();
sliced.Value.ToArray().ShouldBe(copied!);
}
// =========================================================================
// TestSliceHeaderOrderingSuffix — Test ID 250
// =========================================================================
[Fact]
public void SliceHeader_OrderingSuffix_LongerHeaderDoesNotPreemptShorter()
{
byte[]? hdr = null;
hdr = NatsMessageHeaders.GenHeader(hdr, "Previous-Nats-Msg-Id", "user");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsMsgId, "control");
var sliced = NatsMessageHeaders.SliceHeader(NatsHeaderConstants.JsMsgId, hdr!);
var copied = NatsMessageHeaders.GetHeader(NatsHeaderConstants.JsMsgId, hdr!);
sliced.ShouldNotBeNull();
copied.ShouldNotBeNull();
sliced!.Value.ToArray().ShouldBe(copied!);
Encoding.ASCII.GetString(copied!).ShouldBe("control");
}
// =========================================================================
// TestRemoveHeaderIfPresentOrderingPrefix — Test ID 251
// =========================================================================
[Fact]
public void RemoveHeaderIfPresent_OrderingPrefix_OnlyRemovesExactKey()
{
byte[]? hdr = null;
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
hdr = NatsMessageHeaders.RemoveHeaderIfPresent(hdr!, NatsHeaderConstants.JsExpectedLastSubjSeq);
var expected = NatsMessageHeaders.GenHeader(null, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
hdr!.ShouldBe(expected);
}
// =========================================================================
// TestRemoveHeaderIfPresentOrderingSuffix — Test ID 252
// =========================================================================
[Fact]
public void RemoveHeaderIfPresent_OrderingSuffix_OnlyRemovesExactKey()
{
byte[]? hdr = null;
hdr = NatsMessageHeaders.GenHeader(hdr, "Previous-Nats-Msg-Id", "user");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsMsgId, "control");
hdr = NatsMessageHeaders.RemoveHeaderIfPresent(hdr!, NatsHeaderConstants.JsMsgId);
var expected = NatsMessageHeaders.GenHeader(null, "Previous-Nats-Msg-Id", "user");
hdr!.ShouldBe(expected);
}
// =========================================================================
// TestMsgPartsCapsHdrSlice — Test ID 253
// =========================================================================
[Fact]
public void MsgParts_HeaderSliceIsIsolatedCopy()
{
const string hdrContent = NatsHeaderConstants.HdrLine + "Key1: Val1\r\nKey2: Val2\r\n\r\n";
const string msgBody = "hello\r\n";
var buf = Encoding.ASCII.GetBytes(hdrContent + msgBody);
var c = new ClientConnection(ClientKind.Client);
c.ParseCtx.Pa.HeaderSize = hdrContent.Length;
var (hdr, msg) = c.MsgParts(buf);
// Header and body should have correct content.
Encoding.ASCII.GetString(hdr).ShouldBe(hdrContent);
Encoding.ASCII.GetString(msg).ShouldBe(msgBody);
// hdr should be shorter than buf (cap(hdr) < cap(buf) in Go).
hdr.Length.ShouldBeLessThan(buf.Length);
// Appending to hdr should not affect msg.
var extended = hdr.Concat(Encoding.ASCII.GetBytes("test")).ToArray();
Encoding.ASCII.GetString(extended).ShouldBe(hdrContent + "test");
Encoding.ASCII.GetString(msg).ShouldBe("hello\r\n");
}
// =========================================================================
// TestSetHeaderDoesNotOverwriteUnderlyingBuffer — Test ID 254
// =========================================================================
[Theory]
[InlineData("Key1", "Val1Updated", "NATS/1.0\r\nKey1: Val1Updated\r\nKey2: Val2\r\n\r\n", true)]
[InlineData("Key1", "v1", "NATS/1.0\r\nKey1: v1\r\nKey2: Val2\r\n\r\n", false)]
[InlineData("Key3", "Val3", "NATS/1.0\r\nKey1: Val1\r\nKey2: Val2\r\nKey3: Val3\r\n\r\n", true)]
public void SetHeader_DoesNotOverwriteUnderlyingBuffer(
string key, string val, string expectedHdr, bool isNewBuf)
{
const string initialHdr = "NATS/1.0\r\nKey1: Val1\r\nKey2: Val2\r\n\r\n";
const string msgBody = "this is the message body\r\n";
var buf = new byte[initialHdr.Length + msgBody.Length];
Encoding.ASCII.GetBytes(initialHdr).CopyTo(buf, 0);
Encoding.ASCII.GetBytes(msgBody).CopyTo(buf, initialHdr.Length);
var hdrSlice = buf[..initialHdr.Length];
var msgSlice = buf[initialHdr.Length..];
var updatedHdr = NatsMessageHeaders.SetHeader(key, val, hdrSlice);
Encoding.ASCII.GetString(updatedHdr).ShouldBe(expectedHdr);
Encoding.ASCII.GetString(msgSlice).ShouldBe(msgBody);
if (isNewBuf)
{
// New allocation: original buf's header portion must be unchanged.
Encoding.ASCII.GetString(buf, 0, initialHdr.Length).ShouldBe(initialHdr);
}
else
{
// In-place update: C# array slices are copies (not views like Go), so buf
// is unchanged. However, hdrSlice (the array passed to SetHeader) IS
// modified in place via Buffer.BlockCopy.
Encoding.ASCII.GetString(hdrSlice, 0, expectedHdr.Length).ShouldBe(expectedHdr);
}
}
// =========================================================================
// TestSetHeaderOrderingPrefix — Test ID 255
// =========================================================================
[Theory]
[InlineData(true)]
[InlineData(false)]
public void SetHeader_OrderingPrefix_LongerHeaderDoesNotPreemptShorter(bool withSpaces)
{
byte[]? hdr = null;
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsExpectedLastSubjSeq, "24");
if (!withSpaces)
hdr = hdr!.Where(b => b != (byte)' ').ToArray();
hdr = NatsMessageHeaders.SetHeader(NatsHeaderConstants.JsExpectedLastSubjSeq, "12", hdr!);
byte[]? expected = null;
expected = NatsMessageHeaders.GenHeader(expected, NatsHeaderConstants.JsExpectedLastSubjSeqSubj, "foo");
expected = NatsMessageHeaders.GenHeader(expected, NatsHeaderConstants.JsExpectedLastSubjSeq, "12");
if (!withSpaces)
expected = expected!.Where(b => b != (byte)' ').ToArray();
hdr!.ShouldBe(expected!);
}
// =========================================================================
// TestSetHeaderOrderingSuffix — Test ID 256
// =========================================================================
[Theory]
[InlineData(true)]
[InlineData(false)]
public void SetHeader_OrderingSuffix_LongerHeaderDoesNotPreemptShorter(bool withSpaces)
{
byte[]? hdr = null;
hdr = NatsMessageHeaders.GenHeader(hdr, "Previous-Nats-Msg-Id", "user");
hdr = NatsMessageHeaders.GenHeader(hdr, NatsHeaderConstants.JsMsgId, "control");
if (!withSpaces)
hdr = hdr!.Where(b => b != (byte)' ').ToArray();
hdr = NatsMessageHeaders.SetHeader(NatsHeaderConstants.JsMsgId, "other", hdr!);
byte[]? expected = null;
expected = NatsMessageHeaders.GenHeader(expected, "Previous-Nats-Msg-Id", "user");
expected = NatsMessageHeaders.GenHeader(expected, NatsHeaderConstants.JsMsgId, "other");
if (!withSpaces)
expected = expected!.Where(b => b != (byte)' ').ToArray();
hdr!.ShouldBe(expected!);
}
}