// Ported from golang/nats-server/server/jetstream_consumer_test.go // Covers: consumer creation, deliver policies (All, Last, New, ByStartSequence, ByStartTime), // and ack policies (None, Explicit, All) as modelled in the .NET port. // // Go reference tests: // TestJetStreamConsumerCreate (~line 2967) // TestJetStreamConsumerWithStartTime (~line 3160) // TestJetStreamConsumerMaxDeliveries (~line 3265) // TestJetStreamConsumerAckFloorFill (~line 3404) // TestJetStreamConsumerReplayRateNoAck (~line 4505) using System.Text; using NATS.Server.JetStream; using NATS.Server.JetStream.Consumers; using NATS.Server.JetStream.Models; using NATS.Server.JetStream.Storage; namespace NATS.Server.JetStream.Tests.JetStream; /// /// Consumer delivery parity tests ported from the Go reference implementation. /// These tests exercise push/pull delivery, deliver policies, and ack policies against /// the in-process ConsumerManager + StreamManager, mirroring the semantics validated in /// golang/nats-server/server/jetstream_consumer_test.go. /// public class ConsumerDeliveryParityTests { // ------------------------------------------------------------------------- // Test 1 – Pull consumer with DeliverPolicy.All returns all published msgs // // Go reference: TestJetStreamConsumerCreate – verifies that a durable pull // consumer created with default settings fetches all stored messages in // sequence order. // ------------------------------------------------------------------------- [Fact] public async Task Pull_consumer_deliver_all_returns_messages_in_sequence_order() { var streams = new StreamManager(); streams.CreateOrUpdate(new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], }).Error.ShouldBeNull(); var consumers = new ConsumerManager(); consumers.CreateOrUpdate("ORDERS", new ConsumerConfig { DurableName = "PULL", DeliverPolicy = DeliverPolicy.All, }).Error.ShouldBeNull(); streams.Capture("orders.created", "msg-1"u8.ToArray()); streams.Capture("orders.updated", "msg-2"u8.ToArray()); streams.Capture("orders.created", "msg-3"u8.ToArray()); var batch = await consumers.FetchAsync("ORDERS", "PULL", 3, streams, default); batch.Messages.Count.ShouldBe(3); batch.Messages[0].Sequence.ShouldBe((ulong)1); batch.Messages[1].Sequence.ShouldBe((ulong)2); batch.Messages[2].Sequence.ShouldBe((ulong)3); } // ------------------------------------------------------------------------- // Test 2 – Deliver policy Last starts at the final stored sequence // // Go reference: TestJetStreamConsumerWithMultipleStartOptions – verifies // that DeliverLast causes the consumer cursor to begin at the last message // in the stream rather than seq 1. // ------------------------------------------------------------------------- [Fact] public async Task Pull_consumer_deliver_last_starts_at_final_sequence() { var streams = new StreamManager(); streams.CreateOrUpdate(new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], }).Error.ShouldBeNull(); streams.Capture("orders.a", "first"u8.ToArray()); streams.Capture("orders.b", "second"u8.ToArray()); streams.Capture("orders.c", "third"u8.ToArray()); var consumers = new ConsumerManager(); consumers.CreateOrUpdate("ORDERS", new ConsumerConfig { DurableName = "LAST", DeliverPolicy = DeliverPolicy.Last, }).Error.ShouldBeNull(); var batch = await consumers.FetchAsync("ORDERS", "LAST", 5, streams, default); // DeliverLast cursor resolves to sequence 3 (last stored). batch.Messages.Count.ShouldBe(1); batch.Messages[0].Sequence.ShouldBe((ulong)3); } // ------------------------------------------------------------------------- // Test 3 – Deliver policy New skips all messages present at first-fetch time // // Go reference: TestJetStreamConsumerDeliverNewNotConsumingBeforeRestart // (~line 6213) – validates that DeliverNew positions the cursor past the // last stored sequence so that messages already in the stream when the // consumer first fetches are not returned. // // In the .NET port the initial sequence is resolved on the first FetchAsync // call (when NextSequence == 1). DeliverPolicy.New sets the cursor to // lastSeq + 1, so every message present at fetch time is skipped and only // subsequent publishes are visible. // ------------------------------------------------------------------------- [Fact] public async Task Pull_consumer_deliver_new_skips_messages_present_at_first_fetch() { var streams = new StreamManager(); streams.CreateOrUpdate(new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], }).Error.ShouldBeNull(); streams.Capture("orders.a", "pre-1"u8.ToArray()); streams.Capture("orders.b", "pre-2"u8.ToArray()); var consumers = new ConsumerManager(); consumers.CreateOrUpdate("ORDERS", new ConsumerConfig { DurableName = "NEW", DeliverPolicy = DeliverPolicy.New, }).Error.ShouldBeNull(); // First fetch: resolves cursor to lastSeq+1 = 3, which has no message yet. var empty = await consumers.FetchAsync("ORDERS", "NEW", 5, streams, default); empty.Messages.Count.ShouldBe(0); // Now publish a new message – this is the "new" message after the cursor. streams.Capture("orders.c", "post-1"u8.ToArray()); // Second fetch: cursor is already at 3, the newly published message is at 3. var batch = await consumers.FetchAsync("ORDERS", "NEW", 5, streams, default); batch.Messages.Count.ShouldBe(1); batch.Messages[0].Sequence.ShouldBe((ulong)3); } // ------------------------------------------------------------------------- // Test 4 – Deliver policy ByStartTime resolves cursor at the correct seq // // Go reference: TestJetStreamConsumerWithStartTime (~line 3160) – publishes // messages before a recorded timestamp, then creates a consumer with // DeliverByStartTime and verifies the first delivered sequence matches the // first message after that timestamp. // ------------------------------------------------------------------------- [Fact] public async Task Pull_consumer_deliver_by_start_time_resolves_correct_starting_sequence() { var streams = new StreamManager(); streams.CreateOrUpdate(new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], }).Error.ShouldBeNull(); streams.Capture("orders.a", "before-1"u8.ToArray()); streams.Capture("orders.b", "before-2"u8.ToArray()); // Brief pause so that stored timestamps of pre-existing messages are // strictly before the cut point we are about to record. await Task.Delay(10); var startTime = DateTime.UtcNow; streams.Capture("orders.c", "after-1"u8.ToArray()); streams.Capture("orders.d", "after-2"u8.ToArray()); var consumers = new ConsumerManager(); consumers.CreateOrUpdate("ORDERS", new ConsumerConfig { DurableName = "BYTIME", DeliverPolicy = DeliverPolicy.ByStartTime, OptStartTimeUtc = startTime, }).Error.ShouldBeNull(); var batch = await consumers.FetchAsync("ORDERS", "BYTIME", 5, streams, default); // Only messages with timestamp >= startTime should be returned. batch.Messages.Count.ShouldBe(2); batch.Messages.All(m => m.Sequence >= 3).ShouldBeTrue(); } // ------------------------------------------------------------------------- // Test 5 – AckAll advances the ack floor and blocks re-delivery of acked msgs // // Go reference: TestJetStreamConsumerAckFloorFill (~line 3404) – publishes // four messages, acks all via AckAll on seq 4, and then verifies that a // subsequent fetch returns zero messages because every sequence is at or // below the ack floor. // ------------------------------------------------------------------------- [Fact] public async Task Explicit_ack_all_advances_floor_and_suppresses_redelivery() { var streams = new StreamManager(); streams.CreateOrUpdate(new StreamConfig { Name = "ORDERS", Subjects = ["orders.*"], }).Error.ShouldBeNull(); var consumers = new ConsumerManager(); consumers.CreateOrUpdate("ORDERS", new ConsumerConfig { DurableName = "ACK", AckPolicy = AckPolicy.Explicit, AckWaitMs = 100, }).Error.ShouldBeNull(); for (var i = 1; i <= 4; i++) streams.Capture("orders.created", Encoding.UTF8.GetBytes($"msg-{i}")); var first = await consumers.FetchAsync("ORDERS", "ACK", 4, streams, default); first.Messages.Count.ShouldBe(4); // AckAll up to sequence 4 should advance floor and clear all pending. consumers.AckAll("ORDERS", "ACK", 4); // A subsequent fetch must return no messages because the ack floor // now covers all published sequences and there are no new messages. var second = await consumers.FetchAsync("ORDERS", "ACK", 4, streams, default); second.Messages.Count.ShouldBe(0); } }