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,602 @@
// Ported from golang/nats-server/server/jetstream_test.go
// Admin operations: stream/consumer list/names, account info, stream leader stepdown,
// peer info, account purge, server remove, API routing
using System.Text;
using NATS.Server.Auth;
using NATS.Server.JetStream;
using NATS.Server.JetStream.Api;
using NATS.Server.JetStream.Cluster;
using NATS.Server.JetStream.Models;
using NATS.Server.TestUtilities;
namespace NATS.Server.JetStream.Tests.JetStream;
public class JetStreamAdminTests
{
// Go: TestJetStreamRequestAPI server/jetstream_test.go:5429
[Fact]
public async Task Account_info_returns_stream_and_consumer_counts()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("S1", "s1.>");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S2", """{"subjects":["s2.>"]}""");
_ = await fx.CreateConsumerAsync("S1", "C1", "s1.>");
var info = await fx.RequestLocalAsync("$JS.API.INFO", "{}");
info.Error.ShouldBeNull();
info.AccountInfo.ShouldNotBeNull();
info.AccountInfo!.Streams.ShouldBe(2);
info.AccountInfo.Consumers.ShouldBe(1);
}
// Go: TestJetStreamRequestAPI — account info with zero
[Fact]
public void Account_info_empty_returns_zero_counts()
{
var router = new JetStreamApiRouter(new StreamManager(), new ConsumerManager());
var resp = router.Route("$JS.API.INFO", "{}"u8);
resp.Error.ShouldBeNull();
resp.AccountInfo.ShouldNotBeNull();
resp.AccountInfo!.Streams.ShouldBe(0);
resp.AccountInfo.Consumers.ShouldBe(0);
}
// Go: TestJetStreamFilteredStreamNames server/jetstream_test.go:5392
[Fact]
public async Task Stream_names_returns_all_streams()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ALPHA", "alpha.>");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.BETA", """{"subjects":["beta.>"]}""");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.GAMMA", """{"subjects":["gamma.>"]}""");
var names = await fx.RequestLocalAsync("$JS.API.STREAM.NAMES", "{}");
names.StreamNames.ShouldNotBeNull();
names.StreamNames!.Count.ShouldBe(3);
names.StreamNames.ShouldContain("ALPHA");
names.StreamNames.ShouldContain("BETA");
names.StreamNames.ShouldContain("GAMMA");
}
// Go: TestJetStreamFilteredStreamNames — names sorted
[Fact]
public async Task Stream_names_are_sorted()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ZZZ", "zzz.>");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.AAA", """{"subjects":["aaa.>"]}""");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.MMM", """{"subjects":["mmm.>"]}""");
var names = await fx.RequestLocalAsync("$JS.API.STREAM.NAMES", "{}");
names.StreamNames![0].ShouldBe("AAA");
names.StreamNames[1].ShouldBe("MMM");
names.StreamNames[2].ShouldBe("ZZZ");
}
// Go: TestJetStreamStreamList
[Fact]
public async Task Stream_list_returns_same_as_names()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("L1", "l1.>");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.L2", """{"subjects":["l2.>"]}""");
var names = await fx.RequestLocalAsync("$JS.API.STREAM.NAMES", "{}");
var list = await fx.RequestLocalAsync("$JS.API.STREAM.LIST", "{}");
list.StreamNames!.Count.ShouldBe(names.StreamNames!.Count);
}
// Go: TestJetStreamFilteredStreamNames — empty after delete all
[Fact]
public async Task Stream_names_empty_after_all_deleted()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DEL1", "del1.>");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.DEL2", """{"subjects":["del2.>"]}""");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.DELETE.DEL1", "{}");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.DELETE.DEL2", "{}");
var names = await fx.RequestLocalAsync("$JS.API.STREAM.NAMES", "{}");
names.StreamNames!.Count.ShouldBe(0);
}
// Go: TestJetStreamConsumerList
[Fact]
public async Task Consumer_names_returns_all_consumers()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("CL", "cl.>");
_ = await fx.CreateConsumerAsync("CL", "A", "cl.a");
_ = await fx.CreateConsumerAsync("CL", "B", "cl.b");
_ = await fx.CreateConsumerAsync("CL", "C", "cl.c");
var names = await fx.RequestLocalAsync("$JS.API.CONSUMER.NAMES.CL", "{}");
names.ConsumerNames.ShouldNotBeNull();
names.ConsumerNames!.Count.ShouldBe(3);
}
// Go: TestJetStreamConsumerList — names sorted
[Fact]
public async Task Consumer_names_are_sorted()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("CS", "cs.>");
_ = await fx.CreateConsumerAsync("CS", "ZZZ", "cs.>");
_ = await fx.CreateConsumerAsync("CS", "AAA", "cs.>");
_ = await fx.CreateConsumerAsync("CS", "MMM", "cs.>");
var names = await fx.RequestLocalAsync("$JS.API.CONSUMER.NAMES.CS", "{}");
names.ConsumerNames![0].ShouldBe("AAA");
names.ConsumerNames[1].ShouldBe("MMM");
names.ConsumerNames[2].ShouldBe("ZZZ");
}
// Go: TestJetStreamConsumerList — list matches names
[Fact]
public async Task Consumer_list_returns_same_as_names()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("CLM", "clm.>");
_ = await fx.CreateConsumerAsync("CLM", "C1", "clm.>");
_ = await fx.CreateConsumerAsync("CLM", "C2", "clm.>");
var names = await fx.RequestLocalAsync("$JS.API.CONSUMER.NAMES.CLM", "{}");
var list = await fx.RequestLocalAsync("$JS.API.CONSUMER.LIST.CLM", "{}");
list.ConsumerNames!.Count.ShouldBe(names.ConsumerNames!.Count);
}
// Go: TestJetStreamConsumerList — empty after delete all
[Fact]
public async Task Consumer_names_empty_after_all_deleted()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("CD", "cd.>");
_ = await fx.CreateConsumerAsync("CD", "C1", "cd.>");
_ = await fx.RequestLocalAsync("$JS.API.CONSUMER.DELETE.CD.C1", "{}");
var names = await fx.RequestLocalAsync("$JS.API.CONSUMER.NAMES.CD", "{}");
names.ConsumerNames!.Count.ShouldBe(0);
}
// Go: TestJetStreamStreamLeaderStepdown
[Fact]
public async Task Stream_leader_stepdown_returns_success()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("SLD", "sld.>");
var resp = await fx.RequestLocalAsync("$JS.API.STREAM.LEADER.STEPDOWN.SLD", "{}");
resp.Success.ShouldBeTrue();
}
// Go: TestJetStreamStreamPeerRemove
[Fact]
public async Task Stream_peer_remove_returns_success()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("SPR", "spr.>");
var resp = await fx.RequestLocalAsync("$JS.API.STREAM.PEER.REMOVE.SPR", "{}");
resp.Success.ShouldBeTrue();
}
// Go: TestJetStreamConsumerLeaderStepdown
[Fact]
public async Task Consumer_leader_stepdown_returns_success()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("CLSD", "clsd.>");
_ = await fx.CreateConsumerAsync("CLSD", "C1", "clsd.>");
var resp = await fx.RequestLocalAsync("$JS.API.CONSUMER.LEADER.STEPDOWN.CLSD.C1", "{}");
resp.Success.ShouldBeTrue();
}
// Go: TestJetStreamAccountPurge server/jetstream_test.go
[Fact]
public async Task Account_purge_returns_success()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("AP", "ap.>");
var resp = await fx.RequestLocalAsync("$JS.API.ACCOUNT.PURGE.DEFAULT", "{}");
resp.Success.ShouldBeTrue();
}
// Go: TestJetStreamServerRemove
[Fact]
public void Server_remove_returns_success()
{
var router = new JetStreamApiRouter();
var resp = router.Route("$JS.API.SERVER.REMOVE", "{}"u8);
resp.Success.ShouldBeTrue();
}
// Go: TestJetStreamAccountStreamMove
[Fact]
public async Task Account_stream_move_returns_success()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ASM", "asm.>");
var resp = await fx.RequestLocalAsync("$JS.API.ACCOUNT.STREAM.MOVE.MYSTREAM", "{}");
resp.Success.ShouldBeTrue();
}
// Go: TestJetStreamAccountStreamMoveCancel
[Fact]
public async Task Account_stream_move_cancel_returns_success()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ASMC", "asmc.>");
var resp = await fx.RequestLocalAsync("$JS.API.ACCOUNT.STREAM.MOVE.CANCEL.MYSTREAM", "{}");
resp.Success.ShouldBeTrue();
}
// Go: TestJetStreamRequestAPI — unknown subject
[Fact]
public void Unknown_api_subject_returns_not_found()
{
var router = new JetStreamApiRouter();
var resp = router.Route("$JS.API.UNKNOWN.THING", "{}"u8);
resp.Error.ShouldNotBeNull();
resp.Error!.Code.ShouldBe(404);
}
// Go: TestJetStreamRequestAPI — multiple API calls
[Fact]
public async Task Multiple_api_calls_in_sequence()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MULTI", "multi.>");
// INFO
var info = await fx.RequestLocalAsync("$JS.API.INFO", "{}");
info.AccountInfo.ShouldNotBeNull();
// STREAM.NAMES
var names = await fx.RequestLocalAsync("$JS.API.STREAM.NAMES", "{}");
names.StreamNames.ShouldNotBeNull();
// STREAM.INFO
var sInfo = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.MULTI", "{}");
sInfo.StreamInfo.ShouldNotBeNull();
}
// Go: TestJetStreamDisabledLimitsEnforcementJWT server/jetstream_test.go
[Fact]
public async Task Jwt_limited_account_enforces_max_streams()
{
await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 1);
var s1 = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S1", """{"subjects":["s1.>"]}""");
s1.Error.ShouldBeNull();
var s2 = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S2", """{"subjects":["s2.>"]}""");
s2.Error.ShouldNotBeNull();
s2.Error!.Code.ShouldBe(10027);
}
// Go: TestJetStreamDisabledLimitsEnforcementJWT — delete frees slot
[Fact]
public async Task Jwt_limited_account_delete_frees_slot()
{
await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 1);
_ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S1", """{"subjects":["s1.>"]}""");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.DELETE.S1", "{}");
var s2 = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S2", """{"subjects":["s2.>"]}""");
s2.Error.ShouldBeNull();
}
// Go: TestJetStreamSystemLimits server/jetstream_test.go:4636
[Fact]
public async Task Account_info_updates_after_consumer_creation()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("AI", "ai.>");
var before = await fx.RequestLocalAsync("$JS.API.INFO", "{}");
before.AccountInfo!.Consumers.ShouldBe(0);
_ = await fx.CreateConsumerAsync("AI", "C1", "ai.>");
var after = await fx.RequestLocalAsync("$JS.API.INFO", "{}");
after.AccountInfo!.Consumers.ShouldBe(1);
}
// Go: TestJetStreamSystemLimits — account info updates after stream deletion
[Fact]
public async Task Account_info_updates_after_stream_deletion()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("AID", "aid.>");
var before = await fx.RequestLocalAsync("$JS.API.INFO", "{}");
before.AccountInfo!.Streams.ShouldBe(1);
_ = await fx.RequestLocalAsync("$JS.API.STREAM.DELETE.AID", "{}");
var after = await fx.RequestLocalAsync("$JS.API.INFO", "{}");
after.AccountInfo!.Streams.ShouldBe(0);
}
// Go: TestJetStreamConsumerList — consumer names scoped to stream
[Fact]
public async Task Consumer_names_for_non_existent_stream_returns_empty()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("X", "x.>");
var names = await fx.RequestLocalAsync("$JS.API.CONSUMER.NAMES.NOPE", "{}");
names.ConsumerNames.ShouldNotBeNull();
names.ConsumerNames!.Count.ShouldBe(0);
}
// Go: TestJetStreamMetaLeaderStepdown
[Fact]
public void Meta_leader_stepdown_with_meta_group_returns_success()
{
var metaGroup = new JetStreamMetaGroup(3);
var router = new JetStreamApiRouter(new StreamManager(), new ConsumerManager(), metaGroup);
var resp = router.Route("$JS.API.META.LEADER.STEPDOWN", "{}"u8);
resp.Success.ShouldBeTrue();
}
// Go: TestJetStreamMetaLeaderStepdown — without meta group
[Fact]
public void Meta_leader_stepdown_without_meta_group_returns_not_found()
{
var router = new JetStreamApiRouter();
var resp = router.Route("$JS.API.META.LEADER.STEPDOWN", "{}"u8);
resp.Error.ShouldNotBeNull();
resp.Error!.Code.ShouldBe(404);
}
// Go: TestJetStreamStreamLeaderStepdown — non-existent stream
[Fact]
public async Task Stream_leader_stepdown_non_existent_still_succeeds()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("LS", "ls.>");
// Stepdown for non-existent stream doesn't error (no-op)
var resp = await fx.RequestLocalAsync("$JS.API.STREAM.LEADER.STEPDOWN.NOPE", "{}");
resp.Success.ShouldBeTrue();
}
// Go: TestJetStreamConsumerNext — via API router
[Fact]
public async Task Consumer_next_via_api_returns_messages()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NEXT", "next.>");
_ = await fx.CreateConsumerAsync("NEXT", "C1", "next.>");
_ = await fx.PublishAndGetAckAsync("next.x", "data1");
_ = await fx.PublishAndGetAckAsync("next.x", "data2");
var resp = await fx.RequestLocalAsync(
"$JS.API.CONSUMER.MSG.NEXT.NEXT.C1",
"""{"batch":2}""");
resp.PullBatch.ShouldNotBeNull();
resp.PullBatch!.Messages.Count.ShouldBe(2);
}
// Go: TestJetStreamConsumerNext — empty
[Fact]
public async Task Consumer_next_with_no_messages_returns_empty()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NE", "ne.>");
_ = await fx.CreateConsumerAsync("NE", "C1", "ne.>");
var resp = await fx.RequestLocalAsync(
"$JS.API.CONSUMER.MSG.NEXT.NE.C1",
"""{"batch":1}""");
resp.PullBatch.ShouldNotBeNull();
resp.PullBatch!.Messages.Count.ShouldBe(0);
}
// Go: TestJetStreamStorageSelection
[Fact]
public async Task Storage_selection_file()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "FILE",
Subjects = ["file.>"],
Storage = StorageType.File,
});
var backend = await fx.GetStreamBackendTypeAsync("FILE");
backend.ShouldBe("file");
}
// Go: TestJetStreamStorageSelection — memory
[Fact]
public async Task Storage_selection_memory()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "MEM",
Subjects = ["mem.>"],
Storage = StorageType.Memory,
});
var backend = await fx.GetStreamBackendTypeAsync("MEM");
backend.ShouldBe("memory");
}
// Go: TestJetStreamStorageSelection — non-existent
[Fact]
public async Task Storage_backend_type_for_missing_stream()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("X", "x.>");
var backend = await fx.GetStreamBackendTypeAsync("NOPE");
backend.ShouldBe("missing");
}
// Go: TestJetStreamConsumerNames — for specific stream
[Fact]
public async Task Consumer_names_only_include_target_stream()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("S1", "s1.>");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S2", """{"subjects":["s2.>"]}""");
_ = await fx.CreateConsumerAsync("S1", "C1", "s1.>");
_ = await fx.CreateConsumerAsync("S2", "C2", "s2.>");
var names = await fx.RequestLocalAsync("$JS.API.CONSUMER.NAMES.S1", "{}");
names.ConsumerNames!.Count.ShouldBe(1);
names.ConsumerNames.ShouldContain("C1");
names.ConsumerNames.ShouldNotContain("C2");
}
// Go: TestJetStreamConsumerDelete — delete decrements count
[Fact]
public async Task Delete_consumer_decrements_account_info_count()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DCC", "dcc.>");
_ = await fx.CreateConsumerAsync("DCC", "C1", "dcc.>");
_ = await fx.CreateConsumerAsync("DCC", "C2", "dcc.>");
var before = await fx.RequestLocalAsync("$JS.API.INFO", "{}");
before.AccountInfo!.Consumers.ShouldBe(2);
_ = await fx.RequestLocalAsync("$JS.API.CONSUMER.DELETE.DCC.C1", "{}");
var after = await fx.RequestLocalAsync("$JS.API.INFO", "{}");
after.AccountInfo!.Consumers.ShouldBe(1);
}
// Go: TestJetStreamAccountPurge — empty account name fails
[Fact]
public void Account_purge_without_name_returns_not_found()
{
var router = new JetStreamApiRouter();
var resp = router.Route("$JS.API.ACCOUNT.PURGE.", "{}"u8);
resp.Error.ShouldNotBeNull();
}
// Go: TestJetStreamAccountStreamMove — empty stream name fails
[Fact]
public void Account_stream_move_without_name_returns_not_found()
{
var router = new JetStreamApiRouter();
var resp = router.Route("$JS.API.ACCOUNT.STREAM.MOVE.", "{}"u8);
resp.Error.ShouldNotBeNull();
}
// Go: TestJetStreamStreamLeaderStepdown — empty stream name fails
[Fact]
public void Stream_leader_stepdown_without_name_returns_not_found()
{
var router = new JetStreamApiRouter();
var resp = router.Route("$JS.API.STREAM.LEADER.STEPDOWN.", "{}"u8);
resp.Error.ShouldNotBeNull();
}
// Go: TestJetStreamStreamPeerRemove — empty stream name fails
[Fact]
public void Stream_peer_remove_without_name_returns_not_found()
{
var router = new JetStreamApiRouter();
var resp = router.Route("$JS.API.STREAM.PEER.REMOVE.", "{}"u8);
resp.Error.ShouldNotBeNull();
}
// Go: TestJetStreamConsumerLeaderStepdown — malformed subject
[Fact]
public void Consumer_leader_stepdown_with_single_token_returns_not_found()
{
var router = new JetStreamApiRouter();
var resp = router.Route("$JS.API.CONSUMER.LEADER.STEPDOWN.ONLYONE", "{}"u8);
resp.Error.ShouldNotBeNull();
}
// Go: TestJetStreamConsumerReset — non-existent consumer
[Fact]
public async Task Consumer_reset_non_existent_returns_not_found()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("RNE", "rne.>");
var resp = await fx.RequestLocalAsync("$JS.API.CONSUMER.RESET.RNE.NOPE", "{}");
resp.Success.ShouldBeFalse();
}
// Go: TestJetStreamConsumerUnpin — non-existent consumer
[Fact]
public async Task Consumer_unpin_non_existent_returns_not_found()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("UNE", "une.>");
var resp = await fx.RequestLocalAsync("$JS.API.CONSUMER.UNPIN.UNE.NOPE", "{}");
resp.Success.ShouldBeFalse();
}
// Go: TestJetStreamLimits server/jetstream_test.go
[Fact]
public async Task Jwt_limited_account_allows_within_limit()
{
await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 3);
var s1 = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S1", """{"subjects":["s1.>"]}""");
s1.Error.ShouldBeNull();
var s2 = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S2", """{"subjects":["s2.>"]}""");
s2.Error.ShouldBeNull();
var s3 = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S3", """{"subjects":["s3.>"]}""");
s3.Error.ShouldBeNull();
// Fourth should fail
var s4 = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S4", """{"subjects":["s4.>"]}""");
s4.Error.ShouldNotBeNull();
}
// Go: TestJetStreamStreamMessageDeleteViaAPI
[Fact]
public async Task Message_delete_via_api_and_verify()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MDAPI", "mdapi.>");
_ = await fx.PublishAndGetAckAsync("mdapi.x", "msg1");
var ack2 = await fx.PublishAndGetAckAsync("mdapi.x", "msg2");
_ = await fx.PublishAndGetAckAsync("mdapi.x", "msg3");
var del = await fx.RequestLocalAsync(
"$JS.API.STREAM.MSG.DELETE.MDAPI",
$$"""{ "seq": {{ack2.Seq}} }""");
del.Success.ShouldBeTrue();
// Verify the deleted message is gone
var msg = await fx.RequestLocalAsync(
"$JS.API.STREAM.MSG.GET.MDAPI",
$$"""{ "seq": {{ack2.Seq}} }""");
msg.Error.ShouldNotBeNull();
// Other messages still exist
var state = await fx.GetStreamStateAsync("MDAPI");
state.Messages.ShouldBe(2UL);
}
// Go: TestJetStreamRequestAPI — direct get missing sequence
[Fact]
public async Task Direct_get_with_zero_sequence_returns_error()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DGZ", "dgz.>");
_ = await fx.PublishAndGetAckAsync("dgz.x", "data");
var resp = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.DGZ", """{"seq":0}""");
resp.Error.ShouldNotBeNull();
}
// Go: TestJetStreamRequestAPI — direct get non-existent stream
[Fact]
public void Direct_get_non_existent_stream_returns_error()
{
var router = new JetStreamApiRouter();
var resp = router.Route("$JS.API.DIRECT.GET.NOPE", """{"seq":1}"""u8);
resp.Error.ShouldNotBeNull();
}
// Go: TestJetStreamConsumerNext — batch default
[Fact]
public async Task Consumer_next_with_no_batch_defaults_to_one()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("NBAT", "nbat.>");
_ = await fx.CreateConsumerAsync("NBAT", "C1", "nbat.>");
_ = await fx.PublishAndGetAckAsync("nbat.x", "data1");
_ = await fx.PublishAndGetAckAsync("nbat.x", "data2");
var resp = await fx.RequestLocalAsync(
"$JS.API.CONSUMER.MSG.NEXT.NBAT.C1", "{}");
resp.PullBatch!.Messages.Count.ShouldBe(1);
}
}