// Copyright 2012-2026 The NATS Authors // Licensed under the Apache License, Version 2.0 using Shouldly; using ZB.MOM.NatsNet.Server; namespace ZB.MOM.NatsNet.Server.Tests.JetStream; public sealed partial class NatsConsumerTests { [Fact] // T:1304 public void JetStreamConsumerAndStreamNamesWithPathSeparators_ShouldSucceed() { var streamErr = JsApiErrors.NewJSStreamNameContainsPathSeparatorsError(); streamErr.Code.ShouldBe(JsApiErrors.StreamNameContainsPathSeparators.Code); streamErr.ErrCode.ShouldBe(JsApiErrors.StreamNameContainsPathSeparators.ErrCode); streamErr.Description.ShouldBe("Stream name can not contain path separators"); var consumerErr = JsApiErrors.NewJSConsumerNameContainsPathSeparatorsError(); consumerErr.Code.ShouldBe(JsApiErrors.ConsumerNameContainsPathSeparators.Code); consumerErr.ErrCode.ShouldBe(JsApiErrors.ConsumerNameContainsPathSeparators.ErrCode); consumerErr.Description.ShouldBe("Consumer name can not contain path separators"); } [Fact] public void Create_SetLeader_UpdateConfig_AndStop_ShouldBehave() { var account = new Account { Name = "A" }; var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Storage = StorageType.FileStorage }; var stream = NatsStream.Create(account, streamCfg, null, null, null, null); stream.ShouldNotBeNull(); var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit }; var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.CreateOrUpdate, null); consumer.ShouldNotBeNull(); consumer!.IsLeader().ShouldBeFalse(); consumer.SetLeader(true, 3); consumer.IsLeader().ShouldBeTrue(); var updated = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckAll }; consumer.UpdateConfig(updated); consumer.GetConfig().AckPolicy.ShouldBe(AckPolicy.AckAll); var info = consumer.GetInfo(); info.Stream.ShouldBe("S"); info.Name.ShouldBe("D"); consumer.Stop(); consumer.IsLeader().ShouldBeFalse(); } [Fact] // T:1364 public void SortingConsumerPullRequests_ShouldSucceed() { var q = new WaitQueue(max: 100); q.AddPrioritized(new WaitingRequest { Reply = "1a", PriorityGroup = new PriorityGroup { Priority = 1 }, N = 1 }) .ShouldBeTrue(); q.AddPrioritized(new WaitingRequest { Reply = "2a", PriorityGroup = new PriorityGroup { Priority = 2 }, N = 1 }) .ShouldBeTrue(); q.AddPrioritized(new WaitingRequest { Reply = "1b", PriorityGroup = new PriorityGroup { Priority = 1 }, N = 1 }) .ShouldBeTrue(); q.AddPrioritized(new WaitingRequest { Reply = "2b", PriorityGroup = new PriorityGroup { Priority = 2 }, N = 1 }) .ShouldBeTrue(); q.AddPrioritized(new WaitingRequest { Reply = "1c", PriorityGroup = new PriorityGroup { Priority = 1 }, N = 1 }) .ShouldBeTrue(); q.AddPrioritized(new WaitingRequest { Reply = "3a", PriorityGroup = new PriorityGroup { Priority = 3 }, N = 1 }) .ShouldBeTrue(); q.AddPrioritized(new WaitingRequest { Reply = "2c", PriorityGroup = new PriorityGroup { Priority = 2 }, N = 1 }) .ShouldBeTrue(); var expectedOrder = new[] { ("1a", 1), ("1b", 1), ("1c", 1), ("2a", 2), ("2b", 2), ("2c", 2), ("3a", 3), }; q.Len.ShouldBe(expectedOrder.Length); foreach (var (reply, priority) in expectedOrder) { var current = q.Peek(); current.ShouldNotBeNull(); current!.Reply.ShouldBe(reply); current.PriorityGroup.ShouldNotBeNull(); current.PriorityGroup!.Priority.ShouldBe(priority); q.RemoveCurrent(); } q.IsEmpty().ShouldBeTrue(); } [Fact] // T:1365 public void WaitQueuePopAndRequeue_ShouldSucceed() { var q = new WaitQueue(max: 100); q.AddPrioritized(new WaitingRequest { Reply = "1a", N = 2, PriorityGroup = new PriorityGroup { Priority = 1 } }) .ShouldBeTrue(); q.AddPrioritized(new WaitingRequest { Reply = "1b", N = 1, PriorityGroup = new PriorityGroup { Priority = 1 } }) .ShouldBeTrue(); q.AddPrioritized(new WaitingRequest { Reply = "2a", N = 3, PriorityGroup = new PriorityGroup { Priority = 2 } }) .ShouldBeTrue(); var wr = q.PopAndRequeue(); wr.ShouldNotBeNull(); wr!.Reply.ShouldBe("1a"); wr.N.ShouldBe(1); q.Len.ShouldBe(3); wr = q.PopAndRequeue(); wr.ShouldNotBeNull(); wr!.Reply.ShouldBe("1b"); wr.N.ShouldBe(0); q.Len.ShouldBe(2); wr = q.PopAndRequeue(); wr.ShouldNotBeNull(); wr!.Reply.ShouldBe("1a"); wr.N.ShouldBe(0); q.Len.ShouldBe(1); q.Peek()!.Reply.ShouldBe("2a"); q.Peek()!.N.ShouldBe(3); } [Fact] public void AddConsumerWithAction_CreateThenUpdate_ShouldRespectActions() { var account = new Account { Name = "A" }; var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy }; var stream = NatsStream.Create(account, streamCfg, null, null, null, null); stream.ShouldNotBeNull(); var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit }; var (created, createErr) = stream!.AddConsumerWithAction(cfg, "D", ConsumerAction.Create, pedantic: false); createErr.ShouldBeNull(); created.ShouldNotBeNull(); var updateCfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckAll }; var (updated, updateErr) = stream.AddConsumerWithAction(updateCfg, "D", ConsumerAction.Update, pedantic: false); updateErr.ShouldBeNull(); updated.ShouldNotBeNull(); updated!.GetConfig().AckPolicy.ShouldBe(AckPolicy.AckAll); } [Fact] public void AddConsumer_WithAssignment_ShouldAttachAssignment() { var account = new Account { Name = "A" }; var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy }; var stream = NatsStream.Create(account, streamCfg, null, null, null, null); stream.ShouldNotBeNull(); var assignment = new ConsumerAssignment { Name = "D", Stream = "S", Group = new RaftGroup { Name = "RG", Peers = ["N1"] }, }; var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit }; var (consumer, err) = stream!.AddConsumerWithAssignment(cfg, "D", assignment, isRecovering: false, ConsumerAction.Create, pedantic: false); err.ShouldBeNull(); consumer.ShouldNotBeNull(); consumer!.ConsumerAssignment().ShouldBeSameAs(assignment); } [Fact] public void UpdateInactiveThreshold_AndPauseState_ShouldTrackConfigValues() { var account = new Account { Name = "A" }; var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy }; var stream = NatsStream.Create(account, streamCfg, null, null, null, null); stream.ShouldNotBeNull(); var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit }; var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); consumer!.UpdateInactiveThreshold(new ConsumerConfig { InactiveThreshold = TimeSpan.FromSeconds(30) }); consumer.GetConfig().InactiveThreshold.ShouldBe(TimeSpan.FromSeconds(30)); var pauseUntil = DateTime.UtcNow.AddMinutes(1); consumer.UpdatePauseState(new ConsumerConfig { PauseUntil = pauseUntil }); consumer.GetConfig().PauseUntil.ShouldBe(pauseUntil); } [Fact] public void ConsumerAssignment_GetSet_ShouldRoundTrip() { var account = new Account { Name = "A" }; var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"], Retention = RetentionPolicy.LimitsPolicy }; var stream = NatsStream.Create(account, streamCfg, null, null, null, null); stream.ShouldNotBeNull(); var cfg = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit }; var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); var assignment = new ConsumerAssignment { Name = "D", Stream = "S", Group = new RaftGroup { Name = "RG", Peers = ["N1"] }, }; consumer!.SetConsumerAssignment(assignment); consumer.ConsumerAssignment().ShouldBeSameAs(assignment); } [Fact] public async Task MonitorQuitC_AndSignalMonitorQuit_ShouldPublishQuitSignal() { var account = new Account { Name = "A" }; var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] }; var stream = NatsStream.Create(account, streamCfg, null, null, null, null); stream.ShouldNotBeNull(); var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); var monitor = consumer!.MonitorQuitC(); monitor.ShouldNotBeNull(); monitor!.TryRead(out _).ShouldBeFalse(); consumer.SignalMonitorQuit(); using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2)); var signal = await monitor.ReadAsync(cts.Token); signal.ShouldBeTrue(); } [Fact] public void SubscribeInternal_Unsubscribe_AndHasDeliveryInterest_ShouldTrackState() { var account = new Account { Name = "A" }; var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] }; var stream = NatsStream.Create(account, streamCfg, null, null, null, null); stream.ShouldNotBeNull(); var consumer = NatsConsumer.Create( stream!, new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.foo" }, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); consumer!.HasDeliveryInterest().ShouldBeFalse(); consumer.SubscribeInternal("deliver.foo").ShouldBeTrue(); consumer.CheckQueueInterest("deliver.foo").ShouldBeTrue(); consumer.HasDeliveryInterest().ShouldBeTrue(); consumer.Unsubscribe("deliver.foo").ShouldBeTrue(); consumer.HasDeliveryInterest().ShouldBeFalse(); } [Fact] public void AdvisoryHelpers_AndCreatedTime_ShouldBehave() { var account = new Account { Name = "A" }; var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] }; var stream = NatsStream.Create(account, streamCfg, null, null, null, null); stream.ShouldNotBeNull(); var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); consumer!.SendCreateAdvisory().ShouldBeTrue(); consumer.SendDeleteAdvisoryLocked().ShouldBeTrue(); consumer.SendPinnedAdvisoryLocked("pin-1").ShouldBeTrue(); consumer.SendUnpinnedAdvisoryLocked("pin-1").ShouldBeTrue(); consumer.SendPauseAdvisoryLocked(DateTime.UtcNow.AddMinutes(1)).ShouldBeTrue(); var created = DateTime.UtcNow.AddHours(-1); consumer.SetCreatedTime(created); consumer.CreatedTime().ShouldBe(created); } [Fact] public void HandleClusterConsumerInfoRequest_WhenLeader_ReturnsInfo() { var account = new Account { Name = "A" }; var streamCfg = new StreamConfig { Name = "S", Subjects = ["foo"] }; var stream = NatsStream.Create(account, streamCfg, null, null, null, null); stream.ShouldNotBeNull(); var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D" }, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); consumer!.HandleClusterConsumerInfoRequest().ShouldBeNull(); consumer.SetLeader(true, 1); consumer.IsLeaderInternal().ShouldBeTrue(); consumer.HandleClusterConsumerInfoRequest().ShouldNotBeNull(); consumer.ClearNode(); } [Fact] public void UpdateDeliveryInterest_AndDeleteNotActive_ShouldReflectInterestState() { var account = new Account { Name = "A" }; var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null); stream.ShouldNotBeNull(); var consumer = NatsConsumer.Create( stream!, new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.foo", InactiveThreshold = TimeSpan.FromMilliseconds(20) }, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); consumer!.UpdateInactiveThreshold(new ConsumerConfig { InactiveThreshold = TimeSpan.FromMilliseconds(20) }); consumer.UpdateDeliveryInterest(localInterest: false).ShouldBeTrue(); Thread.Sleep(40); consumer.IsClosed().ShouldBeTrue(); } [Fact] public void WatchGWinterest_AndRateLimit_ShouldExecuteWithoutErrors() { var account = new Account { Name = "A" }; var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"], MaxMsgSize = 4096 }, null, null, null, null); stream.ShouldNotBeNull(); var consumer = NatsConsumer.Create( stream!, new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.foo", RateLimit = 8_000 }, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); consumer!.SetRateLimitNeedsLocks(); consumer.WatchGWinterest(); consumer.SubscribeInternal("deliver.foo").ShouldBeTrue(); consumer.UpdateDeliveryInterest(localInterest: true).ShouldBeFalse(); consumer.HasDeliveryInterest().ShouldBeTrue(); } [Fact] public void AccountCheckNewConsumerConfig_InvalidPolicyChanges_ShouldFail() { var account = new Account { Name = "A" }; var current = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit, DeliverPolicy = DeliverPolicy.DeliverAll }; var next = new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckAll, DeliverPolicy = DeliverPolicy.DeliverAll }; var err = account.CheckNewConsumerConfig(current, next); err.ShouldNotBeNull(); err.Message.ShouldContain("ack policy"); } [Fact] public void UpdateDeliverSubject_AndConfigsEqualSansDelivery_ShouldBehave() { var account = new Account { Name = "A" }; var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null); stream.ShouldNotBeNull(); var cfg = new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.a", AckPolicy = AckPolicy.AckExplicit }; var consumer = NatsConsumer.Create(stream!, cfg, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); consumer!.SubscribeInternal("deliver.a"); consumer.UpdateDeliverSubject("deliver.b").ShouldBeTrue(); consumer.SubscribeInternal("deliver.b"); consumer.HasDeliveryInterest().ShouldBeTrue(); var left = new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.a", AckPolicy = AckPolicy.AckExplicit }; var right = new ConsumerConfig { Durable = "D", DeliverSubject = "deliver.b", AckPolicy = AckPolicy.AckExplicit }; NatsConsumer.ConfigsEqualSansDelivery(left, right).ShouldBeTrue(); } [Fact] public void AckFlow_NewMessage_Push_Process_Progress_UpdateSkipped_ShouldBehave() { var account = new Account { Name = "A" }; var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null); stream.ShouldNotBeNull(); var consumer = NatsConsumer.Create(stream!, new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit }, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); var ack = NatsConsumer.NewJSAckMsg("a.b.10.20.1", "reply", 0, "+ACK"u8.ToArray()); ack.Subject.ShouldBe("a.b.10.20.1"); consumer!.PushAck("a.b.10.20.1", "reply", 0, "+ACK"u8.ToArray()); consumer.ProcessAck("a.b.10.20.1", "reply", 0, "+ACK"u8.ToArray()); consumer.ProgressUpdate(10); consumer.UpdateSkipped(25); var state = consumer.GetConsumerState(); state.AckFloor.Stream.ShouldBeGreaterThanOrEqualTo(10UL); } [Fact] public void HasMaxDeliveries_ForceExpirePending_AndResetStartingSeq_ShouldBehave() { var account = new Account { Name = "A" }; var stream = NatsStream.Create(account, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null); stream.ShouldNotBeNull(); var consumer = NatsConsumer.Create( stream!, new ConsumerConfig { Durable = "D", AckPolicy = AckPolicy.AckExplicit, MaxDeliver = 2, DeliverPolicy = DeliverPolicy.DeliverAll }, ConsumerAction.Create, null); consumer.ShouldNotBeNull(); consumer!.ProcessAck("a.b.5.7.1", "reply", 0, "-NAK"u8.ToArray()); consumer.HasMaxDeliveries(5).ShouldBeFalse(); consumer.HasMaxDeliveries(5).ShouldBeTrue(); consumer.ForceExpirePending(); var (seq, canRespond, err) = consumer.ResetStartingSeq(10, "reply"); err.ShouldBeNull(); seq.ShouldBe(10UL); canRespond.ShouldBeTrue(); } [Fact] public void JsPubMsg_ReturnToPool_ShouldResetState() { var msg = new JsPubMsg { Subject = "foo", Reply = "bar", Hdr = [1, 2], Msg = [3, 4], Pa = new object(), Sync = new object(), }; msg.ReturnToPool(); msg.Subject.ShouldBe(string.Empty); msg.Reply.ShouldBeNull(); msg.Hdr.ShouldBeNull(); msg.Msg.ShouldBeNull(); msg.Pa.ShouldBeNull(); msg.Sync.ShouldBeNull(); } }