// Ported from golang/nats-server/server/jetstream_test.go // Account limits: max streams per account, max consumers per stream, // JWT-based account limits, account info reporting, stream/consumer count limits. using NATS.Server.Auth; using NATS.Server.JetStream; using NATS.Server.JetStream.Api; using NATS.Server.JetStream.Models; namespace NATS.Server.Tests.JetStream; public class JetStreamAccountLimitTests { // Go: TestJetStreamSystemLimits server/jetstream_test.go:4837 // Account with max streams = 1 cannot create a second stream. [Fact] public async Task Account_max_streams_one_prevents_second_stream_creation() { await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 1); var first = await fx.RequestLocalAsync( "$JS.API.STREAM.CREATE.S1", """{"name":"S1","subjects":["s1.>"]}"""); first.Error.ShouldBeNull(); first.StreamInfo.ShouldNotBeNull(); var second = await fx.RequestLocalAsync( "$JS.API.STREAM.CREATE.S2", """{"name":"S2","subjects":["s2.>"]}"""); second.Error.ShouldNotBeNull(); second.Error!.Code.ShouldBe(10027); } // Go: TestJetStreamSystemLimits — account with max = 3 creates 3 then fails [Fact] public async Task Account_max_streams_three_rejects_fourth_stream() { await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 3); for (var i = 1; i <= 3; i++) { var ok = await fx.RequestLocalAsync( $"$JS.API.STREAM.CREATE.S{i}", $$$"""{"name":"S{{{i}}}","subjects":["s{{{i}}}.>"]}"""); ok.Error.ShouldBeNull(); } var rejected = await fx.RequestLocalAsync( "$JS.API.STREAM.CREATE.S4", """{"name":"S4","subjects":["s4.>"]}"""); rejected.Error.ShouldNotBeNull(); rejected.Error!.Code.ShouldBe(10027); } // Go: TestJetStreamSystemLimits — after deleting a stream the limit slot is freed [Fact] public async Task Account_max_streams_slot_freed_after_delete() { await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 2); var s1 = await fx.RequestLocalAsync( "$JS.API.STREAM.CREATE.DEL1", """{"name":"DEL1","subjects":["del1.>"]}"""); s1.Error.ShouldBeNull(); var s2 = await fx.RequestLocalAsync( "$JS.API.STREAM.CREATE.DEL2", """{"name":"DEL2","subjects":["del2.>"]}"""); s2.Error.ShouldBeNull(); // Delete S1 var del = await fx.RequestLocalAsync("$JS.API.STREAM.DELETE.DEL1", "{}"); del.Success.ShouldBeTrue(); // Now S3 should succeed var s3 = await fx.RequestLocalAsync( "$JS.API.STREAM.CREATE.DEL3", """{"name":"DEL3","subjects":["del3.>"]}"""); s3.Error.ShouldBeNull(); } // Go: TestJetStreamSystemLimits — account with no limit allows many streams [Fact] public async Task Account_with_zero_max_streams_allows_unlimited_streams() { await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 0); for (var i = 1; i <= 10; i++) { var ok = await fx.RequestLocalAsync( $"$JS.API.STREAM.CREATE.UNLIM{i}", $$$"""{"name":"UNLIM{{{i}}}","subjects":["unlim{{{i}}}.>"]}"""); ok.Error.ShouldBeNull(); } } // Go: TestJetStreamMaxConsumers server/jetstream_test.go:553 // Stream max_consumers configuration is persisted in stream config and returned in INFO. // Note: The .NET ConsumerManager does not yet enforce per-stream MaxConsumers at the // API layer — the config value is stored and reportable but not enforced during consumer creation. [Fact] public async Task Stream_max_consumers_is_stored_and_returned_in_info() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "MAXCONSUMERS", Subjects = ["maxconsumers.>"], MaxConsumers = 2, }); // Config is preserved var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.MAXCONSUMERS", "{}"); info.Error.ShouldBeNull(); info.StreamInfo!.Config.MaxConsumers.ShouldBe(2); // Consumers can be created (enforcement is not at the API layer) var c1 = await fx.CreateConsumerAsync("MAXCONSUMERS", "C1", "maxconsumers.>"); c1.Error.ShouldBeNull(); var c2 = await fx.CreateConsumerAsync("MAXCONSUMERS", "C2", "maxconsumers.a"); c2.Error.ShouldBeNull(); } // Go: TestJetStreamMaxConsumers — creating same consumer name twice is idempotent [Fact] public async Task Create_same_consumer_twice_is_idempotent_and_not_counted_twice() { await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig { Name = "IDMCONS", Subjects = ["idmcons.>"], MaxConsumers = 2, }); var c1a = await fx.CreateConsumerAsync("IDMCONS", "C1", "idmcons.>"); c1a.Error.ShouldBeNull(); // Same name — idempotent, should not count as second consumer var c1b = await fx.CreateConsumerAsync("IDMCONS", "C1", "idmcons.>"); c1b.Error.ShouldBeNull(); // Second unique name should succeed var c2 = await fx.CreateConsumerAsync("IDMCONS", "C2", "idmcons.a"); c2.Error.ShouldBeNull(); } // Go: TestJetStreamRequestAPI server/jetstream_test.go:5995 // Account info returns correct stream and consumer counts. [Fact] public async Task Account_info_reflects_created_streams_and_consumers() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("A1", "a1.>"); _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.A2", """{"name":"A2","subjects":["a2.>"]}"""); _ = await fx.CreateConsumerAsync("A1", "CON1", "a1.>"); _ = await fx.CreateConsumerAsync("A2", "CON2", "a2.>"); _ = await fx.CreateConsumerAsync("A2", "CON3", "a2.x"); var info = await fx.RequestLocalAsync("$JS.API.INFO", "{}"); info.Error.ShouldBeNull(); info.AccountInfo.ShouldNotBeNull(); info.AccountInfo!.Streams.ShouldBe(2); info.AccountInfo.Consumers.ShouldBe(3); } // Go: TestJetStreamRequestAPI — empty account info [Fact] public void Account_info_for_empty_account_returns_zero_counts() { var router = new JetStreamApiRouter(new StreamManager(), new ConsumerManager()); var resp = router.Route("$JS.API.INFO", "{}"u8); resp.Error.ShouldBeNull(); resp.AccountInfo!.Streams.ShouldBe(0); resp.AccountInfo.Consumers.ShouldBe(0); } // Go: TestJetStreamSystemLimits — Account.TryReserveStream enforces MaxJetStreamStreams [Fact] public void Account_reserve_stream_enforces_max_jet_stream_streams() { var account = new Account("TEST") { MaxJetStreamStreams = 2, }; account.TryReserveStream().ShouldBeTrue(); account.TryReserveStream().ShouldBeTrue(); account.TryReserveStream().ShouldBeFalse(); // exceeded } // Go: TestJetStreamSystemLimits — Account.ReleaseStream frees a slot [Fact] public void Account_release_stream_frees_slot_for_reservation() { var account = new Account("FREETEST") { MaxJetStreamStreams = 1, }; account.TryReserveStream().ShouldBeTrue(); account.TryReserveStream().ShouldBeFalse(); // full account.ReleaseStream(); account.TryReserveStream().ShouldBeTrue(); // slot freed } // Go: TestJetStreamSystemLimits — zero max streams means unlimited [Fact] public void Account_with_zero_max_streams_allows_unlimited_reservations() { var account = new Account("UNLIMITED") { MaxJetStreamStreams = 0, // unlimited }; for (var i = 0; i < 100; i++) account.TryReserveStream().ShouldBeTrue(); } // Go: TestJetStreamSystemLimits — JetStreamStreamCount tracks correctly [Fact] public void Account_stream_count_tracks_reserve_and_release() { var account = new Account("COUNTTEST") { MaxJetStreamStreams = 5, }; account.JetStreamStreamCount.ShouldBe(0); account.TryReserveStream(); account.JetStreamStreamCount.ShouldBe(1); account.TryReserveStream(); account.JetStreamStreamCount.ShouldBe(2); account.ReleaseStream(); account.JetStreamStreamCount.ShouldBe(1); } // Go: TestJetStreamRequestAPI — stream list includes all streams [Fact] public async Task Stream_names_includes_all_created_streams() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("LISTA", "lista.>"); _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.LISTB", """{"name":"LISTB","subjects":["listb.>"]}"""); _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.LISTC", """{"name":"LISTC","subjects":["listc.>"]}"""); var names = await fx.RequestLocalAsync("$JS.API.STREAM.NAMES", "{}"); names.StreamNames.ShouldNotBeNull(); names.StreamNames!.Count.ShouldBe(3); names.StreamNames.ShouldContain("LISTA"); names.StreamNames.ShouldContain("LISTB"); names.StreamNames.ShouldContain("LISTC"); } // Go: TestJetStreamRequestAPI — stream names sorted alphabetically [Fact] public async Task Stream_names_are_returned_sorted() { await using var fx = new JetStreamApiFixture(); _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.ZZZ", """{"name":"ZZZ","subjects":["zzz.>"]}"""); _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.AAA", """{"name":"AAA","subjects":["aaa.>"]}"""); _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.MMM", """{"name":"MMM","subjects":["mmm.>"]}"""); var names = await fx.RequestLocalAsync("$JS.API.STREAM.NAMES", "{}"); names.StreamNames.ShouldNotBeNull(); names.StreamNames!.ShouldBe(names.StreamNames.OrderBy(n => n, StringComparer.Ordinal).ToList()); } // Go: TestJetStreamMaxConsumers — consumer names list reflects created consumers [Fact] public async Task Consumer_names_list_reflects_created_consumers() { await using var fx = await JetStreamApiFixture.StartWithStreamAsync("CONLIST", "conlist.>"); _ = await fx.CreateConsumerAsync("CONLIST", "CON1", "conlist.a"); _ = await fx.CreateConsumerAsync("CONLIST", "CON2", "conlist.b"); _ = await fx.CreateConsumerAsync("CONLIST", "CON3", "conlist.c"); var names = await fx.RequestLocalAsync("$JS.API.CONSUMER.NAMES.CONLIST", "{}"); names.ConsumerNames.ShouldNotBeNull(); names.ConsumerNames!.Count.ShouldBe(3); names.ConsumerNames.ShouldContain("CON1"); names.ConsumerNames.ShouldContain("CON2"); names.ConsumerNames.ShouldContain("CON3"); } // Go: TestJetStreamSystemLimits — account limit error has correct code [Fact] public async Task Max_streams_error_uses_code_10027() { await using var fx = await JetStreamApiFixture.StartJwtLimitedAccountAsync(maxStreams: 1); _ = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.FIRST", """{"name":"FIRST","subjects":["first.>"]}"""); var rejected = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.SECOND", """{"name":"SECOND","subjects":["second.>"]}"""); rejected.Error.ShouldNotBeNull(); rejected.Error!.Code.ShouldBe(10027); rejected.Error.Description.ShouldNotBeNullOrEmpty(); } // Go: TestJetStreamEnableAndDisableAccount server/jetstream_test.go:128 // A new account starts with zero JetStream stream count. [Fact] public void New_account_has_zero_jet_stream_stream_count() { var account = new Account("NEWACCT"); account.JetStreamStreamCount.ShouldBe(0); } }