using Jellyfin.Plugin.Multilang.Services; namespace Jellyfin.Plugin.Multilang.Tests; public sealed class ItemsProxyCacheTests { [Fact] public void PolicyChangesInvalidateCachedDataAndOldInFlightWrites() { var cache = new ItemsProxyCache(); var generation = cache.ObserveUserPolicy("user", "all libraries"); Store(cache, "key"); Assert.Equal(generation, cache.ObserveUserPolicy("user", "all libraries")); Assert.True(cache.TryGet("key", TimeSpan.FromMinutes(1), out _)); Assert.True(cache.ObserveUserPolicy("user", "no libraries") > generation); Assert.False(cache.TryGet("key", TimeSpan.FromMinutes(1), out _)); Assert.False(cache.Store("key", "user", "/Items", 1, "body", "application/json", 1, 100, TimeSpan.FromMinutes(1), generation)); } [Fact] public void StoreRetrievesAndClearsCachedResponses() { var cache = new ItemsProxyCache(); Assert.True(Store(cache, "key")); Assert.True(cache.TryGet("key", TimeSpan.FromMinutes(1), out var response)); Assert.Equal("body-key", response.Body); Assert.Equal(1, response.ItemCount); cache.ClearAll(); Assert.False(cache.TryGet("key", TimeSpan.FromMinutes(1), out _)); } [Fact] public void MicroCacheServesFastResponsesAndIsInvalidatedWithTheMainCache() { var cache = new ItemsProxyCache(); Assert.True(cache.StoreMicro("key", "user", "/Items", 1, "micro", "application/json", 1, 1024)); Assert.True(cache.TryGet("key", TimeSpan.FromMinutes(1), out var response)); Assert.Equal("micro", response.Body); cache.ClearAll(); Assert.False(cache.TryGet("key", TimeSpan.FromMinutes(1), out _)); } [Fact] public void StoreEvictsTheOldestEntryWhenCapacityIsExceeded() { var cache = new ItemsProxyCache(); Assert.True(Store(cache, "first", body: "1111", maxBytes: 6)); Thread.Sleep(10); Assert.True(Store(cache, "second", body: "2222", maxBytes: 6)); Assert.False(cache.TryGet("first", TimeSpan.FromMinutes(1), out _)); Assert.True(cache.TryGet("second", TimeSpan.FromMinutes(1), out _)); } [Fact] public void StoreEntriesExpireAtTheConfiguredTtl() { var cache = new ItemsProxyCache(); Assert.True(Store(cache, "key", ttl: TimeSpan.FromMilliseconds(1))); Thread.Sleep(20); Assert.False(cache.TryGet("key", TimeSpan.FromMilliseconds(1), out _)); } [Fact] public void StoreRejectsResponsesLargerThanTheConfiguredLimit() { var cache = new ItemsProxyCache(); Assert.False(Store(cache, "key", body: "12345", maxBytes: 4)); Assert.False(cache.TryGet("key", TimeSpan.FromMinutes(1), out _)); } private static bool Store( ItemsProxyCache cache, string key, int itemCount = 1, string? body = null, long maxBytes = 1024, TimeSpan? ttl = null) => cache.Store( key, "user", "/Items", itemCount, body ?? "body-" + key, "application/json", 100, maxBytes, ttl ?? TimeSpan.FromMinutes(1)); [Fact] public void MicroAndMainEntriesShareOneBudget() { var cache = new ItemsProxyCache(); Assert.True(Store(cache, "main", body: "1111", maxBytes: 6)); Assert.True(cache.StoreMicro("micro1", "user", "/Items", 1, "2222", "application/json", 1, 6)); Assert.False(cache.TryGet("main", TimeSpan.FromMinutes(1), out _)); Assert.True(cache.StoreMicro("micro2", "user", "/Items", 1, "3333", "application/json", 1, 6)); Assert.False(cache.TryGet("micro1", TimeSpan.FromMinutes(1), out _)); Assert.True(cache.TryGet("micro2", TimeSpan.FromMinutes(1), out _)); } [Fact] public void InvalidationRejectsStaleInFlightWrites() { var cache = new ItemsProxyCache(); var generation = cache.Generation; cache.ClearAll(); Assert.False(cache.Store("key", "user", "/Items", 1, "body", "application/json", 1, 100, TimeSpan.FromMinutes(1), generation)); Assert.False(cache.StoreMicro("key", "user", "/Items", 1, "body", "application/json", 1, 100, generation)); } [Fact] public void ClearingOneUserPreservesOtherUsersCachedResponses() { var cache = new ItemsProxyCache(); Store(cache, "one"); cache.Store("two", "other", "/Items", 1, "body", "application/json", 1, 100, TimeSpan.FromMinutes(1)); cache.ClearUser("user"); Assert.False(cache.TryGet("one", TimeSpan.FromMinutes(1), out _)); Assert.True(cache.TryGet("two", TimeSpan.FromMinutes(1), out _)); } }