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,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);
}
}