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,309 @@
|
||||
// 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.JetStream.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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user