Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.IntegrationTests/MiscTests.cs

788 lines
30 KiB
C#

// 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;
/// <summary>
/// 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.
/// </summary>
[Trait("Category", "Integration")]
public sealed class MiscTests
{
// =========================================================================
// msgtrace_test.go — 7 tests
// =========================================================================
// -------------------------------------------------------------------------
// TestMsgTraceConnName (T:3063) — structural variant
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that <c>GetConnName</c> 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.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that <c>GenHeaderMapIfTraceHeadersPresent</c> returns an empty map
/// when no trace headers are present.
/// Mirrors the negative cases in TestMsgTraceGenHeaderMap.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that <c>GenHeaderMapIfTraceHeadersPresent</c> correctly parses
/// headers when the Nats-Trace-Dest header is present.
/// Mirrors the positive cases in TestMsgTraceGenHeaderMap.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that an enabled traceparent header triggers external tracing.
/// Mirrors the external header cases in TestMsgTraceGenHeaderMap.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that header values are trimmed of surrounding whitespace.
/// Mirrors the trimming cases in TestMsgTraceGenHeaderMap.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that multiple values for the same header key are aggregated.
/// Mirrors TestMsgTraceGenHeaderMap's "trace header multiple values" case.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that <c>GetCompressionType</c> correctly identifies compression types.
/// Mirrors the compression type selection logic in msgtrace.go.
/// </summary>
[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
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that an invalid cluster advertise address can be detected at
/// option-validation time.
/// Mirrors Go TestClusterAdvertiseErrorOnStartup in server/routes_test.go.
/// </summary>
[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
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that <c>RoutesFromStr</c> correctly parses comma-separated route URLs.
/// Mirrors Go TestRouteConfig in server/routes_test.go.
/// </summary>
[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
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that client advertise and cluster advertise options round-trip.
/// Mirrors Go TestClientAdvertise in server/routes_test.go.
/// </summary>
[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
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that RouteType enum has the expected values.
/// Mirrors the implicit/explicit route distinction in route.go.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that MaxPending and MaxPayload can be configured on server options.
/// Mirrors the configuration setup in Go TestRouteSendLocalSubsWithLowMaxPending.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies basic store/load/remove operations on the file store.
/// Mirrors Go TestFileStoreBasics in server/filestore_test.go.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that message headers are stored and retrieved correctly.
/// Mirrors Go TestFileStoreMsgHeaders in server/filestore_test.go.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that the file store enforces MaxMsgs limits by evicting oldest messages.
/// Mirrors Go TestFileStoreMsgLimit in server/filestore_test.go.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that the file store enforces MaxBytes limits.
/// Mirrors Go TestFileStoreBytesLimit in server/filestore_test.go.
/// </summary>
[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
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that messages survive a stop/restart cycle.
/// Mirrors part of Go TestFileStoreBasicWriteMsgsAndRestore.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that DiscardNew policy prevents writes beyond the byte limit.
/// Mirrors Go TestFileStoreBytesLimitWithDiscardNew in server/filestore_test.go.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that a freshly created server has zero routes, remotes, and subscriptions.
/// Mirrors Go TestStartupAndShutdown in server/server_test.go.
/// </summary>
[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)
// -------------------------------------------------------------------------
/// <summary>
/// Verifies basic store/load operations on the in-memory JetStream store.
/// Mirrors Go TestMemStoreBasics in server/memstore_test.go.
/// </summary>
[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
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that GatewayOpts can be configured with header support settings.
/// Mirrors Go TestGatewayHeaderInfo in server/gateway_test.go.
/// </summary>
[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
// -------------------------------------------------------------------------
/// <summary>
/// Verifies that WebSocket control frame detection works for all opcode types.
/// Mirrors Go TestWSIsControlFrame in server/websocket_test.go.
/// </summary>
[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();
}
}