using System.Text; using Shouldly; using ZB.MOM.NatsNet.Server; namespace ZB.MOM.NatsNet.Server.Tests.JetStream; public sealed class NatsConsumerDispatchTests { [Fact] public void ProcessWaiting_EndOfStream_ShouldExpireNoWaitRequests() { var consumer = CreatePullConsumer(maxWaiting: 8); consumer.ProcessNextMsgRequest("_INBOX.a", Encoding.UTF8.GetBytes("{\"batch\":1,\"no_wait\":true}")).ShouldBeTrue(); var result = consumer.ProcessWaiting(endOfStream: true); result.Expired.ShouldBe(1); result.Waiting.ShouldBe(0); consumer.CheckWaitingForInterest().ShouldBeFalse(); } [Fact] public void HbTimer_HeartbeatConfigured_ShouldReturnTimer() { var consumer = CreatePullConsumer(maxWaiting: 8, heartbeat: TimeSpan.FromMilliseconds(50)); var (duration, timer) = consumer.HbTimer(); duration.ShouldBe(TimeSpan.FromMilliseconds(50)); timer.ShouldNotBeNull(); timer!.Dispose(); } [Fact] public void CheckAckFloor_WithPendingEntries_ShouldAdvanceFloor() { var consumer = CreatePullConsumer(maxWaiting: 8); consumer.ApplyState(new ConsumerState { Delivered = new SequencePair { Consumer = 20, Stream = 20 }, AckFloor = new SequencePair { Consumer = 0, Stream = 0 }, Pending = new Dictionary { [10] = new Pending { Sequence = 3, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }, }, }); consumer.CheckAckFloor(); var state = consumer.ReadStoredState(); state.AckFloor.Stream.ShouldBe(9UL); state.AckFloor.Consumer.ShouldBe(2UL); } [Fact] public void ProcessInboundAcks_QueuedAck_ShouldAdvanceAckFloor() { var consumer = CreatePullConsumer(maxWaiting: 8); consumer.PushAck("$JS.ACK.2.8.1", "reply", 0, Encoding.ASCII.GetBytes("+ACK")); var processed = consumer.ProcessInboundAcks(CancellationToken.None); var state = consumer.GetConsumerState(); processed.ShouldBe(1); state.AckFloor.Stream.ShouldBe(8UL); } [Fact] public void ProcessInboundNextMsgReqs_QueuedRequest_ShouldPopulateWaitingQueue() { var consumer = CreatePullConsumer(maxWaiting: 8); consumer.ProcessNextMsgReq("_INBOX.next", Encoding.UTF8.GetBytes("{\"batch\":2}")); var processed = consumer.ProcessInboundNextMsgReqs(CancellationToken.None); var pending = consumer.PendingRequests(); processed.ShouldBe(1); pending.ShouldContainKey("_INBOX.next"); pending["_INBOX.next"].N.ShouldBe(2); } [Fact] public void PendingCounters_AndAckReply_ShouldTrackValues() { var consumer = CreatePullConsumer(maxWaiting: 8); consumer.ApplyState(new ConsumerState { Delivered = new SequencePair { Consumer = 10, Stream = 30 }, AckFloor = new SequencePair { Consumer = 7, Stream = 22 }, }); var (pending, error) = consumer.CheckNumPending(); error.ShouldBeNull(); pending.ShouldBe(8UL); consumer.NumPending().ShouldBe(8UL); consumer.CheckNumPendingOnEOF(); consumer.SetMaxPendingBytes(256); var ackReply = consumer.AckReply(30, 11, 1, 12345, pending); ackReply.ShouldContain("$JS.ACK.1.30.11.12345.8"); } [Fact] public void SendIdleHeartbeat_ShouldReturnFormattedHeartbeat() { var consumer = CreatePullConsumer(maxWaiting: 8); consumer.ApplyState(new ConsumerState { Delivered = new SequencePair { Consumer = 5, Stream = 9 }, AckFloor = new SequencePair { Consumer = 4, Stream = 8 }, }); var heartbeat = consumer.SendIdleHeartbeat("$JS.HEARTBEAT"); heartbeat.ShouldContain("100 Idle Heartbeat"); heartbeat.ShouldContain("Nats-Last-Consumer: 5"); heartbeat.ShouldContain("Nats-Last-Stream: 9"); } [Fact] public void LoopAndGatherMsgs_WithPendingEntries_ShouldDeliverMessages() { var consumer = CreatePullConsumer(maxWaiting: 8); consumer.ApplyState(new ConsumerState { Delivered = new SequencePair { Consumer = 0, Stream = 0 }, AckFloor = new SequencePair { Consumer = 0, Stream = 0 }, Pending = new Dictionary { [1] = new Pending { Sequence = 1, Timestamp = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds() }, }, }); consumer.ProcessNextMsgRequest("_INBOX.loop", Encoding.UTF8.GetBytes("{\"batch\":1}")).ShouldBeTrue(); var delivered = consumer.LoopAndGatherMsgs(4, CancellationToken.None); delivered.ShouldBeGreaterThan(0); consumer.GetConsumerState().Delivered.Stream.ShouldBeGreaterThanOrEqualTo(1UL); } private static NatsConsumer CreatePullConsumer(int maxWaiting, TimeSpan? heartbeat = null) { var stream = NatsStream.Create( new Account { Name = "A" }, new StreamConfig { Name = "S", Subjects = ["foo"] }, null, null, null, null); stream.ShouldNotBeNull(); var config = new ConsumerConfig { Durable = "D", MaxWaiting = maxWaiting, Heartbeat = heartbeat ?? TimeSpan.Zero, }; var consumer = NatsConsumer.Create(stream!, config, ConsumerAction.CreateOrUpdate, null); consumer.ShouldNotBeNull(); return consumer!; } }