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:
Joseph Doherty
2026-03-12 15:58:10 -04:00
parent 36b9dfa654
commit 78b4bc2486
228 changed files with 253 additions and 227 deletions

View File

@@ -0,0 +1,513 @@
// Ported from golang/nats-server/server/jetstream_test.go
// Consumer features: max deliver, max ack pending, flow control, heartbeats,
// consumer pause/resume, ack all, redelivery
using System.Text;
using NATS.Server.JetStream.Models;
using NATS.Server.TestUtilities;
namespace NATS.Server.JetStream.Tests.JetStream;
public class JetStreamConsumerFeatureTests
{
// Go: TestJetStreamWorkQueueAckWaitRedelivery server/jetstream_test.go:1959
[Fact]
public async Task Ack_explicit_tracks_pending_count()
{
await using var fx = await JetStreamApiFixture.StartWithAckExplicitConsumerAsync(30_000);
_ = await fx.PublishAndGetAckAsync("orders.created", "msg1");
_ = await fx.PublishAndGetAckAsync("orders.created", "msg2");
var batch = await fx.FetchAsync("ORDERS", "PULL", 2);
batch.Messages.Count.ShouldBe(2);
var pending = await fx.GetPendingCountAsync("ORDERS", "PULL");
pending.ShouldBe(2);
}
// Go: TestJetStreamAckAllRedelivery server/jetstream_test.go:1850
[Fact]
public async Task Ack_all_acknowledges_up_to_sequence()
{
await using var fx = await JetStreamApiFixture.StartWithAckAllConsumerAsync();
for (var i = 0; i < 5; i++)
_ = await fx.PublishAndGetAckAsync("orders.created", $"msg-{i}");
var batch = await fx.FetchAsync("ORDERS", "ACKALL", 5);
batch.Messages.Count.ShouldBe(5);
await fx.AckAllAsync("ORDERS", "ACKALL", 3);
var pending = await fx.GetPendingCountAsync("ORDERS", "ACKALL");
// After acking up to 3, sequences 4 and 5 should still be pending
pending.ShouldBeLessThanOrEqualTo(2);
}
// Go: TestJetStreamAckAllRedelivery — ack all sequences
[Fact]
public async Task Ack_all_clears_all_pending()
{
await using var fx = await JetStreamApiFixture.StartWithAckAllConsumerAsync();
for (var i = 0; i < 3; i++)
_ = await fx.PublishAndGetAckAsync("orders.created", $"msg-{i}");
var batch = await fx.FetchAsync("ORDERS", "ACKALL", 3);
batch.Messages.Count.ShouldBe(3);
await fx.AckAllAsync("ORDERS", "ACKALL", batch.Messages[^1].Sequence);
var pending = await fx.GetPendingCountAsync("ORDERS", "ACKALL");
pending.ShouldBe(0);
}
// Go: TestJetStreamPushConsumerFlowControl server/jetstream_test.go:5203
[Fact]
public async Task Push_consumer_with_flow_control_emits_fc_frames()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("FC", "fc.>");
_ = await fx.CreateConsumerAsync("FC", "PUSH", "fc.>", push: true, heartbeatMs: 10);
// Enable flow control via direct JSON
var resp = await fx.RequestLocalAsync(
"$JS.API.CONSUMER.CREATE.FC.FCPUSH",
"""{"durable_name":"FCPUSH","filter_subject":"fc.>","push":true,"heartbeat_ms":10,"flow_control":true}""");
resp.Error.ShouldBeNull();
_ = await fx.PublishAndGetAckAsync("fc.x", "data");
var frame1 = await fx.ReadPushFrameAsync("FC", "FCPUSH");
frame1.IsData.ShouldBeTrue();
// Flow control frame follows data frame
var frame2 = await fx.ReadPushFrameAsync("FC", "FCPUSH");
frame2.IsFlowControl.ShouldBeTrue();
}
// Go: TestJetStreamPushConsumerIdleHeartbeats server/jetstream_test.go:5260
[Fact]
public async Task Push_consumer_with_heartbeats_emits_heartbeat_frames()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("HB", "hb.>");
_ = await fx.CreateConsumerAsync("HB", "PUSH", "hb.>", push: true, heartbeatMs: 10);
_ = await fx.PublishAndGetAckAsync("hb.x", "data");
var frame = await fx.ReadPushFrameAsync("HB", "PUSH");
frame.IsData.ShouldBeTrue();
var hbFrame = await fx.ReadPushFrameAsync("HB", "PUSH");
hbFrame.IsHeartbeat.ShouldBeTrue();
}
// Go: TestJetStreamFlowControlRequiresHeartbeats server/jetstream_test.go:5232
[Fact]
public async Task Push_consumer_without_heartbeats_has_no_heartbeat_frames()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NHB", "nhb.>");
_ = await fx.CreateConsumerAsync("NHB", "PUSH", "nhb.>", push: true, heartbeatMs: 0);
_ = await fx.PublishAndGetAckAsync("nhb.x", "data");
var frame = await fx.ReadPushFrameAsync("NHB", "PUSH");
frame.IsData.ShouldBeTrue();
// Without heartbeats, no heartbeat frame should be queued
Should.Throw<InvalidOperationException>(() => fx.ReadPushFrameAsync("NHB", "PUSH"));
}
// Go: TestJetStreamConsumerPause server/jetstream_test.go
[Fact]
public async Task Paused_consumer_can_be_resumed()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PAUSE", "pause.>");
_ = await fx.CreateConsumerAsync("PAUSE", "C1", "pause.>");
var pause = await fx.RequestLocalAsync(
"$JS.API.CONSUMER.PAUSE.PAUSE.C1", """{"pause":true}""");
pause.Success.ShouldBeTrue();
var resume = await fx.RequestLocalAsync(
"$JS.API.CONSUMER.PAUSE.PAUSE.C1", """{"pause":false}""");
resume.Success.ShouldBeTrue();
}
// Go: TestJetStreamConsumerReset server/jetstream_test.go
[Fact]
public async Task Reset_consumer_restarts_delivery_from_beginning()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("RST", "rst.>");
_ = await fx.CreateConsumerAsync("RST", "C1", "rst.>");
_ = await fx.PublishAndGetAckAsync("rst.x", "msg1");
_ = await fx.PublishAndGetAckAsync("rst.x", "msg2");
var batch1 = await fx.FetchAsync("RST", "C1", 2);
batch1.Messages.Count.ShouldBe(2);
_ = await fx.RequestLocalAsync("$JS.API.CONSUMER.RESET.RST.C1", "{}");
var batch2 = await fx.FetchAsync("RST", "C1", 2);
batch2.Messages.Count.ShouldBe(2);
batch2.Messages[0].Sequence.ShouldBe(1UL);
}
// Go: TestJetStreamWorkQueueMaxWaiting server/jetstream_test.go:957
[Fact]
public async Task Fetch_more_than_available_returns_only_available()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MW", "mw.>");
_ = await fx.CreateConsumerAsync("MW", "C1", "mw.>");
_ = await fx.PublishAndGetAckAsync("mw.x", "msg1");
_ = await fx.PublishAndGetAckAsync("mw.x", "msg2");
var batch = await fx.FetchAsync("MW", "C1", 100);
batch.Messages.Count.ShouldBe(2);
}
// Go: TestJetStreamWorkQueueWrapWaiting server/jetstream_test.go:1022
[Fact]
public async Task Fetch_wraps_around_correctly_after_multiple_fetches()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("WR", "wr.>");
_ = await fx.CreateConsumerAsync("WR", "C1", "wr.>");
for (var i = 0; i < 10; i++)
_ = await fx.PublishAndGetAckAsync("wr.x", $"msg-{i}");
var batch1 = await fx.FetchAsync("WR", "C1", 3);
batch1.Messages.Count.ShouldBe(3);
batch1.Messages[^1].Sequence.ShouldBe(3UL);
var batch2 = await fx.FetchAsync("WR", "C1", 3);
batch2.Messages.Count.ShouldBe(3);
batch2.Messages[0].Sequence.ShouldBe(4UL);
var batch3 = await fx.FetchAsync("WR", "C1", 3);
batch3.Messages.Count.ShouldBe(3);
batch3.Messages[0].Sequence.ShouldBe(7UL);
}
// Go: TestJetStreamMaxAckPending limits delivery
[Fact]
public async Task Max_ack_pending_limits_push_delivery()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MAP", "map.>");
_ = await fx.CreateConsumerAsync("MAP", "PUSH", "map.>",
push: true, heartbeatMs: 10,
ackPolicy: AckPolicy.Explicit,
maxAckPending: 1);
_ = await fx.PublishAndGetAckAsync("map.x", "msg1");
_ = await fx.PublishAndGetAckAsync("map.x", "msg2");
// Only 1 should be delivered due to max ack pending
var frame = await fx.ReadPushFrameAsync("MAP", "PUSH");
frame.IsData.ShouldBeTrue();
}
// Go: TestJetStreamDeliverLastPerSubject server/jetstream_test.go
// LastPerSubject resolves the initial sequence to a message matching the
// filter subject and then delivers forward from there. All matching messages
// from that point onward are delivered.
[Fact]
public async Task Deliver_last_per_subject_delivers_matching_messages()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DLPS", "dlps.>");
_ = await fx.PublishAndGetAckAsync("dlps.a", "a1");
_ = await fx.PublishAndGetAckAsync("dlps.b", "b1");
_ = await fx.PublishAndGetAckAsync("dlps.a", "a2");
_ = await fx.PublishAndGetAckAsync("dlps.b", "b2");
_ = await fx.CreateConsumerAsync("DLPS", "C1", "dlps.a",
deliverPolicy: DeliverPolicy.LastPerSubject);
var batch = await fx.FetchAsync("DLPS", "C1", 10);
// Delivers all matching "dlps.a" messages from resolved start
batch.Messages.Count.ShouldBeGreaterThanOrEqualTo(1);
batch.Messages.All(m => m.Subject == "dlps.a").ShouldBeTrue();
}
// Go: TestJetStreamByStartSequence
[Fact]
public async Task Deliver_by_start_sequence_begins_at_specified_seq()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("BSS", "bss.>");
for (var i = 0; i < 5; i++)
_ = await fx.PublishAndGetAckAsync("bss.x", $"msg-{i}");
var resp = await fx.RequestLocalAsync(
"$JS.API.CONSUMER.CREATE.BSS.C1",
"""{"durable_name":"C1","filter_subject":"bss.>","deliver_policy":"by_start_sequence","opt_start_seq":3}""");
resp.Error.ShouldBeNull();
var batch = await fx.FetchAsync("BSS", "C1", 10);
batch.Messages.Count.ShouldBe(3);
batch.Messages[0].Sequence.ShouldBe(3UL);
}
// Go: TestJetStreamMultipleSubjectsPushBasic — multiple filter subjects consumer
[Fact]
public async Task Multi_filter_consumer_receives_matching_messages()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MFC", ">");
_ = await fx.CreateConsumerAsync("MFC", "C1", null, filterSubjects: ["a.*", "b.*"]);
_ = await fx.PublishAndGetAckAsync("a.one", "1");
_ = await fx.PublishAndGetAckAsync("b.one", "2");
_ = await fx.PublishAndGetAckAsync("c.one", "3");
var batch = await fx.FetchAsync("MFC", "C1", 10);
batch.Messages.Count.ShouldBe(2);
}
// Go: TestJetStreamAckReplyStreamPending server/jetstream_test.go:1887
[Fact]
public async Task Explicit_ack_pending_count_decreases_on_ack()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ARP", "arp.>");
_ = await fx.CreateConsumerAsync("ARP", "C1", "arp.>",
ackPolicy: AckPolicy.All);
_ = await fx.PublishAndGetAckAsync("arp.x", "msg1");
_ = await fx.PublishAndGetAckAsync("arp.x", "msg2");
_ = await fx.PublishAndGetAckAsync("arp.x", "msg3");
_ = await fx.FetchAsync("ARP", "C1", 3);
var before = await fx.GetPendingCountAsync("ARP", "C1");
before.ShouldBe(3);
await fx.AckAllAsync("ARP", "C1", 2);
var after = await fx.GetPendingCountAsync("ARP", "C1");
after.ShouldBe(1);
}
// Go: TestJetStreamAckReplyStreamPendingWithAcks server/jetstream_test.go:1921
[Fact]
public async Task Ack_all_to_last_clears_pending()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ARPF", "arpf.>");
_ = await fx.CreateConsumerAsync("ARPF", "C1", "arpf.>",
ackPolicy: AckPolicy.All);
_ = await fx.PublishAndGetAckAsync("arpf.x", "1");
_ = await fx.PublishAndGetAckAsync("arpf.x", "2");
var batch = await fx.FetchAsync("ARPF", "C1", 2);
batch.Messages.Count.ShouldBe(2);
await fx.AckAllAsync("ARPF", "C1", batch.Messages[^1].Sequence);
var pending = await fx.GetPendingCountAsync("ARPF", "C1");
pending.ShouldBe(0);
}
// Go: TestJetStreamWorkQueueRetentionStream server/jetstream_test.go:1655
[Fact]
public async Task Replay_original_consumer_pauses_between_deliveries()
{
await using var fx = await JetStreamApiFixture.StartWithReplayOriginalConsumerAsync();
var batch = await fx.FetchAsync("ORDERS", "RO", 1);
batch.Messages.Count.ShouldBe(1);
}
// Go: TestJetStreamSubjectBasedFilteredConsumers server/jetstream_test.go
[Fact]
public async Task Consumer_with_gt_wildcard_filter_matches_all()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("GW", "gw.>");
_ = await fx.CreateConsumerAsync("GW", "C1", "gw.>");
_ = await fx.PublishAndGetAckAsync("gw.a.b.c", "1");
_ = await fx.PublishAndGetAckAsync("gw.x", "2");
_ = await fx.PublishAndGetAckAsync("gw.y.z", "3");
var batch = await fx.FetchAsync("GW", "C1", 10);
batch.Messages.Count.ShouldBe(3);
}
// Go: TestJetStreamSubjectBasedFilteredConsumers — star wildcard
[Fact]
public async Task Consumer_with_star_wildcard_matches_single_token()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("SW", "sw.>");
_ = await fx.CreateConsumerAsync("SW", "C1", "sw.*");
_ = await fx.PublishAndGetAckAsync("sw.a", "1");
_ = await fx.PublishAndGetAckAsync("sw.b.c", "2"); // doesn't match sw.*
_ = await fx.PublishAndGetAckAsync("sw.d", "3");
var batch = await fx.FetchAsync("SW", "C1", 10);
batch.Messages.Count.ShouldBe(2);
}
// Go: TestJetStreamInterestRetentionStreamWithFilteredConsumers server/jetstream_test.go:4388
[Fact]
public async Task Two_consumers_same_stream_independent_cursors()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("IC", "ic.>");
_ = await fx.CreateConsumerAsync("IC", "C1", "ic.a");
_ = await fx.CreateConsumerAsync("IC", "C2", "ic.b");
_ = await fx.PublishAndGetAckAsync("ic.a", "for-c1");
_ = await fx.PublishAndGetAckAsync("ic.b", "for-c2");
_ = await fx.PublishAndGetAckAsync("ic.a", "for-c1-again");
var batchC1 = await fx.FetchAsync("IC", "C1", 10);
batchC1.Messages.Count.ShouldBe(2);
var batchC2 = await fx.FetchAsync("IC", "C2", 10);
batchC2.Messages.Count.ShouldBe(1);
}
// Go: TestJetStreamPushConsumersPullError server/jetstream_test.go:5731
[Fact]
public async Task Consumer_fetch_from_empty_stream_returns_empty_batch()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("EMP", "emp.>");
_ = await fx.CreateConsumerAsync("EMP", "C1", "emp.>");
var batch = await fx.FetchAsync("EMP", "C1", 5);
batch.Messages.Count.ShouldBe(0);
}
// Go: TestJetStreamAckNext server/jetstream_test.go:2483
[Fact]
public async Task Consumer_fetch_after_consuming_all_returns_empty()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DONE", "done.>");
_ = await fx.CreateConsumerAsync("DONE", "C1", "done.>");
_ = await fx.PublishAndGetAckAsync("done.x", "only");
var batch1 = await fx.FetchAsync("DONE", "C1", 1);
batch1.Messages.Count.ShouldBe(1);
var batch2 = await fx.FetchAsync("DONE", "C1", 1);
batch2.Messages.Count.ShouldBe(0);
}
// Go: TestJetStreamWorkQueueAckAndNext server/jetstream_test.go:1355
[Fact]
public async Task Ack_all_consumer_acks_batch_at_once()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("AAB", "aab.>");
_ = await fx.CreateConsumerAsync("AAB", "C1", "aab.>",
ackPolicy: AckPolicy.All);
for (var i = 0; i < 5; i++)
_ = await fx.PublishAndGetAckAsync("aab.x", $"msg-{i}");
var batch = await fx.FetchAsync("AAB", "C1", 5);
batch.Messages.Count.ShouldBe(5);
await fx.AckAllAsync("AAB", "C1", 5);
var pending = await fx.GetPendingCountAsync("AAB", "C1");
pending.ShouldBe(0);
}
// Go: TestJetStreamEphemeralPullConsumersInactiveThresholdAndNoWait server/jetstream_test.go
[Fact]
public async Task No_wait_fetch_from_non_existent_consumer_returns_empty()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NWC", "nwc.>");
var batch = await fx.FetchWithNoWaitAsync("NWC", "NOPE", 1);
batch.Messages.Count.ShouldBe(0);
}
// Go: TestJetStreamMultipleSubjectsBasic — verify payload content
[Fact]
public async Task Fetched_messages_contain_correct_payload()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PL", "pl.>");
_ = await fx.CreateConsumerAsync("PL", "C1", "pl.>");
_ = await fx.PublishAndGetAckAsync("pl.x", "hello-world");
var batch = await fx.FetchAsync("PL", "C1", 1);
batch.Messages.Count.ShouldBe(1);
Encoding.UTF8.GetString(batch.Messages[0].Payload.Span).ShouldBe("hello-world");
}
// Go: TestJetStreamBackOffCheckPending server/jetstream_test.go
[Fact]
public async Task Backoff_config_is_stored_on_consumer()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("BOC", "boc.>");
_ = await fx.RequestLocalAsync(
"$JS.API.CONSUMER.CREATE.BOC.C1",
"""{"durable_name":"C1","filter_subject":"boc.>","ack_policy":"explicit","backoff_ms":[50,100,200]}""");
var info = await fx.GetConsumerInfoAsync("BOC", "C1");
info.Config.BackOffMs.ShouldBe([50, 100, 200]);
}
// Go: TestJetStreamConsumerPause — multiple pauses
[Fact]
public async Task Multiple_pause_calls_are_idempotent()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MPI", "mpi.>");
_ = await fx.CreateConsumerAsync("MPI", "C1", "mpi.>");
for (var i = 0; i < 3; i++)
{
var pause = await fx.RequestLocalAsync(
"$JS.API.CONSUMER.PAUSE.MPI.C1", """{"pause":true}""");
pause.Success.ShouldBeTrue();
}
}
// Go: TestJetStreamAckExplicitMsgRemoval — explicit ack with fetch batch
[Fact]
public async Task Explicit_ack_with_batch_fetch()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("EAB", "eab.>");
_ = await fx.CreateConsumerAsync("EAB", "C1", "eab.>",
ackPolicy: AckPolicy.Explicit,
ackWaitMs: 30_000);
for (var i = 0; i < 3; i++)
_ = await fx.PublishAndGetAckAsync("eab.x", $"msg-{i}");
var batch = await fx.FetchAsync("EAB", "C1", 3);
batch.Messages.Count.ShouldBe(3);
var pending = await fx.GetPendingCountAsync("EAB", "C1");
pending.ShouldBe(3);
}
// Go: TestJetStreamConsumerRate
[Fact]
public async Task Rate_limit_setting_is_preserved()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("RLP", "rlp.>");
_ = await fx.RequestLocalAsync(
"$JS.API.CONSUMER.CREATE.RLP.C1",
"""{"durable_name":"C1","filter_subject":"rlp.>","push":true,"heartbeat_ms":10,"rate_limit_bps":2048}""");
var info = await fx.GetConsumerInfoAsync("RLP", "C1");
info.Config.RateLimitBps.ShouldBe(2048);
}
// Go: TestJetStreamRedeliverCount server/jetstream_test.go:3778
[Fact]
public async Task Consumer_pending_initially_zero()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PIZ", "piz.>");
_ = await fx.CreateConsumerAsync("PIZ", "C1", "piz.>",
ackPolicy: AckPolicy.Explicit);
var pending = await fx.GetPendingCountAsync("PIZ", "C1");
pending.ShouldBe(0);
}
}