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,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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user