Files
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

208 lines
8.1 KiB
C#

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