using NATS.Server.Gateways; using Shouldly; namespace NATS.Server.Gateways.Tests.Gateways; /// /// Tests for the ReplyMapCache LRU cache with TTL expiration. /// Go reference: gateway.go — reply mapping cache. /// public class ReplyMapCacheTests { // Go: gateway.go — cache is initially empty, lookups return false [Fact] public void TryGet_returns_false_on_empty() { var cache = new ReplyMapCache(capacity: 16, ttlMs: 60_000); var found = cache.TryGet("_INBOX.abc", out var value); found.ShouldBeFalse(); value.ShouldBeNull(); } // Go: gateway.go — cached mapping is retrievable after Set [Fact] public void Set_and_TryGet_round_trips() { var cache = new ReplyMapCache(capacity: 16, ttlMs: 60_000); cache.Set("_INBOX.abc", "_GR_.cluster1.42._INBOX.abc"); var found = cache.TryGet("_INBOX.abc", out var value); found.ShouldBeTrue(); value.ShouldBe("_GR_.cluster1.42._INBOX.abc"); } // Go: gateway.go — LRU eviction removes the least-recently-used entry when at capacity [Fact] public void LRU_eviction_at_capacity() { var cache = new ReplyMapCache(capacity: 2, ttlMs: 60_000); cache.Set("key1", "val1"); cache.Set("key2", "val2"); // Adding a third entry should evict the LRU entry (key1, since key2 was added last) cache.Set("key3", "val3"); cache.TryGet("key1", out _).ShouldBeFalse(); cache.TryGet("key2", out var v2).ShouldBeTrue(); v2.ShouldBe("val2"); cache.TryGet("key3", out var v3).ShouldBeTrue(); v3.ShouldBe("val3"); } // Go: gateway.go — entries expire after the configured TTL window [Fact] [SlopwatchSuppress("SW004", "TTL expiry test requires real wall-clock time to elapse; no synchronisation primitive can replace observing a time-based cache eviction")] public void TTL_expiration() { var cache = new ReplyMapCache(capacity: 16, ttlMs: 1); cache.Set("_INBOX.ttl", "_GR_.c1.1._INBOX.ttl"); Thread.Sleep(5); // Wait longer than the 1ms TTL var found = cache.TryGet("_INBOX.ttl", out var value); found.ShouldBeFalse(); value.ShouldBeNull(); } // Go: gateway.go — hit counter tracks successful cache lookups [Fact] public void Hits_incremented_on_hit() { var cache = new ReplyMapCache(capacity: 16, ttlMs: 60_000); cache.Set("key", "value"); cache.TryGet("key", out _); cache.TryGet("key", out _); cache.Hits.ShouldBe(2); cache.Misses.ShouldBe(0); } // Go: gateway.go — miss counter tracks failed lookups [Fact] public void Misses_incremented_on_miss() { var cache = new ReplyMapCache(capacity: 16, ttlMs: 60_000); cache.TryGet("nope", out _); cache.TryGet("also-nope", out _); cache.Misses.ShouldBe(2); cache.Hits.ShouldBe(0); } // Go: gateway.go — Clear removes all entries from the cache [Fact] public void Clear_removes_all() { var cache = new ReplyMapCache(capacity: 16, ttlMs: 60_000); cache.Set("a", "1"); cache.Set("b", "2"); cache.Set("c", "3"); cache.Clear(); cache.Count.ShouldBe(0); cache.TryGet("a", out _).ShouldBeFalse(); cache.TryGet("b", out _).ShouldBeFalse(); cache.TryGet("c", out _).ShouldBeFalse(); } // Go: gateway.go — Set on existing key updates the value and promotes to MRU [Fact] public void Set_updates_existing() { var cache = new ReplyMapCache(capacity: 16, ttlMs: 60_000); cache.Set("key", "original"); cache.Set("key", "updated"); cache.TryGet("key", out var value).ShouldBeTrue(); value.ShouldBe("updated"); cache.Count.ShouldBe(1); } // Go: gateway.go — PurgeExpired removes only expired entries [Fact] [SlopwatchSuppress("SW004", "TTL expiry test requires real wall-clock time to elapse; no synchronisation primitive can replace observing a time-based cache eviction")] public void PurgeExpired_removes_old_entries() { var cache = new ReplyMapCache(capacity: 16, ttlMs: 1); cache.Set("old1", "v1"); cache.Set("old2", "v2"); Thread.Sleep(5); // Ensure both entries are past the 1ms TTL var purged = cache.PurgeExpired(); purged.ShouldBe(2); cache.Count.ShouldBe(0); } // Go: gateway.go — Count reflects the current number of cached entries [Fact] public void Count_reflects_entries() { var cache = new ReplyMapCache(capacity: 16, ttlMs: 60_000); cache.Count.ShouldBe(0); cache.Set("a", "1"); cache.Count.ShouldBe(1); cache.Set("b", "2"); cache.Count.ShouldBe(2); cache.Set("c", "3"); cache.Count.ShouldBe(3); cache.Clear(); cache.Count.ShouldBe(0); } }