// Go reference: server/monitor.go — closedClients ring buffer, ClosedState tracking. // These tests verify the fixed-size ring buffer used to track recently closed connections // for the /connz?state=closed monitoring endpoint. using NATS.Server.Monitoring; namespace NATS.Server.Tests.Monitoring; public class ClosedConnectionRingBufferTests { // ----------------------------------------------------------------------- // Helpers // ----------------------------------------------------------------------- private static ClosedClient MakeEntry(ulong cid, string reason = "normal") => new() { Cid = cid, Ip = "127.0.0.1", Port = 4222, Start = DateTime.UtcNow.AddSeconds(-10), Stop = DateTime.UtcNow, Reason = reason, }; // ----------------------------------------------------------------------- // 1. Add_IncreasesCount // ----------------------------------------------------------------------- /// /// Adding entries should increase Count up to capacity. /// [Fact] public void Add_IncreasesCount() { var buf = new ClosedConnectionRingBuffer(capacity: 10); buf.Count.ShouldBe(0); buf.Add(MakeEntry(1)); buf.Count.ShouldBe(1); buf.Add(MakeEntry(2)); buf.Count.ShouldBe(2); buf.Add(MakeEntry(3)); buf.Count.ShouldBe(3); } // ----------------------------------------------------------------------- // 2. Add_RingOverwrite_CapacityNotExceeded // ----------------------------------------------------------------------- /// /// When capacity is exceeded the count stays at capacity (oldest entry is overwritten). /// [Fact] public void Add_RingOverwrite_CapacityNotExceeded() { const int capacity = 5; var buf = new ClosedConnectionRingBuffer(capacity); for (var i = 1; i <= capacity + 3; i++) buf.Add(MakeEntry((ulong)i)); buf.Count.ShouldBe(capacity); } // ----------------------------------------------------------------------- // 3. GetAll_ReturnsNewestFirst // ----------------------------------------------------------------------- /// /// GetAll should return entries ordered newest-first. /// [Fact] public void GetAll_ReturnsNewestFirst() { var buf = new ClosedConnectionRingBuffer(capacity: 10); buf.Add(MakeEntry(1)); buf.Add(MakeEntry(2)); buf.Add(MakeEntry(3)); var all = buf.GetAll(); all.Count.ShouldBe(3); all[0].Cid.ShouldBe(3UL); // newest all[1].Cid.ShouldBe(2UL); all[2].Cid.ShouldBe(1UL); // oldest } // ----------------------------------------------------------------------- // 4. GetAll_EmptyBuffer_ReturnsEmpty // ----------------------------------------------------------------------- /// /// GetAll on an empty buffer should return an empty list. /// [Fact] public void GetAll_EmptyBuffer_ReturnsEmpty() { var buf = new ClosedConnectionRingBuffer(capacity: 10); var all = buf.GetAll(); all.ShouldBeEmpty(); } // ----------------------------------------------------------------------- // 5. GetRecent_ReturnsRequestedCount // ----------------------------------------------------------------------- /// /// GetRecent(n) where n <= Count should return exactly n entries. /// [Fact] public void GetRecent_ReturnsRequestedCount() { var buf = new ClosedConnectionRingBuffer(capacity: 10); for (var i = 1; i <= 8; i++) buf.Add(MakeEntry((ulong)i)); var recent = buf.GetRecent(3); recent.Count.ShouldBe(3); recent[0].Cid.ShouldBe(8UL); // newest recent[1].Cid.ShouldBe(7UL); recent[2].Cid.ShouldBe(6UL); } // ----------------------------------------------------------------------- // 6. GetRecent_LessThanAvailable_ReturnsAll // ----------------------------------------------------------------------- /// /// GetRecent(n) where n > Count should return all available entries. /// [Fact] public void GetRecent_LessThanAvailable_ReturnsAll() { var buf = new ClosedConnectionRingBuffer(capacity: 10); buf.Add(MakeEntry(1)); buf.Add(MakeEntry(2)); var recent = buf.GetRecent(100); recent.Count.ShouldBe(2); } // ----------------------------------------------------------------------- // 7. TotalClosed_TracksAllAdditions // ----------------------------------------------------------------------- /// /// TotalClosed should increment for every Add, even after the ring wraps around. /// [Fact] public void TotalClosed_TracksAllAdditions() { const int capacity = 4; var buf = new ClosedConnectionRingBuffer(capacity); buf.TotalClosed.ShouldBe(0L); for (var i = 1; i <= 10; i++) buf.Add(MakeEntry((ulong)i)); buf.TotalClosed.ShouldBe(10L); buf.Count.ShouldBe(capacity); // buffer is full but total reflects all 10 } // ----------------------------------------------------------------------- // 8. Clear_ResetsCountAndBuffer // ----------------------------------------------------------------------- /// /// Clear should reset Count to zero and GetAll should return an empty list. /// TotalClosed is intentionally not reset because it is a running lifetime counter. /// [Fact] public void Clear_ResetsCountAndBuffer() { var buf = new ClosedConnectionRingBuffer(capacity: 10); buf.Add(MakeEntry(1)); buf.Add(MakeEntry(2)); buf.Add(MakeEntry(3)); buf.TotalClosed.ShouldBe(3L); buf.Clear(); buf.Count.ShouldBe(0); buf.GetAll().ShouldBeEmpty(); // TotalClosed is a lifetime counter; it is not reset by Clear. buf.TotalClosed.ShouldBe(3L); } // ----------------------------------------------------------------------- // 9. Capacity_ReturnsConfiguredSize // ----------------------------------------------------------------------- /// /// Capacity should reflect the value passed to the constructor. /// [Fact] public void Capacity_ReturnsConfiguredSize() { var buf = new ClosedConnectionRingBuffer(capacity: 42); buf.Capacity.ShouldBe(42); } // ----------------------------------------------------------------------- // 10. Add_WrapsCorrectly // ----------------------------------------------------------------------- /// /// After adding capacity+1 items the oldest entry (cid=1) should no longer be present, /// and the buffer should contain the most recent 'capacity' items. /// [Fact] public void Add_WrapsCorrectly() { const int capacity = 5; var buf = new ClosedConnectionRingBuffer(capacity); for (var i = 1; i <= capacity + 1; i++) buf.Add(MakeEntry((ulong)i)); var all = buf.GetAll(); all.Count.ShouldBe(capacity); // cid=1 (the oldest) should have been overwritten all.Any(e => e.Cid == 1UL).ShouldBeFalse(); // The newest entry (cid=capacity+1) should be first all[0].Cid.ShouldBe((ulong)(capacity + 1)); } }