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)!; } }