using NATS.Server.TestUtilities; // 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.JetStream.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(); } }