feat: Wave 6 batch 2 — accounts/auth, gateways, routes, JetStream API, JetStream cluster tests

Add comprehensive Go-parity test coverage across 5 subsystems:
- Accounts/Auth: isolation, import/export, auth mechanisms, permissions (82 tests)
- Gateways: connection, forwarding, interest mode, config (106 tests)
- Routes: connection, subscription, forwarding, config validation (78 tests)
- JetStream API: stream/consumer CRUD, pub/sub, features, admin (234 tests)
- JetStream Cluster: streams, consumers, failover, meta (108 tests)

Total: ~608 new test annotations across 22 files (+13,844 lines)
All tests pass individually; suite total: 2,283 passing, 3 skipped
This commit is contained in:
Joseph Doherty
2026-02-23 22:35:06 -05:00
parent 9554d53bf5
commit f1353868af
23 changed files with 13844 additions and 74 deletions

View File

@@ -0,0 +1,710 @@
// Ported from golang/nats-server/server/jetstream_test.go
// Stream CRUD operations: create, update, delete, purge, info, validation
using NATS.Server.JetStream;
using NATS.Server.JetStream.Api;
using NATS.Server.JetStream.Models;
using NATS.Server.JetStream.Validation;
namespace NATS.Server.Tests.JetStream;
public class JetStreamStreamCrudTests
{
// Go: TestJetStreamAddStream server/jetstream_test.go:178
[Fact]
public async Task Create_stream_returns_config_and_empty_state()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.ORDERS", "{}");
info.Error.ShouldBeNull();
info.StreamInfo.ShouldNotBeNull();
info.StreamInfo!.Config.Name.ShouldBe("ORDERS");
info.StreamInfo.Config.Subjects.ShouldContain("orders.*");
info.StreamInfo.State.Messages.ShouldBe(0UL);
}
// Go: TestJetStreamAddStreamDiscardNew server/jetstream_test.go:122
// Verifies discard new policy with max_bytes rejects new messages when stream is full.
[Fact]
public async Task Create_stream_with_discard_new_policy()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "DN",
Subjects = ["dn.>"],
MaxBytes = 30,
Discard = DiscardPolicy.New,
});
var ack1 = await fx.PublishAndGetAckAsync("dn.one", "1");
ack1.ErrorCode.ShouldBeNull();
var ack2 = await fx.PublishAndGetAckAsync("dn.two", "2");
ack2.ErrorCode.ShouldBeNull();
// Oversized publish should be rejected due to discard new + max_bytes
var ack3 = await fx.PublishAndGetAckAsync("dn.three", "this-is-a-large-payload-that-exceeds-bytes");
ack3.ErrorCode.ShouldNotBeNull();
}
// Go: TestJetStreamAddStreamMaxMsgSize server/jetstream_test.go:484
[Fact]
public async Task Create_stream_with_max_msg_size_rejects_oversized()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "SIZED",
Subjects = ["sized.>"],
MaxMsgSize = 10,
});
var small = await fx.PublishAndGetAckAsync("sized.ok", "tiny");
small.ErrorCode.ShouldBeNull();
var big = await fx.PublishAndGetAckAsync("sized.big", "this-is-definitely-larger-than-ten-bytes");
big.ErrorCode.ShouldNotBeNull();
}
// Go: TestJetStreamAddStreamCanonicalNames server/jetstream_test.go:537
[Fact]
public async Task Create_stream_name_is_preserved_in_info()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MyStream", "my.>");
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.MyStream", "{}");
info.Error.ShouldBeNull();
info.StreamInfo!.Config.Name.ShouldBe("MyStream");
}
// Go: TestJetStreamAddStreamSameConfigOK server/jetstream_test.go:701
[Fact]
public async Task Create_stream_with_same_config_is_idempotent()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");
var second = await fx.RequestLocalAsync(
"$JS.API.STREAM.CREATE.ORDERS",
"""{"name":"ORDERS","subjects":["orders.*"]}""");
second.Error.ShouldBeNull();
second.StreamInfo.ShouldNotBeNull();
second.StreamInfo!.Config.Name.ShouldBe("ORDERS");
}
// Go: TestJetStreamUpdateStream server/jetstream_test.go:6409
[Fact]
public async Task Update_stream_changes_subjects_and_limits()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*");
_ = await fx.PublishAndGetAckAsync("orders.x", "1");
var update = await fx.RequestLocalAsync(
"$JS.API.STREAM.UPDATE.ORDERS",
"""{"name":"ORDERS","subjects":["orders.v2.*"],"max_msgs":50}""");
update.Error.ShouldBeNull();
update.StreamInfo!.Config.Subjects.ShouldContain("orders.v2.*");
update.StreamInfo.Config.MaxMsgs.ShouldBe(50);
}
// Go: TestJetStreamStreamPurge server/jetstream_test.go:4182
[Fact]
public async Task Purge_stream_removes_all_messages()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("P", "p.*");
for (var i = 0; i < 5; i++)
_ = await fx.PublishAndGetAckAsync("p.msg", $"payload-{i}");
var before = await fx.GetStreamStateAsync("P");
before.Messages.ShouldBe(5UL);
var purge = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.P", "{}");
purge.Success.ShouldBeTrue();
var after = await fx.GetStreamStateAsync("P");
after.Messages.ShouldBe(0UL);
}
// Go: TestJetStreamDeleteMsg server/jetstream_test.go:6464
[Fact]
public async Task Delete_individual_message_by_sequence()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DEL", "del.>");
var ack1 = await fx.PublishAndGetAckAsync("del.a", "1");
_ = await fx.PublishAndGetAckAsync("del.b", "2");
var del = await fx.RequestLocalAsync(
"$JS.API.STREAM.MSG.DELETE.DEL",
$$"""{ "seq": {{ack1.Seq}} }""");
del.Success.ShouldBeTrue();
var state = await fx.GetStreamStateAsync("DEL");
state.Messages.ShouldBe(1UL);
}
// Go: TestJetStreamAddStream — delete removes stream
[Fact]
public async Task Delete_stream_makes_it_inaccessible()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("GONE", "gone.>");
_ = await fx.PublishAndGetAckAsync("gone.x", "data");
var del = await fx.RequestLocalAsync("$JS.API.STREAM.DELETE.GONE", "{}");
del.Success.ShouldBeTrue();
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.GONE", "{}");
info.Error.ShouldNotBeNull();
}
// Go: TestJetStreamStreamPurge — publish after purge works
[Fact]
public async Task Publish_after_purge_adds_new_message()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PP", "pp.>");
_ = await fx.PublishAndGetAckAsync("pp.x", "before");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.PP", "{}");
var ack = await fx.PublishAndGetAckAsync("pp.x", "after");
ack.ErrorCode.ShouldBeNull();
var state = await fx.GetStreamStateAsync("PP");
state.Messages.ShouldBe(1UL);
}
// Go: TestJetStreamBasicNilConfig server/jetstream_test.go:56
[Fact]
public void Stream_config_requires_name()
{
var sm = new StreamManager();
var resp = sm.CreateOrUpdate(new StreamConfig { Name = "" });
resp.Error.ShouldNotBeNull();
resp.Error!.Code.ShouldBe(400);
}
// Go: TestJetStreamAddStreamBadSubjects server/jetstream_test.go:587
[Fact]
public void Validation_rejects_empty_name_and_subjects()
{
var config = new StreamConfig { Name = "", Subjects = [] };
var result = JetStreamConfigValidator.Validate(config);
result.IsValid.ShouldBeFalse();
}
// Go: TestJetStreamAddStreamBadSubjects — valid name required
[Fact]
public void Validation_accepts_valid_stream_config()
{
var config = new StreamConfig { Name = "OK", Subjects = ["ok.>"] };
var result = JetStreamConfigValidator.Validate(config);
result.IsValid.ShouldBeTrue();
}
// Go: TestJetStreamMaxConsumers server/jetstream_test.go:619
[Fact]
public void Validation_workqueue_requires_max_consumers()
{
var config = new StreamConfig
{
Name = "WQ",
Subjects = ["wq.>"],
Retention = RetentionPolicy.WorkQueue,
MaxConsumers = 0,
};
var result = JetStreamConfigValidator.Validate(config);
result.IsValid.ShouldBeFalse();
}
// Go: TestJetStreamInvalidConfigValues server/jetstream_test.go
[Fact]
public void Validation_rejects_negative_max_msg_size()
{
var config = new StreamConfig
{
Name = "NEG",
Subjects = ["neg.>"],
MaxMsgSize = -1,
};
var result = JetStreamConfigValidator.Validate(config);
result.IsValid.ShouldBeFalse();
}
// Go: TestJetStreamInvalidConfigValues
[Fact]
public void Validation_rejects_negative_max_msgs_per()
{
var config = new StreamConfig
{
Name = "NEG2",
Subjects = ["neg2.>"],
MaxMsgsPer = -1,
};
var result = JetStreamConfigValidator.Validate(config);
result.IsValid.ShouldBeFalse();
}
// Go: TestJetStreamInvalidConfigValues
[Fact]
public void Validation_rejects_negative_max_age_ms()
{
var config = new StreamConfig
{
Name = "NEG3",
Subjects = ["neg3.>"],
MaxAgeMs = -1,
};
var result = JetStreamConfigValidator.Validate(config);
result.IsValid.ShouldBeFalse();
}
// Go: TestJetStreamStreamPurge — sealed stream cannot be purged
[Fact]
public async Task Sealed_stream_rejects_purge()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "SEAL",
Subjects = ["seal.>"],
Sealed = true,
});
var purge = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.SEAL", "{}");
purge.Success.ShouldBeFalse();
}
// Go: TestJetStreamDeleteMsg — deny_delete prevents removal
[Fact]
public async Task Deny_delete_prevents_message_removal()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "NODELETE",
Subjects = ["nodelete.>"],
DenyDelete = true,
});
var ack = await fx.PublishAndGetAckAsync("nodelete.x", "data");
ack.ErrorCode.ShouldBeNull();
var del = await fx.RequestLocalAsync(
"$JS.API.STREAM.MSG.DELETE.NODELETE",
$$"""{ "seq": {{ack.Seq}} }""");
del.Success.ShouldBeFalse();
}
// Go: TestJetStreamDeleteMsg — deny_purge prevents purge
[Fact]
public async Task Deny_purge_prevents_stream_purge()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "NOPURGE",
Subjects = ["nopurge.>"],
DenyPurge = true,
});
_ = await fx.PublishAndGetAckAsync("nopurge.x", "data");
var purge = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.NOPURGE", "{}");
purge.Success.ShouldBeFalse();
}
// Go: TestJetStreamStreamStorageTrackingAndLimits server/jetstream_test.go:4931
[Fact]
public async Task Stream_with_max_msgs_limit_enforces_count()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("LIMITED", "limited.>", maxMsgs: 3);
for (var i = 0; i < 5; i++)
_ = await fx.PublishAndGetAckAsync("limited.x", $"msg-{i}");
var state = await fx.GetStreamStateAsync("LIMITED");
state.Messages.ShouldBeLessThanOrEqualTo(3UL);
}
// Go: TestJetStreamMaxBytesIgnored server/jetstream_test.go
[Fact]
public async Task Stream_with_max_bytes_discard_old_evicts_oldest()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "BYTES",
Subjects = ["bytes.>"],
MaxBytes = 100,
Discard = DiscardPolicy.Old,
});
for (var i = 0; i < 20; i++)
_ = await fx.PublishAndGetAckAsync("bytes.x", $"payload-{i:D10}");
var state = await fx.GetStreamStateAsync("BYTES");
((long)state.Bytes).ShouldBeLessThanOrEqualTo(100L);
}
// Go: TestJetStreamMaxMsgsPerSubject server/jetstream_test.go
[Fact]
public async Task Max_msgs_per_subject_enforces_limit()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "MPS",
Subjects = ["mps.>"],
MaxMsgsPer = 2,
});
_ = await fx.PublishAndGetAckAsync("mps.a", "1");
_ = await fx.PublishAndGetAckAsync("mps.a", "2");
_ = await fx.PublishAndGetAckAsync("mps.a", "3");
_ = await fx.PublishAndGetAckAsync("mps.b", "4");
var state = await fx.GetStreamStateAsync("MPS");
// mps.a should have 2 kept, mps.b has 1 = 3 total
state.Messages.ShouldBeLessThanOrEqualTo(3UL);
}
// Go: TestJetStreamStreamFileTrackingAndLimits server/jetstream_test.go:4982
[Fact]
public async Task Stream_with_file_storage_type()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "FSTORE",
Subjects = ["fstore.>"],
Storage = StorageType.File,
});
var ack = await fx.PublishAndGetAckAsync("fstore.x", "data");
ack.ErrorCode.ShouldBeNull();
var backendType = await fx.GetStreamBackendTypeAsync("FSTORE");
backendType.ShouldBe("file");
}
// Go: TestJetStreamStreamFileTrackingAndLimits — memory store
[Fact]
public async Task Stream_with_memory_storage_type()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "MSTORE",
Subjects = ["mstore.>"],
Storage = StorageType.Memory,
});
var ack = await fx.PublishAndGetAckAsync("mstore.x", "data");
ack.ErrorCode.ShouldBeNull();
var backendType = await fx.GetStreamBackendTypeAsync("MSTORE");
backendType.ShouldBe("memory");
}
// Go: TestJetStreamStreamLimitUpdate server/jetstream_test.go:4905
[Fact]
public async Task Update_stream_max_msgs_trims_existing_messages()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("UPD", "upd.>");
for (var i = 0; i < 10; i++)
_ = await fx.PublishAndGetAckAsync("upd.x", $"msg-{i}");
var before = await fx.GetStreamStateAsync("UPD");
before.Messages.ShouldBe(10UL);
// Update to max_msgs=3
var update = await fx.RequestLocalAsync(
"$JS.API.STREAM.UPDATE.UPD",
"""{"name":"UPD","subjects":["upd.>"],"max_msgs":3}""");
update.Error.ShouldBeNull();
// Publish one more to trigger enforcement
_ = await fx.PublishAndGetAckAsync("upd.x", "trigger");
var after = await fx.GetStreamStateAsync("UPD");
after.Messages.ShouldBeLessThanOrEqualTo(3UL);
}
// Go: TestJetStreamAllowDirectAfterUpdate server/jetstream_test.go
[Fact]
public async Task Allow_direct_can_be_set_via_update()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "DIR",
Subjects = ["dir.>"],
AllowDirect = false,
});
var update = await fx.RequestLocalAsync(
"$JS.API.STREAM.UPDATE.DIR",
"""{"name":"DIR","subjects":["dir.>"],"allow_direct":true}""");
update.Error.ShouldBeNull();
update.StreamInfo!.Config.AllowDirect.ShouldBeTrue();
}
// Go: TestJetStreamStreamConfigClone server/jetstream_test.go
[Fact]
public async Task Stream_config_is_independent_after_creation()
{
var config = new StreamConfig
{
Name = "CLONE",
Subjects = ["clone.>"],
MaxMsgs = 100,
};
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(config);
// Mutate the original config
config.MaxMsgs = 999;
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.CLONE", "{}");
info.StreamInfo!.Config.MaxMsgs.ShouldBe(100);
}
// Go: TestJetStreamStreamPurgeWithConsumer server/jetstream_test.go:4215
[Fact]
public async Task Purge_with_active_consumer_resets_delivery()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PC", "pc.>");
_ = await fx.CreateConsumerAsync("PC", "C1", "pc.>");
for (var i = 0; i < 5; i++)
_ = await fx.PublishAndGetAckAsync("pc.x", $"msg-{i}");
var purge = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.PC", "{}");
purge.Success.ShouldBeTrue();
var state = await fx.GetStreamStateAsync("PC");
state.Messages.ShouldBe(0UL);
}
// Go: TestJetStreamGetLastMsgBySubject server/jetstream_test.go
[Fact]
public async Task Get_message_by_sequence_returns_correct_data()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("GM", "gm.>");
var ack = await fx.PublishAndGetAckAsync("gm.first", "hello");
_ = await fx.PublishAndGetAckAsync("gm.second", "world");
var msg = await fx.RequestLocalAsync(
"$JS.API.STREAM.MSG.GET.GM",
$$"""{ "seq": {{ack.Seq}} }""");
msg.StreamMessage.ShouldNotBeNull();
msg.StreamMessage!.Payload.ShouldBe("hello");
msg.StreamMessage.Subject.ShouldBe("gm.first");
}
// Go: TestJetStreamStateTimestamps server/jetstream_test.go:758
[Fact]
public async Task Stream_state_tracks_first_and_last_sequence()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("TS", "ts.>");
_ = await fx.PublishAndGetAckAsync("ts.a", "1");
_ = await fx.PublishAndGetAckAsync("ts.b", "2");
_ = await fx.PublishAndGetAckAsync("ts.c", "3");
var state = await fx.GetStreamStateAsync("TS");
state.Messages.ShouldBe(3UL);
state.FirstSeq.ShouldBe(1UL);
state.LastSeq.ShouldBe(3UL);
}
// Go: TestJetStreamAddStreamDiscardNew — discard new + max bytes
[Fact]
public async Task Discard_new_with_max_bytes_rejects_when_full()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "DNB",
Subjects = ["dnb.>"],
MaxBytes = 50,
Discard = DiscardPolicy.New,
});
// Fill up
for (var i = 0; i < 10; i++)
{
_ = await fx.PublishAndGetAckAsync("dnb.x", $"msg-{i:D20}");
}
// Eventually one should be rejected
var state = await fx.GetStreamStateAsync("DNB");
((long)state.Bytes).ShouldBeLessThanOrEqualTo(50L + 50);
}
// Go: TestJetStreamStreamRetentionUpdatesConsumers server/jetstream_test.go
[Fact]
public async Task Stream_info_after_multiple_publishes()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("INF", "inf.>");
for (var i = 0; i < 10; i++)
_ = await fx.PublishAndGetAckAsync("inf.x", $"data-{i}");
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.INF", "{}");
info.Error.ShouldBeNull();
info.StreamInfo!.State.Messages.ShouldBe(10UL);
info.StreamInfo.State.FirstSeq.ShouldBe(1UL);
info.StreamInfo.State.LastSeq.ShouldBe(10UL);
}
// Go: TestJetStreamDeleteMsg — sequence 0 returns error
[Fact]
public async Task Delete_message_with_zero_sequence_returns_error()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DZ", "dz.>");
_ = await fx.PublishAndGetAckAsync("dz.x", "data");
var del = await fx.RequestLocalAsync("$JS.API.STREAM.MSG.DELETE.DZ", """{"seq":0}""");
del.Error.ShouldNotBeNull();
}
// Go: TestJetStreamDeleteMsg — non-existent stream
[Fact]
public async Task Delete_message_from_non_existent_stream()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("EXISTS", "exists.>");
var del = await fx.RequestLocalAsync("$JS.API.STREAM.MSG.DELETE.NOTEXIST", """{"seq":1}""");
del.Success.ShouldBeFalse();
}
// Go: TestJetStreamRestoreBadStream server/jetstream_test.go
[Fact]
public async Task Info_for_non_existent_stream_returns_error()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("X", "x.>");
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.DOESNOTEXIST", "{}");
info.Error.ShouldNotBeNull();
}
// Go: TestJetStreamStreamPurge — multiple purges are idempotent
[Fact]
public async Task Multiple_purges_are_idempotent()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("MP", "mp.>");
for (var i = 0; i < 3; i++)
_ = await fx.PublishAndGetAckAsync("mp.x", $"msg-{i}");
_ = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.MP", "{}");
var second = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.MP", "{}");
second.Success.ShouldBeTrue();
var state = await fx.GetStreamStateAsync("MP");
state.Messages.ShouldBe(0UL);
}
// Go: TestJetStreamAddStream — retention policy Limits
[Fact]
public async Task Create_stream_with_limits_retention()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "LIM",
Subjects = ["lim.>"],
Retention = RetentionPolicy.Limits,
});
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.LIM", "{}");
info.StreamInfo!.Config.Retention.ShouldBe(RetentionPolicy.Limits);
}
// Go: TestJetStreamInterestRetentionStream server/jetstream_test.go:4336
[Fact]
public async Task Create_stream_with_interest_retention()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "INT",
Subjects = ["int.>"],
Retention = RetentionPolicy.Interest,
});
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.INT", "{}");
info.StreamInfo!.Config.Retention.ShouldBe(RetentionPolicy.Interest);
}
// Go: TestJetStreamBasicWorkQueue server/jetstream_test.go:937
[Fact]
public async Task Create_stream_with_workqueue_retention()
{
await using var fx = await JetStreamApiFixture.StartWithStreamConfigAsync(new StreamConfig
{
Name = "WQ",
Subjects = ["wq.>"],
Retention = RetentionPolicy.WorkQueue,
MaxConsumers = 1,
});
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.WQ", "{}");
info.StreamInfo!.Config.Retention.ShouldBe(RetentionPolicy.WorkQueue);
}
// Go: TestJetStreamSnapshotsAPI server/jetstream_test.go:3328
[Fact]
public async Task Snapshot_and_restore_roundtrip()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("SNAP", "snap.>");
_ = await fx.PublishAndGetAckAsync("snap.a", "data1");
_ = await fx.PublishAndGetAckAsync("snap.b", "data2");
var snap = await fx.RequestLocalAsync("$JS.API.STREAM.SNAPSHOT.SNAP", "{}");
snap.Error.ShouldBeNull();
snap.Snapshot.ShouldNotBeNull();
snap.Snapshot!.Payload.ShouldNotBeNullOrWhiteSpace();
// Restore into the same stream
var restore = await fx.RequestLocalAsync(
"$JS.API.STREAM.RESTORE.SNAP",
snap.Snapshot.Payload);
restore.Success.ShouldBeTrue();
}
// Go: TestJetStreamAddStreamOverlapWithJSAPISubjects server/jetstream_test.go:666
[Fact]
public async Task Create_multiple_streams_with_non_overlapping_subjects()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("S1", "s1.>");
var s2 = await fx.RequestLocalAsync("$JS.API.STREAM.CREATE.S2", """{"subjects":["s2.>"]}""");
s2.Error.ShouldBeNull();
var ack1 = await fx.PublishAndGetAckAsync("s1.x", "data1");
ack1.Stream.ShouldBe("S1");
var ack2 = await fx.PublishAndGetAckAsync("s2.x", "data2");
ack2.Stream.ShouldBe("S2");
}
// Go: TestJetStreamStreamPurge — verify bytes reset after purge
[Fact]
public async Task Purge_resets_byte_count()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("PB", "pb.>");
for (var i = 0; i < 5; i++)
_ = await fx.PublishAndGetAckAsync("pb.x", "some-data");
var before = await fx.GetStreamStateAsync("PB");
before.Bytes.ShouldBeGreaterThan(0UL);
_ = await fx.RequestLocalAsync("$JS.API.STREAM.PURGE.PB", "{}");
var after = await fx.GetStreamStateAsync("PB");
after.Bytes.ShouldBe(0UL);
}
// Go: TestJetStreamDefaultMaxMsgsPer server/jetstream_test.go
[Fact]
public async Task Stream_defaults_replicas_to_one()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("DEF", "def.>");
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.DEF", "{}");
info.StreamInfo!.Config.Replicas.ShouldBe(1);
}
// Go: TestJetStreamSuppressAllowDirect server/jetstream_test.go
[Fact]
public async Task Allow_direct_defaults_to_false()
{
await using var fx = await JetStreamApiFixture.StartWithStreamAsync("AD", "ad.>");
var info = await fx.RequestLocalAsync("$JS.API.STREAM.INFO.AD", "{}");
info.StreamInfo!.Config.AllowDirect.ShouldBeFalse();
}
}