// 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 System.Linq; using Shouldly; using Xunit; using ZB.MOM.NatsNet.Server.Auth; using ZB.MOM.NatsNet.Server.Internal; using ZB.MOM.NatsNet.Server.Protocol; namespace ZB.MOM.NatsNet.Server.Tests; /// /// Standalone unit tests for helper functions. /// Adapted from server/client_test.go. /// 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(() => 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); } [Fact] public void IsInternalClient_SystemJetStreamAccount_ShouldBeTrue() { ClientKindHelpers.IsInternalClient(ClientKind.System).ShouldBeTrue(); ClientKindHelpers.IsInternalClient(ClientKind.JetStream).ShouldBeTrue(); ClientKindHelpers.IsInternalClient(ClientKind.Account).ShouldBeTrue(); ClientKindHelpers.IsInternalClient(ClientKind.Client).ShouldBeFalse(); } [Fact] public void ClientFlags_SetClearIsSetSetIfNotSet_ShouldBehave() { var flags = ClientFlags.None; flags = flags.Set(ClientFlags.ConnectReceived); flags.IsSet(ClientFlags.ConnectReceived).ShouldBeTrue(); ClientFlagExtensions.SetIfNotSet(ref flags, ClientFlags.ConnectReceived).ShouldBeFalse(); ClientFlagExtensions.SetIfNotSet(ref flags, ClientFlags.InfoReceived).ShouldBeTrue(); flags.IsSet(ClientFlags.InfoReceived).ShouldBeTrue(); flags = flags.Clear(ClientFlags.ConnectReceived); flags.IsSet(ClientFlags.ConnectReceived).ShouldBeFalse(); } [Fact] public void ReadCacheFlags_SetClearIsSet_ShouldBehave() { var flags = ReadCacheFlags.None; flags = flags.Set(ReadCacheFlags.HasMappings); flags.IsSet(ReadCacheFlags.HasMappings).ShouldBeTrue(); flags = flags.Clear(ReadCacheFlags.HasMappings); flags.IsSet(ReadCacheFlags.HasMappings).ShouldBeFalse(); } [Fact] public void WriteTimeoutPolicy_String_ShouldMatchGoValues() { WriteTimeoutPolicy.Close.String().ShouldBe("close"); WriteTimeoutPolicy.Retry.String().ShouldBe("retry"); WriteTimeoutPolicy.Default.String().ShouldBe(string.Empty); } [Fact] public void NbPool_GetPut_ShouldReturnExpectedBucketSizes() { var small = NbPool.Get(10); var medium = NbPool.Get(NbPool.SmallSize + 1); var large = NbPool.Get(NbPool.MediumSize + 1); small.Length.ShouldBeGreaterThanOrEqualTo(NbPool.SmallSize); medium.Length.ShouldBeGreaterThanOrEqualTo(NbPool.MediumSize); large.Length.ShouldBeGreaterThanOrEqualTo(NbPool.LargeSize); NbPool.Put(small); NbPool.Put(medium); NbPool.Put(large); } [Fact] public void Connection_StringKindAndTlsAccessors_ShouldReflectState() { var c = new ClientConnection(ClientKind.Router); c.GetKind().ShouldBe(ClientKind.Router); c.String().ShouldBe(string.Empty); c.GetTLSConnectionState().ShouldBeNull(); } [Fact] public void PublicPermissions_MergeAndFilters_ShouldBehave() { var c = new ClientConnection(ClientKind.Client); c.RegisterUser(new User { Permissions = new Permissions { Publish = new SubjectPermission { Allow = ["foo"], Deny = ["deny.once"] }, Subscribe = new SubjectPermission { Allow = ["bar"], Deny = ["sub.deny"] }, Response = new ResponsePermission { MaxMsgs = 10, Expires = TimeSpan.FromSeconds(1) }, }, }); var initial = c.PublicPermissions(); initial.ShouldNotBeNull(); initial!.Publish!.Allow.ShouldContain("foo"); initial.Publish.Deny.ShouldContain("deny.once"); initial.Subscribe!.Allow.ShouldContain("bar"); initial.Subscribe.Deny.ShouldContain("sub.deny"); initial.Response.ShouldNotBeNull(); c.MergeDenyPermissions(DenyType.Pub, ["deny.once", "deny.two"]); c.MergeDenyPermissionsLocked(DenyType.Sub, ["sub.two"]); var merged = c.PublicPermissions(); merged.ShouldNotBeNull(); merged!.Publish!.Deny!.Count(s => s == "deny.once").ShouldBe(1); merged.Publish.Deny.ShouldContain("deny.two"); merged.Subscribe!.Deny.ShouldContain("sub.two"); c.LoadMsgDenyFilter(); c.MPerms.ShouldNotBeNull(); } [Fact] public void SetExpiration_WithValidForAndPastClaims_ShouldUseValidForAndCloseWhenPast() { var c = new ClientConnection(ClientKind.Client); c.SetExpiration(0, TimeSpan.FromMilliseconds(30)); c.IsClosed().ShouldBeFalse(); var wait = SpinWait.SpinUntil(c.IsClosed, TimeSpan.FromSeconds(2)); wait.ShouldBeTrue(); var c2 = new ClientConnection(ClientKind.Client); c2.SetExpiration(DateTimeOffset.UtcNow.AddSeconds(-1).ToUnixTimeSeconds(), TimeSpan.Zero); SpinWait.SpinUntil(c2.IsClosed, TimeSpan.FromSeconds(2)).ShouldBeTrue(); } [Fact] public void ReplyHelpers_ServiceAndReserved_ShouldClassifyPrefixes() { ClientConnection.IsServiceReply(Encoding.ASCII.GetBytes("_R_.A.B")).ShouldBeTrue(); ClientConnection.IsServiceReply(Encoding.ASCII.GetBytes("foo.bar")).ShouldBeFalse(); } } /// /// Standalone unit tests for functions. /// Adapted from server/client_test.go (header utility tests). /// 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!); } }