Stream lifecycle, publish/ack, consumer delivery, retention policy, API endpoints, cluster formation, and leader failover tests ported from Go nats-server reference. 1006 total tests passing.
123 lines
5.4 KiB
C#
123 lines
5.4 KiB
C#
// Go reference: golang/nats-server/server/jetstream.go — $JS.API.* subject dispatch
|
|
// Covers create/info/update/delete for streams, create/info/list/delete for consumers,
|
|
// direct-get access, account info, and 404 routing for unknown subjects.
|
|
|
|
namespace NATS.Server.Tests;
|
|
|
|
public class ApiEndpointParityTests
|
|
{
|
|
// Go ref: jsStreamCreateT handler — stream create persists config and info round-trips correctly.
|
|
[Fact]
|
|
public async Task Stream_create_info_update_delete_lifecycle()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("EVENTS", "events.*");
|
|
|
|
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.EVENTS", "{}");
|
|
info.Error.ShouldBeNull();
|
|
info.StreamInfo.ShouldNotBeNull();
|
|
info.StreamInfo!.Config.Name.ShouldBe("EVENTS");
|
|
info.StreamInfo.Config.Subjects.ShouldContain("events.*");
|
|
|
|
var update = await fx.RequestLocalAsync(
|
|
"$JS.API.STREAM.UPDATE.EVENTS",
|
|
"{\"name\":\"EVENTS\",\"subjects\":[\"events.*\"],\"max_msgs\":100}");
|
|
update.Error.ShouldBeNull();
|
|
update.StreamInfo.ShouldNotBeNull();
|
|
update.StreamInfo!.Config.MaxMsgs.ShouldBe(100);
|
|
|
|
var delete = await fx.RequestLocalAsync("$JS.API.STREAM.DELETE.EVENTS", "{}");
|
|
delete.Error.ShouldBeNull();
|
|
delete.Success.ShouldBeTrue();
|
|
|
|
var infoAfterDelete = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.EVENTS", "{}");
|
|
infoAfterDelete.Error.ShouldNotBeNull();
|
|
infoAfterDelete.Error!.Code.ShouldBe(404);
|
|
}
|
|
|
|
// Go ref: jsConsumerCreateT / jsConsumerInfoT handlers — consumer create then info returns config.
|
|
[Fact]
|
|
public async Task Consumer_create_info_list_delete_lifecycle()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");
|
|
|
|
var create = await fx.CreateConsumerAsync("ORDERS", "MON", "orders.created");
|
|
create.Error.ShouldBeNull();
|
|
create.ConsumerInfo.ShouldNotBeNull();
|
|
create.ConsumerInfo!.Config.DurableName.ShouldBe("MON");
|
|
|
|
var info = await fx.RequestLocalAsync("$JS.API.CONSUMER.INFO.ORDERS.MON", "{}");
|
|
info.Error.ShouldBeNull();
|
|
info.ConsumerInfo.ShouldNotBeNull();
|
|
info.ConsumerInfo!.Config.FilterSubject.ShouldBe("orders.created");
|
|
|
|
var names = await fx.RequestLocalAsync("$JS.API.CONSUMER.NAMES.ORDERS", "{}");
|
|
names.Error.ShouldBeNull();
|
|
names.ConsumerNames.ShouldNotBeNull();
|
|
names.ConsumerNames.ShouldContain("MON");
|
|
|
|
var list = await fx.RequestLocalAsync("$JS.API.CONSUMER.LIST.ORDERS", "{}");
|
|
list.Error.ShouldBeNull();
|
|
list.ConsumerNames.ShouldNotBeNull();
|
|
list.ConsumerNames.ShouldContain("MON");
|
|
|
|
var del = await fx.RequestLocalAsync("$JS.API.CONSUMER.DELETE.ORDERS.MON", "{}");
|
|
del.Error.ShouldBeNull();
|
|
del.Success.ShouldBeTrue();
|
|
|
|
var infoAfterDelete = await fx.RequestLocalAsync("$JS.API.CONSUMER.INFO.ORDERS.MON", "{}");
|
|
infoAfterDelete.Error.ShouldNotBeNull();
|
|
infoAfterDelete.Error!.Code.ShouldBe(404);
|
|
}
|
|
|
|
// Go ref: jsDirectMsgGetT handler — direct get returns message payload at correct sequence.
|
|
[Fact]
|
|
public async Task Direct_get_returns_message_at_sequence()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("LOGS", "logs.*");
|
|
var ack = await fx.PublishAndGetAckAsync("logs.app", "hello-direct");
|
|
|
|
var direct = await fx.RequestLocalAsync("$JS.API.DIRECT.GET.LOGS", $"{{\"seq\":{ack.Seq}}}");
|
|
direct.Error.ShouldBeNull();
|
|
direct.DirectMessage.ShouldNotBeNull();
|
|
direct.DirectMessage!.Sequence.ShouldBe(ack.Seq);
|
|
direct.DirectMessage.Payload.ShouldBe("hello-direct");
|
|
}
|
|
|
|
// Go ref: jsStreamNamesT / $JS.API.INFO handler — names list reflects created streams,
|
|
// account info reflects total stream and consumer counts.
|
|
[Fact]
|
|
public async Task Stream_names_and_account_info_reflect_state()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ALPHA", "alpha.*");
|
|
_ = await fx.CreateStreamAsync("BETA", ["beta.*"]);
|
|
_ = await fx.CreateConsumerAsync("ALPHA", "C1", "alpha.>");
|
|
_ = await fx.CreateConsumerAsync("BETA", "C2", "beta.>");
|
|
|
|
var names = await fx.RequestLocalAsync("$JS.API.STREAM.NAMES", "{}");
|
|
names.Error.ShouldBeNull();
|
|
names.StreamNames.ShouldNotBeNull();
|
|
names.StreamNames.ShouldContain("ALPHA");
|
|
names.StreamNames.ShouldContain("BETA");
|
|
|
|
var accountInfo = await fx.RequestLocalAsync("$JS.API.INFO", "{}");
|
|
accountInfo.Error.ShouldBeNull();
|
|
accountInfo.AccountInfo.ShouldNotBeNull();
|
|
accountInfo.AccountInfo!.Streams.ShouldBe(2);
|
|
accountInfo.AccountInfo.Consumers.ShouldBe(2);
|
|
}
|
|
|
|
// Go ref: JetStreamApiRouter dispatch — subjects not matching any handler return 404 error shape.
|
|
[Fact]
|
|
public async Task Unknown_api_subject_returns_404_error_response()
|
|
{
|
|
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");
|
|
|
|
var response = await fx.RequestLocalAsync("$JS.API.STREAM.FROBNICATE.ORDERS", "{}");
|
|
response.Error.ShouldNotBeNull();
|
|
response.Error!.Code.ShouldBe(404);
|
|
response.StreamInfo.ShouldBeNull();
|
|
response.ConsumerInfo.ShouldBeNull();
|
|
response.Success.ShouldBeFalse();
|
|
}
|
|
}
|