feat: add SUB permission caching with generation invalidation (Gap 5.8)
Extend PermissionLruCache with SetSub/TryGetSub (internal key prefix "S:")
alongside existing PUB API ("P:" prefix, backward-compatible). Add Invalidate()
and Generation property for generation-based cache invalidation. Add
GenerationId/IncrementGeneration to Account for account-level change signalling.
10 new tests in SubPermissionCacheTests cover all paths.
This commit is contained in:
161
tests/NATS.Server.Tests/Auth/SubPermissionCacheTests.cs
Normal file
161
tests/NATS.Server.Tests/Auth/SubPermissionCacheTests.cs
Normal file
@@ -0,0 +1,161 @@
|
||||
using NATS.Server.Auth;
|
||||
using Shouldly;
|
||||
|
||||
namespace NATS.Server.Tests.Auth;
|
||||
|
||||
/// <summary>
|
||||
/// Tests for SUB permission caching and generation-based invalidation.
|
||||
/// Reference: Go server/client.go — subPermCache, pubPermCache, perm cache invalidation on account update.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user