using System.Text; using NATS.Server.JetStream; using NATS.Server.JetStream.Models; using NATS.Server.JetStream.Publish; namespace NATS.Server.JetStream.Tests; public class JetStreamStreamReplicaGroupTests { [Fact] public async Task Leader_stepdown_preserves_stream_write_availability_after_new_election() { await using var fixture = await JetStreamReplicaFixture.StartAsync(nodes: 3); await fixture.CreateStreamAsync("ORDERS", replicas: 3); await fixture.StepDownStreamLeaderAsync("ORDERS"); var ack = await fixture.PublishAndGetAckAsync("orders.created", "1"); ack.Stream.ShouldBe("ORDERS"); ack.Seq.ShouldBeGreaterThan((ulong)0); } } internal sealed class JetStreamReplicaFixture : IAsyncDisposable { private readonly StreamManager _streamManager; private readonly JetStreamPublisher _publisher; private JetStreamReplicaFixture(StreamManager streamManager) { _streamManager = streamManager; _publisher = new JetStreamPublisher(_streamManager); } public static Task StartAsync(int nodes) { _ = nodes; var streamManager = new StreamManager(); return Task.FromResult(new JetStreamReplicaFixture(streamManager)); } public Task CreateStreamAsync(string name, int replicas) { var response = _streamManager.CreateOrUpdate(new StreamConfig { Name = name, Subjects = ["orders.*"], Replicas = replicas, }); if (response.Error is not null) throw new InvalidOperationException(response.Error.Description); return Task.CompletedTask; } public Task StepDownStreamLeaderAsync(string stream) { return _streamManager.StepDownStreamLeaderAsync(stream, default); } public Task PublishAndGetAckAsync(string subject, string payload) { if (_publisher.TryCapture(subject, Encoding.UTF8.GetBytes(payload), null, out var ack)) return Task.FromResult(ack); throw new InvalidOperationException("Publish did not match a stream."); } public ValueTask DisposeAsync() => ValueTask.CompletedTask; }