using NATS.Server.Auth; using Shouldly; namespace NATS.Server.Tests.Auth; /// /// Tests for SUB permission caching and generation-based invalidation. /// Reference: Go server/client.go — subPermCache, pubPermCache, perm cache invalidation on account update. /// public sealed class SubPermissionCacheTests { // ── SUB API ─────────────────────────────────────────────────────────────── [Fact] public void SetSub_and_TryGetSub_round_trips() { var cache = new PermissionLruCache(); cache.SetSub("foo.bar", true); cache.TryGetSub("foo.bar", out var result).ShouldBeTrue(); result.ShouldBeTrue(); } [Fact] public void TryGetSub_returns_false_for_unknown() { var cache = new PermissionLruCache(); cache.TryGetSub("unknown.subject", out var result).ShouldBeFalse(); result.ShouldBeFalse(); } [Fact] public void PUB_and_SUB_stored_independently() { var cache = new PermissionLruCache(); // Same logical subject, different PUB/SUB outcomes cache.Set("orders.>", false); // PUB denied cache.SetSub("orders.>", true); // SUB allowed cache.TryGet("orders.>", out var pubAllowed).ShouldBeTrue(); pubAllowed.ShouldBeFalse(); cache.TryGetSub("orders.>", out var subAllowed).ShouldBeTrue(); subAllowed.ShouldBeTrue(); } // ── Invalidation ───────────────────────────────────────────────────────── [Fact] public void Invalidate_clears_on_next_access() { var cache = new PermissionLruCache(); cache.Set("pub.subject", true); cache.SetSub("sub.subject", true); cache.Invalidate(); // Both PUB and SUB lookups should miss after invalidation cache.TryGet("pub.subject", out _).ShouldBeFalse(); cache.TryGetSub("sub.subject", out _).ShouldBeFalse(); } [Fact] public void Generation_increments_on_invalidate() { var cache = new PermissionLruCache(); var before = cache.Generation; cache.Invalidate(); var afterOne = cache.Generation; cache.Invalidate(); var afterTwo = cache.Generation; afterOne.ShouldBe(before + 1); afterTwo.ShouldBe(before + 2); } // ── LRU eviction ───────────────────────────────────────────────────────── [Fact] public void LRU_eviction_applies_to_SUB_entries() { // capacity = 4: fill with 4 SUB entries then add a 5th; the oldest should be evicted var cache = new PermissionLruCache(capacity: 4); cache.SetSub("a", true); cache.SetSub("b", true); cache.SetSub("c", true); cache.SetSub("d", true); // Touch "a" so it becomes MRU; "b" becomes LRU cache.TryGetSub("a", out _); // Adding "e" should evict "b" (LRU) cache.SetSub("e", true); cache.Count.ShouldBe(4); cache.TryGetSub("b", out _).ShouldBeFalse("b should have been evicted"); cache.TryGetSub("e", out _).ShouldBeTrue("e was just added"); } // ── Backward compatibility ──────────────────────────────────────────────── [Fact] public void Existing_PUB_API_still_works() { var cache = new PermissionLruCache(); cache.Set("pub.only", true); cache.TryGet("pub.only", out var value).ShouldBeTrue(); value.ShouldBeTrue(); // Overwrite with false cache.Set("pub.only", false); cache.TryGet("pub.only", out value).ShouldBeTrue(); value.ShouldBeFalse(); } // ── Account.GenerationId ────────────────────────────────────────────────── [Fact] public void Account_GenerationId_starts_at_zero() { var account = new Account("test"); account.GenerationId.ShouldBe(0L); } [Fact] public void Account_IncrementGeneration_increments() { var account = new Account("test"); account.IncrementGeneration(); account.GenerationId.ShouldBe(1L); account.IncrementGeneration(); account.GenerationId.ShouldBe(2L); } // ── Mixed PUB + SUB count ───────────────────────────────────────────────── [Fact] public void Mixed_PUB_SUB_count_includes_both() { var cache = new PermissionLruCache(); cache.Set("pub.a", true); cache.Set("pub.b", false); cache.SetSub("sub.a", true); cache.SetSub("sub.b", false); // All four entries (stored under different internal keys) contribute to Count cache.Count.ShouldBe(4); } }