refactor: extract NATS.Server.Gateways.Tests project

Move 25 gateway-related test files from NATS.Server.Tests into a
dedicated NATS.Server.Gateways.Tests project. Update namespaces,
replace private ReadUntilAsync with SocketTestHelper from TestUtilities,
inline TestServerFactory usage, add InternalsVisibleTo, and register
the project in the solution file. All 261 tests pass.
This commit is contained in:
Joseph Doherty
2026-03-12 15:10:50 -04:00
parent a6be5e11ed
commit 9972b74bc3
29 changed files with 101 additions and 58 deletions

View File

@@ -0,0 +1,164 @@
using NATS.Server.Gateways;
using Shouldly;
namespace NATS.Server.Gateways.Tests.Gateways;
/// <summary>
/// Tests for the ReplyMapCache LRU cache with TTL expiration.
/// Go reference: gateway.go — reply mapping cache.
/// </summary>
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);
}
}