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.
165 lines
4.9 KiB
C#
165 lines
4.9 KiB
C#
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);
|
|
}
|
|
}
|