Files
natsdotnet/tests/NATS.Server.Tests/JetStream/Api/ApiEndpointParityTests.cs
Joseph Doherty 61b1a00800 feat: phase C jetstream depth test parity — 34 new tests across 7 subsystems
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.
2026-02-23 19:55:31 -05:00

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