// Go reference: server/consumer.go — pauseConsumer / resumeConsumer / isPaused // Tests for the consumer pause/resume API endpoint, including pause_until (RFC3339) // time-bounded pauses and response body containing pause state. using NATS.Server.JetStream.Api; using NATS.Server.TestUtilities; namespace NATS.Server.JetStream.Tests.JetStream.Api; public class ConsumerPauseApiTests : IAsyncLifetime { private JetStreamApiFixture _fx = null!; public async Task InitializeAsync() { _fx = await JetStreamApiFixture.StartWithStreamAsync("ORDERS", "orders.*"); _ = await _fx.CreateConsumerAsync("ORDERS", "MON", "orders.created"); } public async Task DisposeAsync() => await _fx.DisposeAsync(); // Go ref: consumer.go pauseConsumer — pause=true pauses consumer. [Fact] public async Task HandlePause_with_pause_true_pauses_consumer() { var resp = await _fx.RequestLocalAsync( "$JS.API.CONSUMER.PAUSE.ORDERS.MON", "{\"pause\":true}"); resp.Error.ShouldBeNull(); resp.Success.ShouldBeTrue(); resp.Paused.ShouldBe(true); } // Go ref: consumer.go resumeConsumer — pause=false resumes consumer. [Fact] public async Task HandlePause_with_pause_false_resumes_consumer() { // First pause await _fx.RequestLocalAsync("$JS.API.CONSUMER.PAUSE.ORDERS.MON", "{\"pause\":true}"); // Then resume var resp = await _fx.RequestLocalAsync( "$JS.API.CONSUMER.PAUSE.ORDERS.MON", "{\"pause\":false}"); resp.Error.ShouldBeNull(); resp.Success.ShouldBeTrue(); resp.Paused.ShouldBe(false); } // Go ref: consumer.go pauseConsumer — pause_until sets deadline UTC datetime. [Fact] public async Task HandlePause_with_pause_until_sets_deadline() { var future = DateTime.UtcNow.AddHours(1); var iso = future.ToString("O"); // RFC3339 round-trip format var resp = await _fx.RequestLocalAsync( "$JS.API.CONSUMER.PAUSE.ORDERS.MON", $"{{\"pause_until\":\"{iso}\"}}"); resp.Error.ShouldBeNull(); resp.PauseUntil.ShouldNotBeNull(); resp.PauseUntil!.Value.Should_Be_Close_To_Utc(future, tolerance: TimeSpan.FromSeconds(2)); } // Go ref: consumer.go pauseConsumer — pause_until implies pause=true. [Fact] public async Task HandlePause_with_pause_until_implies_pause_true() { var future = DateTime.UtcNow.AddHours(1); var iso = future.ToString("O"); var resp = await _fx.RequestLocalAsync( "$JS.API.CONSUMER.PAUSE.ORDERS.MON", $"{{\"pause_until\":\"{iso}\"}}"); resp.Error.ShouldBeNull(); resp.Paused.ShouldBe(true); } // Go ref: consumer.go isPaused — response includes current pause state. [Fact] public async Task HandlePause_returns_pause_state_in_response() { var resp = await _fx.RequestLocalAsync( "$JS.API.CONSUMER.PAUSE.ORDERS.MON", "{\"pause\":true}"); resp.Paused.ShouldBe(true); var resumeResp = await _fx.RequestLocalAsync( "$JS.API.CONSUMER.PAUSE.ORDERS.MON", "{\"pause\":false}"); resumeResp.Paused.ShouldBe(false); } // Go ref: consumer.go pauseUntil — response includes pause_until when set. [Fact] public async Task HandlePause_returns_pause_until_in_response() { var future = DateTime.UtcNow.AddMinutes(30); var iso = future.ToString("O"); var resp = await _fx.RequestLocalAsync( "$JS.API.CONSUMER.PAUSE.ORDERS.MON", $"{{\"pause_until\":\"{iso}\"}}"); resp.PauseUntil.ShouldNotBeNull(); resp.PauseUntil!.Value.Kind.ShouldBe(DateTimeKind.Utc); } // Go ref: consumer.go pauseConsumer — 404 when consumer not found. [Fact] public async Task HandlePause_returns_not_found_for_missing_consumer() { var resp = await _fx.RequestLocalAsync( "$JS.API.CONSUMER.PAUSE.ORDERS.NONEXISTENT", "{\"pause\":true}"); resp.Error.ShouldNotBeNull(); resp.Error!.Code.ShouldBe(404); } // Go ref: consumer.go resumeConsumer — empty payload resumes consumer. [Fact] public async Task HandlePause_with_empty_payload_resumes() { // Pause first await _fx.RequestLocalAsync("$JS.API.CONSUMER.PAUSE.ORDERS.MON", "{\"pause\":true}"); // Empty body = resume var resp = await _fx.RequestLocalAsync("$JS.API.CONSUMER.PAUSE.ORDERS.MON", ""); resp.Error.ShouldBeNull(); resp.Success.ShouldBeTrue(); resp.Paused.ShouldBe(false); } // Go ref: consumer.go pauseConsumer — past pause_until auto-resumes immediately. [Fact] public async Task HandlePause_with_past_pause_until_auto_resumes() { var past = DateTime.UtcNow.AddHours(-1); var iso = past.ToString("O"); var resp = await _fx.RequestLocalAsync( "$JS.API.CONSUMER.PAUSE.ORDERS.MON", $"{{\"pause_until\":\"{iso}\"}}"); // Deadline already passed — consumer should auto-resume, so paused=false. resp.Error.ShouldBeNull(); resp.Success.ShouldBeTrue(); resp.Paused.ShouldBe(false); } // Go ref: jsConsumerPauseT — bad subject (not matching stream.consumer pattern) returns 404. [Fact] public async Task HandlePause_returns_not_found_for_bad_subject() { var resp = await _fx.RequestLocalAsync( "$JS.API.CONSUMER.PAUSE.ONLY_ONE_TOKEN", "{\"pause\":true}"); resp.Error.ShouldNotBeNull(); resp.Error!.Code.ShouldBe(404); } } /// /// Shouldly-compatible extension for DateTime proximity assertions. /// internal static class DateTimeAssertExtensions { public static void Should_Be_Close_To_Utc(this DateTime actual, DateTime expected, TimeSpan tolerance) { var diff = (actual.ToUniversalTime() - expected.ToUniversalTime()).Duration(); diff.ShouldBeLessThanOrEqualTo(tolerance); } }