Files
natsdotnet/tests/NATS.Server.JetStream.Tests/JetStream/Api/ApiEndpointParityTests.cs
Joseph Doherty 78b4bc2486 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.
2026-03-12 15:58:10 -04:00

125 lines
5.4 KiB
C#

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