refactor: extract NATS.Server.JetStream.Tests project
Move 225 JetStream-related test files from NATS.Server.Tests into a dedicated NATS.Server.JetStream.Tests project. This includes root-level JetStream*.cs files, storage test files (FileStore, MemStore, StreamStoreContract), and the full JetStream/ subfolder tree (Api, Cluster, Consumers, MirrorSource, Snapshots, Storage, Streams). Updated all namespaces, added InternalsVisibleTo, registered in the solution file, and added the JETSTREAM_INTEGRATION_MATRIX define.
This commit is contained in:
@@ -0,0 +1,406 @@
|
||||
// Ported from golang/nats-server/server/jetstream_test.go
|
||||
// Consumer delivery edge cases: ack wait timeout tracking, max deliver attempts,
|
||||
// backoff lists, idle heartbeat config, deliver policies, push vs pull.
|
||||
|
||||
using NATS.Server.JetStream.Consumers;
|
||||
using NATS.Server.JetStream.Models;
|
||||
using NATS.Server.TestUtilities;
|
||||
|
||||
namespace NATS.Server.JetStream.Tests.JetStream;
|
||||
|
||||
public class JetStreamConsumerDeliveryEdgeTests
|
||||
{
|
||||
// Go: TestJetStreamWorkQueueAckWaitRedelivery server/jetstream_test.go:2213
|
||||
// AckWait is stored in consumer config and used by ack processor.
|
||||
[Fact]
|
||||
public async Task Ack_wait_ms_stored_in_consumer_config()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ACKWAIT", "ackwait.>");
|
||||
var resp = await fx.CreateConsumerAsync("ACKWAIT", "C1", "ackwait.>",
|
||||
ackPolicy: AckPolicy.Explicit, ackWaitMs: 250);
|
||||
|
||||
resp.Error.ShouldBeNull();
|
||||
resp.ConsumerInfo!.Config.AckWaitMs.ShouldBe(250);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueAckWaitRedelivery — registers pending on fetch
|
||||
[Fact]
|
||||
public async Task Fetch_with_ack_explicit_registers_pending_messages()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithAckExplicitConsumerAsync(500);
|
||||
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", "msg1");
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", "msg2");
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", "msg3");
|
||||
|
||||
var batch = await fx.FetchAsync("ORDERS", "PULL", 3);
|
||||
batch.Messages.Count.ShouldBe(3);
|
||||
|
||||
var pending = await fx.GetPendingCountAsync("ORDERS", "PULL");
|
||||
pending.ShouldBe(3);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueNakRedelivery server/jetstream_test.go:2311
|
||||
// After ack all, pending count drops to zero.
|
||||
[Fact]
|
||||
public async Task Ack_all_on_explicit_consumer_clears_all_pending()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithAckExplicitConsumerAsync(30_000);
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", $"m{i}");
|
||||
|
||||
var batch = await fx.FetchAsync("ORDERS", "PULL", 5);
|
||||
batch.Messages.Count.ShouldBe(5);
|
||||
|
||||
await fx.AckAllAsync("ORDERS", "PULL", batch.Messages[^1].Sequence);
|
||||
var pending = await fx.GetPendingCountAsync("ORDERS", "PULL");
|
||||
pending.ShouldBe(0);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamAckAllRedelivery server/jetstream_test.go:1921
|
||||
// Ack all up to sequence N leaves messages above N still pending.
|
||||
[Fact]
|
||||
public async Task Ack_all_up_to_mid_sequence_leaves_tail_pending()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithAckAllConsumerAsync();
|
||||
|
||||
for (var i = 0; i < 6; i++)
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", $"m{i}");
|
||||
|
||||
var batch = await fx.FetchAsync("ORDERS", "ACKALL", 6);
|
||||
batch.Messages.Count.ShouldBe(6);
|
||||
|
||||
// Ack messages 1-3 only
|
||||
await fx.AckAllAsync("ORDERS", "ACKALL", batch.Messages[2].Sequence);
|
||||
|
||||
var pending = await fx.GetPendingCountAsync("ORDERS", "ACKALL");
|
||||
// Messages 4, 5, 6 should still be pending
|
||||
pending.ShouldBeGreaterThan(0);
|
||||
pending.ShouldBeLessThanOrEqualTo(3);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamPushConsumerIdleHeartbeats server/jetstream_test.go:5804
|
||||
// Push consumer with heartbeats configured is created without error.
|
||||
[Fact]
|
||||
public async Task Push_consumer_with_heartbeats_is_created_successfully()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("HBT", "hbt.>");
|
||||
var resp = await fx.CreateConsumerAsync("HBT", "PUSHH", "hbt.>", push: true, heartbeatMs: 100);
|
||||
|
||||
resp.Error.ShouldBeNull();
|
||||
resp.ConsumerInfo!.Config.HeartbeatMs.ShouldBe(100);
|
||||
resp.ConsumerInfo.Config.Push.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// Go: TestJetStreamFlowControlRequiresHeartbeats server/jetstream_test.go:5784
|
||||
// Flow control can be configured on push consumer alongside heartbeats.
|
||||
[Fact]
|
||||
public async Task Push_consumer_with_flow_control_config_is_accepted()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("FCHB", "fchb.>");
|
||||
var resp = await fx.RequestLocalAsync(
|
||||
"$JS.API.CONSUMER.CREATE.FCHB.FC1",
|
||||
"""{"durable_name":"FC1","filter_subject":"fchb.>","push":true,"heartbeat_ms":50,"flow_control":true}""");
|
||||
|
||||
resp.Error.ShouldBeNull();
|
||||
resp.ConsumerInfo!.Config.Push.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// Go: TestJetStreamActiveDelivery server/jetstream_test.go:3726
|
||||
// Push consumer receives messages published after creation.
|
||||
[Fact]
|
||||
public async Task Push_consumer_receives_published_message()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithPushConsumerAsync();
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", "order-data");
|
||||
|
||||
var frame = await fx.ReadPushFrameAsync("ORDERS", "PUSH");
|
||||
frame.IsData.ShouldBeTrue();
|
||||
frame.Subject.ShouldBe("orders.created");
|
||||
}
|
||||
|
||||
// Go: TestJetStreamBasicDeliverSubject server/jetstream_test.go:844
|
||||
// Push consumer heartbeat frame is emitted after data frame.
|
||||
[Fact]
|
||||
public async Task Push_consumer_emits_heartbeat_frame_after_data()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithPushConsumerAsync();
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", "first");
|
||||
|
||||
var dataFrame = await fx.ReadPushFrameAsync("ORDERS", "PUSH");
|
||||
dataFrame.IsData.ShouldBeTrue();
|
||||
|
||||
var hbFrame = await fx.ReadPushFrameAsync("ORDERS", "PUSH");
|
||||
hbFrame.IsHeartbeat.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// Go: TestJetStreamPushConsumerFlowControl server/jetstream_test.go:5690
|
||||
// Flow control frame follows data frame when enabled.
|
||||
[Fact]
|
||||
public async Task Push_consumer_with_fc_emits_fc_frame_after_data()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PUSHFC", "pushfc.>");
|
||||
_ = await fx.RequestLocalAsync(
|
||||
"$JS.API.CONSUMER.CREATE.PUSHFC.FCTEST",
|
||||
"""{"durable_name":"FCTEST","filter_subject":"pushfc.>","push":true,"heartbeat_ms":10,"flow_control":true}""");
|
||||
|
||||
_ = await fx.PublishAndGetAckAsync("pushfc.msg", "data");
|
||||
|
||||
var dataFrame = await fx.ReadPushFrameAsync("PUSHFC", "FCTEST");
|
||||
dataFrame.IsData.ShouldBeTrue();
|
||||
|
||||
var fcFrame = await fx.ReadPushFrameAsync("PUSHFC", "FCTEST");
|
||||
fcFrame.IsFlowControl.ShouldBeTrue();
|
||||
}
|
||||
|
||||
// Go: TestJetStreamEphemeralConsumers server/jetstream_test.go:3781
|
||||
// Ephemeral consumer is created with generated durable name.
|
||||
[Fact]
|
||||
public async Task Ephemeral_consumer_gets_generated_name()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("EPH", "eph.>");
|
||||
var resp = await fx.CreateConsumerAsync("EPH", "EPHNAME", "eph.>", ephemeral: true);
|
||||
|
||||
resp.Error.ShouldBeNull();
|
||||
resp.ConsumerInfo.ShouldNotBeNull();
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueMaxWaiting server/jetstream_test.go:1094
|
||||
// Pull consumer fetch with no_wait returns immediately with available messages.
|
||||
[Fact]
|
||||
public async Task Fetch_no_wait_returns_available_messages_immediately()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithPullConsumerAsync();
|
||||
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", "msg1");
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", "msg2");
|
||||
|
||||
var batch = await fx.FetchWithNoWaitAsync("ORDERS", "PULL", 10);
|
||||
batch.Messages.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueMaxWaiting — fetch when empty returns zero
|
||||
[Fact]
|
||||
public async Task Fetch_no_wait_returns_empty_when_no_messages()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithPullConsumerAsync();
|
||||
|
||||
var batch = await fx.FetchWithNoWaitAsync("ORDERS", "PULL", 10);
|
||||
batch.Messages.Count.ShouldBe(0);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueAckAndNext server/jetstream_test.go:1634
|
||||
// Fetching after acking gives next available messages.
|
||||
[Fact]
|
||||
public async Task Fetch_after_ack_all_returns_next_messages()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithAckAllConsumerAsync();
|
||||
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", "msg1");
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", "msg2");
|
||||
|
||||
var batch1 = await fx.FetchAsync("ORDERS", "ACKALL", 1);
|
||||
batch1.Messages.Count.ShouldBe(1);
|
||||
|
||||
await fx.AckAllAsync("ORDERS", "ACKALL", batch1.Messages[0].Sequence);
|
||||
|
||||
var batch2 = await fx.FetchAsync("ORDERS", "ACKALL", 1);
|
||||
batch2.Messages.Count.ShouldBe(1);
|
||||
batch2.Messages[0].Sequence.ShouldBeGreaterThan(batch1.Messages[0].Sequence);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamRedeliverCount server/jetstream_test.go:3959
|
||||
// AckProcessor tracks pending count correctly per delivery.
|
||||
[Fact]
|
||||
public void Ack_processor_registers_and_clears_pending_entries()
|
||||
{
|
||||
var proc = new AckProcessor();
|
||||
|
||||
proc.Register(1, 30_000);
|
||||
proc.Register(2, 30_000);
|
||||
proc.Register(3, 30_000);
|
||||
|
||||
proc.PendingCount.ShouldBe(3);
|
||||
|
||||
proc.AckAll(2);
|
||||
proc.PendingCount.ShouldBe(1); // only seq 3 remains
|
||||
|
||||
proc.AckAll(3);
|
||||
proc.PendingCount.ShouldBe(0);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamRedeliverCount — ack floor advances monotonically
|
||||
[Fact]
|
||||
public void Ack_processor_ack_floor_advances_after_ack_all()
|
||||
{
|
||||
var proc = new AckProcessor();
|
||||
|
||||
proc.Register(1, 30_000);
|
||||
proc.Register(2, 30_000);
|
||||
proc.Register(3, 30_000);
|
||||
|
||||
proc.AckFloor.ShouldBe(0UL);
|
||||
proc.AckAll(2);
|
||||
proc.AckFloor.ShouldBe(2UL);
|
||||
proc.AckAll(3);
|
||||
proc.AckFloor.ShouldBe(3UL);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueAckWaitRedelivery — expired entry detected
|
||||
[Fact]
|
||||
public async Task Ack_processor_detects_expired_pending_entry()
|
||||
{
|
||||
var proc = new AckProcessor();
|
||||
proc.Register(1, 20); // 20ms ack wait
|
||||
|
||||
await Task.Delay(50);
|
||||
|
||||
proc.TryGetExpired(out var seq, out _).ShouldBeTrue();
|
||||
seq.ShouldBe(1UL);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueTerminateDelivery server/jetstream_test.go:2465
|
||||
// Drop removes a pending entry from the processor.
|
||||
[Fact]
|
||||
public void Ack_processor_drop_removes_pending_entry()
|
||||
{
|
||||
var proc = new AckProcessor();
|
||||
proc.Register(1, 30_000);
|
||||
proc.Register(2, 30_000);
|
||||
|
||||
proc.Drop(1);
|
||||
proc.PendingCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamPushConsumerIdleHeartbeatsWithFilterSubject server/jetstream_test.go:5864
|
||||
// Push consumer with heartbeats and filter subject is created without error.
|
||||
[Fact]
|
||||
public async Task Push_consumer_with_heartbeats_and_filter_subject()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("HBFILT", "hbfilt.>");
|
||||
var resp = await fx.CreateConsumerAsync(
|
||||
"HBFILT", "HBCONS", "hbfilt.orders",
|
||||
push: true, heartbeatMs: 100);
|
||||
|
||||
resp.Error.ShouldBeNull();
|
||||
resp.ConsumerInfo!.Config.FilterSubject.ShouldBe("hbfilt.orders");
|
||||
resp.ConsumerInfo.Config.HeartbeatMs.ShouldBe(100);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamAckNext server/jetstream_test.go:2565
|
||||
// Consumer advances sequence correctly after each fetch.
|
||||
[Fact]
|
||||
public async Task Consumer_sequence_advances_with_each_fetch()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithPullConsumerAsync();
|
||||
|
||||
for (var i = 0; i < 5; i++)
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", $"msg-{i}");
|
||||
|
||||
var seqs = new List<ulong>();
|
||||
for (var i = 0; i < 5; i++)
|
||||
{
|
||||
var batch = await fx.FetchAsync("ORDERS", "PULL", 1);
|
||||
batch.Messages.Count.ShouldBe(1);
|
||||
seqs.Add(batch.Messages[0].Sequence);
|
||||
}
|
||||
|
||||
seqs.ShouldBeInOrder();
|
||||
seqs.Distinct().Count().ShouldBe(5); // all unique sequences
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueAckWaitRedelivery — schedule redelivery increases delivery count
|
||||
[Fact]
|
||||
public void Ack_processor_schedule_redelivery_increments_delivery_count()
|
||||
{
|
||||
var proc = new AckProcessor();
|
||||
proc.Register(1, 30_000);
|
||||
proc.ScheduleRedelivery(1, 30_000);
|
||||
|
||||
// After rescheduling, pending is still 1
|
||||
proc.PendingCount.ShouldBe(1);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueRequest server/jetstream_test.go:1267
|
||||
// Fetch batch respects count limit.
|
||||
[Fact]
|
||||
public async Task Fetch_batch_respects_count_limit()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithPullConsumerAsync();
|
||||
|
||||
for (var i = 0; i < 10; i++)
|
||||
_ = await fx.PublishAndGetAckAsync("orders.created", $"data-{i}");
|
||||
|
||||
var batch = await fx.FetchAsync("ORDERS", "PULL", 3);
|
||||
batch.Messages.Count.ShouldBe(3);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamSubjectFiltering server/jetstream_test.go:1385
|
||||
// Consumer with filter only delivers matching messages.
|
||||
[Fact]
|
||||
public async Task Consumer_filter_delivers_only_matching_messages()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("FILTDEL", "filtdel.>");
|
||||
_ = await fx.CreateConsumerAsync("FILTDEL", "FILTCONS", "filtdel.orders");
|
||||
|
||||
_ = await fx.PublishAndGetAckAsync("filtdel.orders", "order-1");
|
||||
_ = await fx.PublishAndGetAckAsync("filtdel.events", "event-1");
|
||||
_ = await fx.PublishAndGetAckAsync("filtdel.orders", "order-2");
|
||||
|
||||
var batch = await fx.FetchAsync("FILTDEL", "FILTCONS", 10);
|
||||
batch.Messages.Count.ShouldBe(2);
|
||||
batch.Messages.All(m => m.Subject == "filtdel.orders").ShouldBeTrue();
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWildcardSubjectFiltering server/jetstream_test.go:1522
|
||||
// Consumer with wildcard filter delivers only matching messages.
|
||||
[Fact]
|
||||
public async Task Consumer_wildcard_filter_delivers_matching_messages()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("WCFILT", "wcfilt.>");
|
||||
_ = await fx.CreateConsumerAsync("WCFILT", "WCC", "wcfilt.orders.*");
|
||||
|
||||
_ = await fx.PublishAndGetAckAsync("wcfilt.orders.created", "1");
|
||||
_ = await fx.PublishAndGetAckAsync("wcfilt.events.logged", "2");
|
||||
_ = await fx.PublishAndGetAckAsync("wcfilt.orders.shipped", "3");
|
||||
|
||||
var batch = await fx.FetchAsync("WCFILT", "WCC", 10);
|
||||
batch.Messages.Count.ShouldBe(2);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueRequestBatch server/jetstream_test.go:1703
|
||||
// Batch fetch returns all available up to limit.
|
||||
[Fact]
|
||||
public async Task Batch_fetch_returns_all_available_messages_up_to_limit()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("BATCHFULL", "batchfull.>");
|
||||
_ = await fx.CreateConsumerAsync("BATCHFULL", "BC", "batchfull.>");
|
||||
|
||||
for (var i = 0; i < 7; i++)
|
||||
_ = await fx.PublishAndGetAckAsync("batchfull.x", $"msg-{i}");
|
||||
|
||||
var batch = await fx.FetchAsync("BATCHFULL", "BC", 10);
|
||||
batch.Messages.Count.ShouldBe(7);
|
||||
}
|
||||
|
||||
// Go: TestJetStreamWorkQueueRetentionStream server/jetstream_test.go:1788
|
||||
// Pull consumer on work queue stream receives messages.
|
||||
[Fact]
|
||||
public async Task Work_queue_pull_consumer_receives_messages()
|
||||
{
|
||||
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
|
||||
{
|
||||
Name = "WQR",
|
||||
Subjects = ["wqr.>"],
|
||||
Retention = RetentionPolicy.WorkQueue,
|
||||
});
|
||||
_ = await fx.CreateConsumerAsync("WQR", "WQC", "wqr.>");
|
||||
|
||||
_ = await fx.PublishAndGetAckAsync("wqr.task", "task1");
|
||||
_ = await fx.PublishAndGetAckAsync("wqr.task", "task2");
|
||||
|
||||
var batch = await fx.FetchAsync("WQR", "WQC", 5);
|
||||
batch.Messages.Count.ShouldBe(2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user