- git mv JetStreamApiFixture, JetStreamClusterFixture, LeafFixture, Parity utilities, and TestData from NATS.Server.Tests to NATS.Server.TestUtilities - Update namespaces to NATS.Server.TestUtilities (and .Parity sub-ns) - Make fixture classes public for cross-project access - Add PollHelper to replace Task.Delay polling with SemaphoreSlim waits - Refactor all fixture polling loops to use PollHelper - Add 'using NATS.Server.TestUtilities;' to ~75 consuming test files - Rename local fixture duplicates (MetaGroupTestFixture, LeafProtocolTestFixture) to avoid shadowing shared fixtures - Remove TestData entry from NATS.Server.Tests.csproj (moved to TestUtilities)
310 lines
12 KiB
C#
310 lines
12 KiB
C#
// 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;
|
|
using NATS.Server.TestUtilities;
|
|
|
|
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);
|
|
}
|
|
}
|