788 lines
30 KiB
C#
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();
|
|
}
|
|
}
|