Files
natsnet/dotnet/tests/ZB.MOM.NatsNet.Server.Tests/JetStream/StreamLifecycleGroupBTests.cs
2026-02-28 23:20:30 -05:00

256 lines
8.3 KiB
C#

using System.Text;
using Shouldly;
using ZB.MOM.NatsNet.Server;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server.Tests.JetStream;
public sealed class StreamLifecycleGroupBTests
{
[Fact]
public void MaxMsgSize_UsesConfiguredLimit()
{
var stream = CreateStream(new StreamConfig
{
Name = "ORDERS",
Subjects = ["orders.*"],
Storage = StorageType.MemoryStorage,
MaxMsgSize = 1024,
});
stream.MaxMsgSize().ShouldBeGreaterThan(1024UL);
}
[Fact]
public void AutoTuneFileStorageBlockSize_WithMaxMsgsPer_UsesKvDefault()
{
var stream = CreateStream(new StreamConfig
{
Name = "KV",
Subjects = ["kv.>"],
Storage = StorageType.MemoryStorage,
MaxMsgsPer = 10,
});
var cfg = new FileStoreConfig();
stream.AutoTuneFileStorageBlockSize(cfg);
cfg.BlockSize.ShouldBe(FileStoreDefaults.DefaultKvBlockSize);
}
[Fact]
public void ClusterSequenceHelpers_RoundTripValues()
{
var stream = CreateStream();
stream.SetCLFS(4);
stream.SetLastSeq(10);
stream.GetCLFS().ShouldBe(4UL);
stream.LastSeqValue().ShouldBe(10UL);
stream.LastSeqAndCLFS().ShouldBe((10UL, 4UL));
}
[Fact]
public void CreatedTime_SetCreatedTime_UpdatesValue()
{
var stream = CreateStream();
var timestamp = DateTime.UtcNow.AddMinutes(-5);
stream.SetCreatedTime(timestamp);
stream.CreatedTime().ShouldBe(timestamp);
}
[Fact]
public void Update_AndUpdatePedantic_ApplyConfig()
{
var stream = CreateStream();
var updated = new StreamConfig { Name = "ORDERS", Subjects = ["orders.v2"], Storage = StorageType.MemoryStorage };
stream.Update(updated).ShouldBeNull();
stream.GetConfig().Subjects.ShouldBe(["orders.v2"]);
var updatedAgain = new StreamConfig { Name = "ORDERS", Subjects = ["orders.v3"], Storage = StorageType.MemoryStorage };
stream.UpdatePedantic(updatedAgain, pedantic: true).ShouldBeNull();
stream.GetConfig().Subjects.ShouldBe(["orders.v3"]);
}
[Fact]
public void CheckStreamCfg_NormalizesSubjects_AndRejectsNegativeMaxMsgSize()
{
var (server, error) = NatsServer.NewServer(new ServerOptions());
error.ShouldBeNull();
server.ShouldNotBeNull();
var account = new Account { Name = "A" };
var (normalized, okError) = server.CheckStreamCfg(new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage }, account, pedantic: false);
okError.ShouldBeNull();
normalized.Subjects.ShouldBe(["ORDERS.>"]);
var (_, badError) = server.CheckStreamCfg(new StreamConfig
{
Name = "ORDERS",
Storage = StorageType.MemoryStorage,
MaxMsgSize = -1,
}, account, pedantic: false);
badError.ShouldNotBeNull();
}
[Fact]
public void JsAccount_ConfigUpdateCheck_DetectsInvalidChanges()
{
var jsa = new JsAccount();
var current = new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage };
jsa.ConfigUpdateCheck(current, new StreamConfig { Name = "DIFFERENT", Storage = StorageType.MemoryStorage }).ShouldNotBeNull();
jsa.ConfigUpdateCheck(current, new StreamConfig { Name = "ORDERS", Storage = StorageType.FileStorage }).ShouldNotBeNull();
jsa.ConfigUpdateCheck(current, new StreamConfig { Name = "ORDERS", Storage = StorageType.MemoryStorage }).ShouldBeNull();
}
[Fact]
public void JsAccount_SubjectsOverlap_IgnoresOwnAssignment()
{
var jsa = new JsAccount();
var assignment = new StreamAssignment();
var stream = CreateStream(new StreamConfig
{
Name = "ORDERS",
Subjects = ["orders.*"],
Storage = StorageType.MemoryStorage,
});
stream.SetStreamAssignment(assignment);
jsa.Streams["ORDERS"] = stream;
jsa.SubjectsOverlap(["orders.created"], assignment).ShouldBeFalse();
jsa.SubjectsOverlap(["orders.created"], ownAssignment: null).ShouldBeTrue();
}
[Fact]
public void GroupCHelpers_GetCfgNameAndIsMirror_ReturnExpectedValues()
{
var mirrorCfg = new StreamConfig
{
Name = "MIRROR",
Storage = StorageType.MemoryStorage,
Mirror = new StreamSource { Name = "ORIGIN" },
};
var stream = CreateStream(mirrorCfg);
stream.GetCfgName().ShouldBe("MIRROR");
stream.IsMirror().ShouldBeTrue();
stream.MirrorInfo().ShouldBeNull();
}
[Fact]
public void GroupCHelpers_SourceInfoAndBackoff_Behave()
{
var stream = CreateStream();
var info = new StreamSourceInfo
{
Name = "SRC",
FilterSubject = "orders.*",
Lag = 10,
Error = "x",
};
var cloned = stream.SourceInfo(info);
cloned.ShouldNotBeNull();
cloned!.Name.ShouldBe("SRC");
cloned.FilterSubject.ShouldBe("orders.*");
NatsStream.CalculateRetryBackoff(1).ShouldBeGreaterThan(TimeSpan.Zero);
NatsStream.CalculateRetryBackoff(1000).ShouldBe(TimeSpan.FromMinutes(2));
}
[Fact]
public void GroupD_SourceConsumersAndAckParsing_Behave()
{
var stream = CreateStream(new StreamConfig
{
Name = "ORDERS",
Subjects = ["orders.*"],
Storage = StorageType.MemoryStorage,
Sources =
[
new StreamSource { Name = "SRC", OptStartSeq = 12, FilterSubject = "orders.*" },
],
});
stream.SetupSourceConsumers();
var source = stream.StreamSource("SRC orders.* >");
source.ShouldNotBeNull();
source!.Name.ShouldBe("SRC");
stream.StartingSequenceForSources(source.IndexName).ShouldBe(12UL);
stream.SetupSourceConsumer(source.IndexName, 20, DateTime.UtcNow);
stream.ProcessInboundSourceMsg(source.IndexName, new InMsg { Subject = "orders.created", Msg = "x"u8.ToArray() }).ShouldBeTrue();
stream.ResetSourceInfo(source.IndexName);
stream.RetrySourceConsumerAtSeq(source.IndexName, 30);
stream.StartingSequenceForSources(source.IndexName).ShouldBe(30UL);
NatsStream.StreamAndSeqFromAckReply("ORDERS.99").ShouldBe(("ORDERS", 99UL));
NatsStream.StreamAndSeq("A.B.C").ShouldBe(("A", 0UL));
}
[Fact]
public void GroupD_MirrorAndControlPaths_Behave()
{
var stream = CreateStream();
stream.SetupMirrorConsumer();
stream.ProcessInboundMirrorMsg(new InMsg
{
Subject = "$JS.FC.orders",
Reply = "reply",
Hdr = Encoding.ASCII.GetBytes("NATS/1.0\r\n\r\n"),
}).ShouldBeTrue();
stream.ProcessMirrorMsgs(new StreamSourceInfo { Name = "M", Lag = 2 }, [new InMsg { Subject = "orders.created", Msg = [1] }]);
stream.RetryDisconnectedSyncConsumers();
}
[Fact]
public void GroupE_SubscriptionsAndInflightCleanup_Behave()
{
var stream = CreateStream();
stream.SubscribeToDirect();
stream.SubscribeToMirrorDirect();
var (sub, subErr) = stream.SubscribeInternal("orders.created", handler: null);
subErr.ShouldBeNull();
sub.ShouldNotBeNull();
var (qsub, qErr) = stream.QueueSubscribeInternal("orders.updated", "Q", handler: null);
qErr.ShouldBeNull();
qsub.ShouldNotBeNull();
qsub!.Queue.ShouldNotBeNull();
stream.UnsubscribeInternal("orders.created").ShouldBeNull();
stream.RemoveInternalConsumer("orders.updated");
stream.SubscribeToStream();
stream.UnsubscribeToStream();
stream.DeleteInflightBatches(preserveState: false);
stream.DeleteBatchApplyState();
stream.StopSourceConsumers();
stream.UnsubscribeToDirect();
stream.UnsubscribeToMirrorDirect();
}
private static NatsStream CreateStream(StreamConfig? cfg = null)
{
cfg ??= new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], Storage = StorageType.MemoryStorage };
return NatsStream.Create(
new Account { Name = "A" },
cfg,
null,
new JetStreamMemStore(cfg.Clone()),
null,
null)!;
}
}