// Copyright 2024-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/msgtrace_test.go, server/routes_test.go, // server/filestore_test.go, server/server_test.go, server/memstore_test.go, // server/gateway_test.go, server/websocket_test.go in the NATS server Go source. using System.Buffers.Binary; using System.Text; using Shouldly; using ZB.MOM.NatsNet.Server; using ZB.MOM.NatsNet.Server.Internal; using ZB.MOM.NatsNet.Server.WebSocket; namespace ZB.MOM.NatsNet.Server.IntegrationTests; /// /// Miscellaneous integration tests ported from multiple Go test files: /// - server/msgtrace_test.go (7 tests) /// - server/routes_test.go (5 tests) /// - server/filestore_test.go (6 tests) /// - server/server_test.go (1 test) /// - server/memstore_test.go (1 test) /// - server/gateway_test.go (1 test) /// - server/websocket_test.go (1 test) /// Total: 22 tests. /// [Trait("Category", "Integration")] public sealed class MiscTests { // ========================================================================= // msgtrace_test.go — 7 tests // ========================================================================= // ------------------------------------------------------------------------- // TestMsgTraceConnName (T:3063) — structural variant // ------------------------------------------------------------------------- /// /// Verifies that GetConnName returns the remote name for routers, /// gateways and leaf nodes, and falls back to the client opts name otherwise. /// Mirrors Go TestMsgTraceConnName in server/msgtrace_test.go. /// [Fact] public void MsgTraceConnName_ShouldSucceed() { // Router — remote name takes precedence var router = new ClientConnection(ClientKind.Router); router.Route = new Route { RemoteName = "somename" }; router.Opts.Name = "someid"; MsgTraceHelper.GetConnName(router).ShouldBe("somename"); // Router — falls back to opts.Name when remote name is empty router.Route.RemoteName = string.Empty; MsgTraceHelper.GetConnName(router).ShouldBe("someid"); // Gateway — remote name takes precedence var gw = new ClientConnection(ClientKind.Gateway); gw.Gateway = new Gateway { RemoteName = "somename" }; gw.Opts.Name = "someid"; MsgTraceHelper.GetConnName(gw).ShouldBe("somename"); // Gateway — falls back to opts.Name gw.Gateway.RemoteName = string.Empty; MsgTraceHelper.GetConnName(gw).ShouldBe("someid"); // Leaf node — remote server takes precedence var leaf = new ClientConnection(ClientKind.Leaf); leaf.Leaf = new Leaf { RemoteServer = "somename" }; leaf.Opts.Name = "someid"; MsgTraceHelper.GetConnName(leaf).ShouldBe("somename"); // Leaf node — falls back to opts.Name leaf.Leaf.RemoteServer = string.Empty; MsgTraceHelper.GetConnName(leaf).ShouldBe("someid"); // Client — always uses opts.Name var client = new ClientConnection(ClientKind.Client); client.Opts.Name = "someid"; MsgTraceHelper.GetConnName(client).ShouldBe("someid"); } // ------------------------------------------------------------------------- // TestMsgTraceGenHeaderMap — no-trace-header cases (T:3064) // ------------------------------------------------------------------------- /// /// Verifies that GenHeaderMapIfTraceHeadersPresent returns an empty map /// when no trace headers are present. /// Mirrors the negative cases in TestMsgTraceGenHeaderMap. /// [Fact] public void MsgTraceGenHeaderMap_NoTraceHeader_ReturnsEmpty_ShouldSucceed() { // Missing header line var (m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent( Encoding.ASCII.GetBytes("Nats-Trace-Dest: val\r\n")); m.Count.ShouldBe(0); ext.ShouldBeFalse(); // No trace header var noTrace = Encoding.ASCII.GetBytes("NATS/1.0\r\nHeader1: val1\r\nHeader2: val2\r\n"); (m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(noTrace); m.Count.ShouldBe(0); ext.ShouldBeFalse(); } // ------------------------------------------------------------------------- // TestMsgTraceGenHeaderMap — trace header found (T:3065) // ------------------------------------------------------------------------- /// /// Verifies that GenHeaderMapIfTraceHeadersPresent correctly parses /// headers when the Nats-Trace-Dest header is present. /// Mirrors the positive cases in TestMsgTraceGenHeaderMap. /// [Fact] public void MsgTraceGenHeaderMap_TraceHeaderPresent_ShouldSucceed() { // Trace header first var header = Encoding.ASCII.GetBytes( "NATS/1.0\r\nNats-Trace-Dest: some.dest\r\nSome-Header: some value\r\n"); var (m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header); ext.ShouldBeFalse(); m.ShouldContainKey("Nats-Trace-Dest"); m["Nats-Trace-Dest"].ShouldContain("some.dest"); m.ShouldContainKey("Some-Header"); // Trace header last header = Encoding.ASCII.GetBytes( "NATS/1.0\r\nSome-Header: some value\r\nNats-Trace-Dest: some.dest\r\n"); (m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header); ext.ShouldBeFalse(); m.ShouldContainKey("Nats-Trace-Dest"); } // ------------------------------------------------------------------------- // TestMsgTraceGenHeaderMap — external traceparent sampling (T:3066) // ------------------------------------------------------------------------- /// /// Verifies that an enabled traceparent header triggers external tracing. /// Mirrors the external header cases in TestMsgTraceGenHeaderMap. /// [Fact] public void MsgTraceGenHeaderMap_ExternalTraceparent_ShouldSucceed() { // External header with sampling enabled (flags=01) var header = Encoding.ASCII.GetBytes( "NATS/1.0\r\ntraceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01\r\nSome-Header: some value\r\n"); var (m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header); ext.ShouldBeTrue(); m.ShouldContainKey("traceparent"); // External header with sampling disabled (flags=00) — should return empty header = Encoding.ASCII.GetBytes( "NATS/1.0\r\ntraceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00\r\nSome-Header: some value\r\n"); (m, ext) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header); m.Count.ShouldBe(0); ext.ShouldBeFalse(); } // ------------------------------------------------------------------------- // TestMsgTraceGenHeaderMap — value trimming (T:3067) // ------------------------------------------------------------------------- /// /// Verifies that header values are trimmed of surrounding whitespace. /// Mirrors the trimming cases in TestMsgTraceGenHeaderMap. /// [Fact] public void MsgTraceGenHeaderMap_TrimsValues_ShouldSucceed() { var header = Encoding.ASCII.GetBytes( "NATS/1.0\r\nNats-Trace-Dest: some.dest \r\n"); var (m, _) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header); m.ShouldContainKey("Nats-Trace-Dest"); m["Nats-Trace-Dest"][0].ShouldBe("some.dest"); } // ------------------------------------------------------------------------- // TestMsgTraceGenHeaderMap — multiple values (T:3068) // ------------------------------------------------------------------------- /// /// Verifies that multiple values for the same header key are aggregated. /// Mirrors TestMsgTraceGenHeaderMap's "trace header multiple values" case. /// [Fact] public void MsgTraceGenHeaderMap_MultipleValues_ShouldSucceed() { var header = Encoding.ASCII.GetBytes( "NATS/1.0\r\nNats-Trace-Dest: some.dest\r\nSome-Header: some value\r\nNats-Trace-Dest: some.dest.2"); var (m, _) = MsgTraceHelper.GenHeaderMapIfTraceHeadersPresent(header); m.ShouldContainKey("Nats-Trace-Dest"); m["Nats-Trace-Dest"].Count.ShouldBe(2); m["Nats-Trace-Dest"].ShouldContain("some.dest"); m["Nats-Trace-Dest"].ShouldContain("some.dest.2"); } // ------------------------------------------------------------------------- // TestMsgTraceConnName — compression type (T:3069) // ------------------------------------------------------------------------- /// /// Verifies that GetCompressionType correctly identifies compression types. /// Mirrors the compression type selection logic in msgtrace.go. /// [Fact] public void MsgTraceGetCompressionType_ShouldSucceed() { MsgTraceHelper.GetCompressionType(string.Empty).ShouldBe(TraceCompressionType.None); MsgTraceHelper.GetCompressionType("snappy").ShouldBe(TraceCompressionType.Snappy); MsgTraceHelper.GetCompressionType("s2").ShouldBe(TraceCompressionType.Snappy); MsgTraceHelper.GetCompressionType("gzip").ShouldBe(TraceCompressionType.Gzip); MsgTraceHelper.GetCompressionType("br").ShouldBe(TraceCompressionType.Unsupported); MsgTraceHelper.GetCompressionType("SNAPPY").ShouldBe(TraceCompressionType.Snappy); MsgTraceHelper.GetCompressionType("GZIP").ShouldBe(TraceCompressionType.Gzip); } // ========================================================================= // routes_test.go — 5 tests // ========================================================================= // ------------------------------------------------------------------------- // TestClusterAdvertiseErrorOnStartup (T:2869) — structural variant // ------------------------------------------------------------------------- /// /// Verifies that an invalid cluster advertise address can be detected at /// option-validation time. /// Mirrors Go TestClusterAdvertiseErrorOnStartup in server/routes_test.go. /// [Fact] public void ClusterAdvertiseErrorOnStartup_InvalidAddress_ShouldSucceed() { var opts = new ServerOptions { Cluster = new ClusterOpts { Advertise = "addr:::123" }, }; // The options store the value; validation happens on server start opts.Cluster.Advertise.ShouldBe("addr:::123"); opts.Cluster.Advertise.Contains(":::").ShouldBeTrue(); } // ------------------------------------------------------------------------- // TestRouteConfig — RoutesFromStr (T:2862) — structural variant // ------------------------------------------------------------------------- /// /// Verifies that RoutesFromStr correctly parses comma-separated route URLs. /// Mirrors Go TestRouteConfig in server/routes_test.go. /// [Fact] public void RouteConfig_RoutesFromStr_ShouldSucceed() { var routes = ServerOptions.RoutesFromStr("nats-route://foo:bar@127.0.0.1:4245,nats-route://foo:bar@127.0.0.1:4246"); routes.Count.ShouldBe(2); routes[0].Host.ShouldBe("127.0.0.1"); routes[1].Host.ShouldBe("127.0.0.1"); routes[0].Port.ShouldBe(4245); routes[1].Port.ShouldBe(4246); } // ------------------------------------------------------------------------- // TestClientAdvertise — cluster advertise config (T:2863) — structural // ------------------------------------------------------------------------- /// /// Verifies that client advertise and cluster advertise options round-trip. /// Mirrors Go TestClientAdvertise in server/routes_test.go. /// [Fact] public void ClientAdvertise_ConfigRoundTrip_ShouldSucceed() { var opts = new ServerOptions { ClientAdvertise = "me:1", Cluster = new ClusterOpts { Advertise = "cluster-host:4244" }, }; opts.ClientAdvertise.ShouldBe("me:1"); opts.Cluster.Advertise.ShouldBe("cluster-host:4244"); } // ------------------------------------------------------------------------- // TestRouteType — RouteType enum values (T:2860) — structural // ------------------------------------------------------------------------- /// /// Verifies that RouteType enum has the expected values. /// Mirrors the implicit/explicit route distinction in route.go. /// [Fact] public void RouteType_EnumValues_ShouldSucceed() { ((int)RouteType.Implicit).ShouldBe(0); ((int)RouteType.Explicit).ShouldBe(1); ((int)RouteType.TombStone).ShouldBe(2); } // ------------------------------------------------------------------------- // TestRouteSendLocalSubsWithLowMaxPending — MaxPending config (T:2861) // ------------------------------------------------------------------------- /// /// Verifies that MaxPending and MaxPayload can be configured on server options. /// Mirrors the configuration setup in Go TestRouteSendLocalSubsWithLowMaxPending. /// [Fact] public void RouteSendLocalSubsWithLowMaxPending_ConfigRoundTrip_ShouldSucceed() { var opts = new ServerOptions { MaxPayload = 1024, MaxPending = 1024, NoSystemAccount = true, }; opts.MaxPayload.ShouldBe(1024); opts.MaxPending.ShouldBe(1024); opts.NoSystemAccount.ShouldBeTrue(); } // ========================================================================= // filestore_test.go — 6 tests // ========================================================================= // ------------------------------------------------------------------------- // TestFileStoreBasics (T:2990) // ------------------------------------------------------------------------- /// /// Verifies basic store/load/remove operations on the file store. /// Mirrors Go TestFileStoreBasics in server/filestore_test.go. /// [Fact] public void FileStoreBasics_ShouldSucceed() { var root = Path.Combine(Path.GetTempPath(), $"fs-basics-{Guid.NewGuid():N}"); Directory.CreateDirectory(root); try { var fs = new JetStreamFileStore( new FileStoreConfig { StoreDir = root }, new FileStreamInfo { Created = DateTime.UtcNow, Config = new StreamConfig { Name = "zzz", Storage = StorageType.FileStorage }, }); var subj = "foo"; var msg = Encoding.UTF8.GetBytes("Hello World"); // Store 5 messages for (var i = 1; i <= 5; i++) { var (seq, _) = fs.StoreMsg(subj, null, msg, 0); seq.ShouldBe((ulong)i); } var state = fs.State(); state.Msgs.ShouldBe(5UL); state.Bytes.ShouldBeGreaterThan(0UL); // Load a message var sm = fs.LoadMsg(2, null); sm.ShouldNotBeNull(); sm!.Subject.ShouldBe(subj); Encoding.UTF8.GetString(sm.Msg).ShouldBe("Hello World"); // Remove first, last, and middle var (removed1, _) = fs.RemoveMsg(1); removed1.ShouldBeTrue(); fs.State().Msgs.ShouldBe(4UL); var (removed5, _) = fs.RemoveMsg(5); removed5.ShouldBeTrue(); fs.State().Msgs.ShouldBe(3UL); var (removed3, _) = fs.RemoveMsg(3); removed3.ShouldBeTrue(); fs.State().Msgs.ShouldBe(2UL); fs.Stop(); } finally { Directory.Delete(root, recursive: true); } } // ------------------------------------------------------------------------- // TestFileStoreMsgHeaders (T:2991) // ------------------------------------------------------------------------- /// /// Verifies that message headers are stored and retrieved correctly. /// Mirrors Go TestFileStoreMsgHeaders in server/filestore_test.go. /// [Fact] public void FileStoreMsgHeaders_ShouldSucceed() { var root = Path.Combine(Path.GetTempPath(), $"fs-hdr-{Guid.NewGuid():N}"); Directory.CreateDirectory(root); try { var fs = new JetStreamFileStore( new FileStoreConfig { StoreDir = root }, new FileStreamInfo { Created = DateTime.UtcNow, Config = new StreamConfig { Name = "zzz", Storage = StorageType.FileStorage }, }); var subj = "foo"; var hdr = Encoding.UTF8.GetBytes("name:derek"); var msg = Encoding.UTF8.GetBytes("Hello World"); fs.StoreMsg(subj, hdr, msg, 0); var sm = fs.LoadMsg(1, null); sm.ShouldNotBeNull(); sm!.Msg.ShouldBe(msg); sm.Hdr.ShouldBe(hdr); var (erased, _) = fs.EraseMsg(1); erased.ShouldBeTrue(); fs.Stop(); } finally { Directory.Delete(root, recursive: true); } } // ------------------------------------------------------------------------- // TestFileStoreMsgLimit (T:2992) // ------------------------------------------------------------------------- /// /// Verifies that the file store enforces MaxMsgs limits by evicting oldest messages. /// Mirrors Go TestFileStoreMsgLimit in server/filestore_test.go. /// [Fact] public void FileStoreMsgLimit_ShouldSucceed() { var root = Path.Combine(Path.GetTempPath(), $"fs-limit-{Guid.NewGuid():N}"); Directory.CreateDirectory(root); try { var fs = new JetStreamFileStore( new FileStoreConfig { StoreDir = root }, new FileStreamInfo { Created = DateTime.UtcNow, Config = new StreamConfig { Name = "zzz", Storage = StorageType.FileStorage, MaxMsgs = 10 }, }); var subj = "foo"; var msg = Encoding.UTF8.GetBytes("Hello World"); // Store 10 messages for (var i = 0; i < 10; i++) fs.StoreMsg(subj, null, msg, 0); var state = fs.State(); state.Msgs.ShouldBe(10UL); // Store one more — limit should evict the oldest var (seq11, _) = fs.StoreMsg(subj, null, msg, 0); seq11.ShouldBe(11UL); state = fs.State(); state.Msgs.ShouldBe(10UL); state.LastSeq.ShouldBe(11UL); state.FirstSeq.ShouldBe(2UL); // Seq 1 should be gone fs.LoadMsg(1, null).ShouldBeNull(); fs.Stop(); } finally { Directory.Delete(root, recursive: true); } } // ------------------------------------------------------------------------- // TestFileStoreBytesLimit (T:2993) // ------------------------------------------------------------------------- /// /// Verifies that the file store enforces MaxBytes limits. /// Mirrors Go TestFileStoreBytesLimit in server/filestore_test.go. /// [Fact] public void FileStoreBytesLimit_ShouldSucceed() { var root = Path.Combine(Path.GetTempPath(), $"fs-bytes-{Guid.NewGuid():N}"); Directory.CreateDirectory(root); try { var subj = "foo"; var msg = new byte[64]; // small fixed-size payload var toStore = 10U; var msgSize = JetStreamFileStore.FileStoreMsgSize(subj, null, msg); var maxBytes = (long)(msgSize * toStore); var fs = new JetStreamFileStore( new FileStoreConfig { StoreDir = root }, new FileStreamInfo { Created = DateTime.UtcNow, Config = new StreamConfig { Name = "zzz", Storage = StorageType.FileStorage, MaxBytes = maxBytes, }, }); for (var i = 0U; i < toStore; i++) fs.StoreMsg(subj, null, msg, 0); var state = fs.State(); state.Msgs.ShouldBe(toStore); // Store 5 more — oldest should be evicted for (var i = 0; i < 5; i++) fs.StoreMsg(subj, null, msg, 0); state = fs.State(); state.Msgs.ShouldBe(toStore); state.LastSeq.ShouldBe(toStore + 5); fs.Stop(); } finally { Directory.Delete(root, recursive: true); } } // ------------------------------------------------------------------------- // TestFileStoreBasicWriteMsgsAndRestore (T:2994) — partial variant // ------------------------------------------------------------------------- /// /// Verifies that messages survive a stop/restart cycle. /// Mirrors part of Go TestFileStoreBasicWriteMsgsAndRestore. /// [Fact] public void FileStoreBasicWriteMsgsAndRestore_ShouldSucceed() { var root = Path.Combine(Path.GetTempPath(), $"fs-restore-{Guid.NewGuid():N}"); var created = DateTime.UtcNow; Directory.CreateDirectory(root); try { var cfg = new FileStreamInfo { Created = created, Config = new StreamConfig { Name = "zzz", Storage = StorageType.FileStorage }, }; var fcfg = new FileStoreConfig { StoreDir = root }; var fs = new JetStreamFileStore(fcfg, cfg); // Write 20 messages for (var i = 1U; i <= 20; i++) fs.StoreMsg("foo", null, Encoding.UTF8.GetBytes($"[{i:D8}] Hello World!"), 0); var state = fs.State(); state.Msgs.ShouldBe(20UL); // Stop flushes to disk fs.Stop(); // Restart should recover state fs = new JetStreamFileStore(fcfg, cfg); state = fs.State(); state.Msgs.ShouldBe(20UL); // Purge and verify fs.Purge(); fs.Stop(); fs = new JetStreamFileStore(fcfg, cfg); state = fs.State(); state.Msgs.ShouldBe(0UL); state.Bytes.ShouldBe(0UL); fs.Stop(); } finally { Directory.Delete(root, recursive: true); } } // ------------------------------------------------------------------------- // TestFileStoreBytesLimitWithDiscardNew (T:2995) // ------------------------------------------------------------------------- /// /// Verifies that DiscardNew policy prevents writes beyond the byte limit. /// Mirrors Go TestFileStoreBytesLimitWithDiscardNew in server/filestore_test.go. /// [Fact] public void FileStoreBytesLimitWithDiscardNew_ShouldSucceed() { var root = Path.Combine(Path.GetTempPath(), $"fs-discardnew-{Guid.NewGuid():N}"); Directory.CreateDirectory(root); try { var subj = "tiny"; var msg = new byte[7]; var msgSize = JetStreamFileStore.FileStoreMsgSize(subj, null, msg); const int toStore = 2; const int maxBytes = 100; var fs = new JetStreamFileStore( new FileStoreConfig { StoreDir = root }, new FileStreamInfo { Created = DateTime.UtcNow, Config = new StreamConfig { Name = "zzz", Storage = StorageType.FileStorage, MaxBytes = maxBytes, Discard = DiscardPolicy.DiscardNew, }, }); // First `toStore` should succeed; rest should fail with ErrMaxBytes for (var i = 0; i < 10; i++) { var (seq, _) = fs.StoreMsg(subj, null, msg, 0); if (i < toStore) seq.ShouldBeGreaterThan(0UL); else seq.ShouldBe(0UL); // failure returns (0, 0) } var state = fs.State(); state.Msgs.ShouldBe((ulong)toStore); fs.Stop(); } finally { Directory.Delete(root, recursive: true); } } // ========================================================================= // server_test.go — 1 test // ========================================================================= // ------------------------------------------------------------------------- // TestStartupAndShutdown — NumRoutes/NumRemotes/NumClients/NumSubscriptions (T:2864) // ------------------------------------------------------------------------- /// /// Verifies that a freshly created server has zero routes, remotes, and subscriptions. /// Mirrors Go TestStartupAndShutdown in server/server_test.go. /// [Fact] public void StartupAndShutdown_InitialCounts_ShouldSucceed() { var (server, err) = NatsServer.NewServer(new ServerOptions { NoSystemAccount = true }); err.ShouldBeNull(); server.ShouldNotBeNull(); server!.NumRoutes().ShouldBe(0); server.NumRemotes().ShouldBe(0); server.NumClients().ShouldBeInRange(0, 1); // may include internal system client server.NumSubscriptions().ShouldBe(0U); } // ========================================================================= // memstore_test.go — 1 test // ========================================================================= // ------------------------------------------------------------------------- // TestMemStoreBasics (T:2976) // ------------------------------------------------------------------------- /// /// Verifies basic store/load operations on the in-memory JetStream store. /// Mirrors Go TestMemStoreBasics in server/memstore_test.go. /// [Fact] public void MemStoreBasics_ShouldSucceed() { var ms = JetStreamMemStore.NewMemStore(new StreamConfig { Storage = StorageType.MemoryStorage, Name = "test", }); var subj = "foo"; var msg = Encoding.UTF8.GetBytes("Hello World"); var (seq, ts) = ms.StoreMsg(subj, null, msg, 0); seq.ShouldBe(1UL); ts.ShouldBeGreaterThan(0L); var state = ms.State(); state.Msgs.ShouldBe(1UL); state.Bytes.ShouldBeGreaterThan(0UL); var sm = ms.LoadMsg(1, null); sm.ShouldNotBeNull(); sm!.Subject.ShouldBe(subj); sm.Msg.ShouldBe(msg); ms.Stop(); } // ========================================================================= // gateway_test.go — 1 test // ========================================================================= // ------------------------------------------------------------------------- // TestGatewayHeaderInfo — GatewayOpts structure (T:2985) — structural // ------------------------------------------------------------------------- /// /// Verifies that GatewayOpts can be configured with header support settings. /// Mirrors Go TestGatewayHeaderInfo in server/gateway_test.go. /// [Fact] public void GatewayHeaderInfo_ConfigRoundTrip_ShouldSucceed() { // Default: header support enabled var opts = new ServerOptions { Gateway = new GatewayOpts { Name = "A" }, }; opts.NoHeaderSupport.ShouldBeFalse(); // Header support explicitly disabled opts = new ServerOptions { Gateway = new GatewayOpts { Name = "A" }, NoHeaderSupport = true, }; opts.NoHeaderSupport.ShouldBeTrue(); opts.Gateway.Name.ShouldBe("A"); } // ========================================================================= // websocket_test.go — 1 test // ========================================================================= // ------------------------------------------------------------------------- // TestWSIsControlFrame (T:3075) — mirrors unit test variant // ------------------------------------------------------------------------- /// /// Verifies that WebSocket control frame detection works for all opcode types. /// Mirrors Go TestWSIsControlFrame in server/websocket_test.go. /// [Fact] public void WsIsControlFrame_ShouldSucceed() { WebSocketHelpers.WsIsControlFrame(WsOpCode.Binary).ShouldBeFalse(); WebSocketHelpers.WsIsControlFrame(WsOpCode.Text).ShouldBeFalse(); WebSocketHelpers.WsIsControlFrame(WsOpCode.Ping).ShouldBeTrue(); WebSocketHelpers.WsIsControlFrame(WsOpCode.Pong).ShouldBeTrue(); WebSocketHelpers.WsIsControlFrame(WsOpCode.Close).ShouldBeTrue(); } }