// Go: consumer.go hasDeliveryInterest, deleteNotActive using NATS.Server.JetStream.Consumers; namespace NATS.Server.JetStream.Tests.JetStream.Consumers; public class DeliveryInterestTests { // ------------------------------------------------------------------------- // Test 1 — HasInterest is true after a subscribe // // Go reference: consumer.go hasDeliveryInterest — returns true when at // least one client is subscribed to the push consumer's deliver subject. // ------------------------------------------------------------------------- [Fact] public void HasInterest_true_after_subscribe() { var tracker = new DeliveryInterestTracker(); tracker.OnSubscribe(); tracker.HasInterest.ShouldBeTrue(); } // ------------------------------------------------------------------------- // Test 2 — HasInterest is false initially (no subscribers) // // Go reference: consumer.go — on creation there are no delivery subscribers. // ------------------------------------------------------------------------- [Fact] public void HasInterest_false_initially() { var tracker = new DeliveryInterestTracker(); tracker.HasInterest.ShouldBeFalse(); } // ------------------------------------------------------------------------- // Test 3 — HasInterest drops to false after all subscribers unsubscribe // // Go reference: consumer.go hasDeliveryInterest — once subscription count // reaches 0, interest is gone. // ------------------------------------------------------------------------- [Fact] public void HasInterest_false_after_all_unsubscribe() { var tracker = new DeliveryInterestTracker(); tracker.OnSubscribe(); tracker.OnSubscribe(); tracker.OnUnsubscribe(); tracker.OnUnsubscribe(); tracker.HasInterest.ShouldBeFalse(); } // ------------------------------------------------------------------------- // Test 4 — SubscriberCount tracks multiple subscribers accurately // // Go reference: consumer.go — the interest count must reflect the exact // number of active push-consumer delivery subscriptions. // ------------------------------------------------------------------------- [Fact] public void SubscriberCount_tracks_multiple_subscribers() { var tracker = new DeliveryInterestTracker(); tracker.SubscriberCount.ShouldBe(0); tracker.OnSubscribe(); tracker.SubscriberCount.ShouldBe(1); tracker.OnSubscribe(); tracker.SubscriberCount.ShouldBe(2); tracker.OnSubscribe(); tracker.SubscriberCount.ShouldBe(3); tracker.OnUnsubscribe(); tracker.SubscriberCount.ShouldBe(2); } // ------------------------------------------------------------------------- // Test 5 — OnUnsubscribe floors subscriber count at zero (no negatives) // // Go reference: consumer.go deleteNotActive — stray unsub events must not // drive the count below zero and corrupt subsequent interest checks. // ------------------------------------------------------------------------- [Fact] public void OnUnsubscribe_floors_at_zero() { var tracker = new DeliveryInterestTracker(); tracker.OnUnsubscribe(); tracker.OnUnsubscribe(); tracker.SubscriberCount.ShouldBe(0); tracker.HasInterest.ShouldBeFalse(); } // ------------------------------------------------------------------------- // Test 6 — ShouldDelete is false while interest exists // // Go reference: consumer.go deleteNotActive — ephemeral cleanup is only // triggered when there are no active subscribers. // ------------------------------------------------------------------------- [Fact] public void ShouldDelete_false_when_has_interest() { var tracker = new DeliveryInterestTracker(inactiveTimeout: TimeSpan.FromMilliseconds(1)); tracker.OnSubscribe(); tracker.ShouldDelete.ShouldBeFalse(); } // ------------------------------------------------------------------------- // Test 7 — ShouldDelete is false immediately after unsubscribe (timeout // has not yet elapsed) // // Go reference: consumer.go deleteNotActive — the inactive timeout must // fully elapse before the consumer is eligible for deletion. // ------------------------------------------------------------------------- [Fact] public void ShouldDelete_false_immediately_after_unsubscribe() { var tracker = new DeliveryInterestTracker(inactiveTimeout: TimeSpan.FromSeconds(30)); tracker.OnSubscribe(); tracker.OnUnsubscribe(); // No wait — timeout has not elapsed yet. tracker.ShouldDelete.ShouldBeFalse(); } // ------------------------------------------------------------------------- // Test 8 — ShouldDelete is true after the inactive timeout elapses with // zero subscribers // // Go reference: consumer.go deleteNotActive — once the configurable // MaxAckPending / inactive threshold passes, the ephemeral consumer is // scheduled for removal. // ------------------------------------------------------------------------- [SlopwatchSuppress("SW004", "Intentional timeout test: ShouldDelete requires real wall-clock elapsed time to observe the inactive threshold firing; no synchronisation primitive can replace this")] [Fact] public async Task ShouldDelete_true_after_timeout() { var tracker = new DeliveryInterestTracker(inactiveTimeout: TimeSpan.FromMilliseconds(50)); tracker.OnSubscribe(); tracker.OnUnsubscribe(); await Task.Delay(100); // Wait for timeout to elapse. tracker.ShouldDelete.ShouldBeTrue(); } // ------------------------------------------------------------------------- // Test 9 — Reset clears all state (count and inactivity timer) // // Go reference: consumer.go — Reset is used to reinitialise tracking when // a consumer is re-attached or recreated. // ------------------------------------------------------------------------- [SlopwatchSuppress("SW004", "Intentional timeout test: must let the inactive threshold elapse to confirm Reset clears the inactivity timer; no synchronisation primitive can replace this")] [Fact] public async Task Reset_clears_all_state() { var tracker = new DeliveryInterestTracker(inactiveTimeout: TimeSpan.FromMilliseconds(50)); tracker.OnSubscribe(); tracker.OnSubscribe(); tracker.OnUnsubscribe(); tracker.OnUnsubscribe(); await Task.Delay(100); // Let timeout elapse. tracker.Reset(); tracker.SubscriberCount.ShouldBe(0); tracker.HasInterest.ShouldBeFalse(); tracker.ShouldDelete.ShouldBeFalse(); // inactivity timer also cleared } // ------------------------------------------------------------------------- // Test 10 — Subscribing after an unsubscribe clears the inactivity timer // so ShouldDelete stays false even after the original timeout // // Go reference: consumer.go hasDeliveryInterest — a re-subscription resets // the inactive-since timestamp, preventing spurious cleanup. // ------------------------------------------------------------------------- [SlopwatchSuppress("SW004", "Intentional timeout test: must let the original inactive window pass after re-subscribe to confirm the inactivity timer was cleared; no synchronisation primitive can replace this")] [Fact] public async Task Subscribe_clears_inactivity_timer() { var tracker = new DeliveryInterestTracker(inactiveTimeout: TimeSpan.FromMilliseconds(50)); tracker.OnSubscribe(); tracker.OnUnsubscribe(); // Re-subscribe before the timeout elapses. tracker.OnSubscribe(); await Task.Delay(100); // Original timeout window passes. // Still has interest and timer was reset, so ShouldDelete must be false. tracker.HasInterest.ShouldBeTrue(); tracker.ShouldDelete.ShouldBeFalse(); } }