You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
494 lines
18 KiB
494 lines
18 KiB
using Bit.Core.Settings; |
|
using Microsoft.Extensions.Caching.Distributed; |
|
using Microsoft.Extensions.Configuration; |
|
using Microsoft.Extensions.DependencyInjection; |
|
using Microsoft.Extensions.DependencyInjection.Extensions; |
|
using NSubstitute; |
|
using StackExchange.Redis; |
|
using Xunit; |
|
using ZiggyCreatures.Caching.Fusion; |
|
using ZiggyCreatures.Caching.Fusion.Backplane; |
|
|
|
namespace Bit.Core.Test.Utilities; |
|
|
|
public class ExtendedCacheServiceCollectionExtensionsTests |
|
{ |
|
private readonly IServiceCollection _services; |
|
private readonly GlobalSettings _globalSettings; |
|
private const string _cacheName = "TestCache"; |
|
|
|
public ExtendedCacheServiceCollectionExtensionsTests() |
|
{ |
|
_services = new ServiceCollection(); |
|
|
|
var config = new ConfigurationBuilder() |
|
.AddInMemoryCollection(new Dictionary<string, string?>()) |
|
.Build(); |
|
|
|
_globalSettings = new GlobalSettings(); |
|
config.GetSection("GlobalSettings").Bind(_globalSettings); |
|
|
|
_services.TryAddSingleton(config); |
|
_services.TryAddSingleton(_globalSettings); |
|
_services.TryAddSingleton<IGlobalSettings>(_globalSettings); |
|
_services.AddLogging(); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_CustomSettings_OverridesDefaults() |
|
{ |
|
var settings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
Duration = TimeSpan.FromMinutes(12), |
|
FailSafeMaxDuration = TimeSpan.FromHours(1.5), |
|
FailSafeThrottleDuration = TimeSpan.FromMinutes(1), |
|
EagerRefreshThreshold = 0.75f, |
|
FactorySoftTimeout = TimeSpan.FromMilliseconds(20), |
|
FactoryHardTimeout = TimeSpan.FromSeconds(3), |
|
DistributedCacheSoftTimeout = TimeSpan.FromSeconds(0.5), |
|
DistributedCacheHardTimeout = TimeSpan.FromSeconds(1.5), |
|
JitterMaxDuration = TimeSpan.FromSeconds(5), |
|
IsFailSafeEnabled = false, |
|
AllowBackgroundDistributedCacheOperations = false, |
|
}; |
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
var opt = cache.DefaultEntryOptions; |
|
|
|
Assert.Equal(TimeSpan.FromMinutes(12), opt.Duration); |
|
Assert.False(opt.IsFailSafeEnabled); |
|
Assert.Equal(TimeSpan.FromHours(1.5), opt.FailSafeMaxDuration); |
|
Assert.Equal(TimeSpan.FromMinutes(1), opt.FailSafeThrottleDuration); |
|
Assert.Equal(0.75f, opt.EagerRefreshThreshold); |
|
Assert.Equal(TimeSpan.FromMilliseconds(20), opt.FactorySoftTimeout); |
|
Assert.Equal(TimeSpan.FromMilliseconds(3000), opt.FactoryHardTimeout); |
|
Assert.Equal(TimeSpan.FromSeconds(0.5), opt.DistributedCacheSoftTimeout); |
|
Assert.Equal(TimeSpan.FromSeconds(1.5), opt.DistributedCacheHardTimeout); |
|
Assert.False(opt.AllowBackgroundDistributedCacheOperations); |
|
Assert.Equal(TimeSpan.FromSeconds(5), opt.JitterMaxDuration); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_DefaultSettings_ConfiguresExpectedValues() |
|
{ |
|
_services.AddExtendedCache(_cacheName, _globalSettings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
var opt = cache.DefaultEntryOptions; |
|
|
|
Assert.Equal(TimeSpan.FromMinutes(30), opt.Duration); |
|
Assert.True(opt.IsFailSafeEnabled); |
|
Assert.Equal(TimeSpan.FromHours(2), opt.FailSafeMaxDuration); |
|
Assert.Equal(TimeSpan.FromSeconds(30), opt.FailSafeThrottleDuration); |
|
Assert.Equal(0.9f, opt.EagerRefreshThreshold); |
|
Assert.Equal(TimeSpan.FromMilliseconds(100), opt.FactorySoftTimeout); |
|
Assert.Equal(TimeSpan.FromMilliseconds(1500), opt.FactoryHardTimeout); |
|
Assert.Equal(TimeSpan.FromSeconds(1), opt.DistributedCacheSoftTimeout); |
|
Assert.Equal(TimeSpan.FromSeconds(2), opt.DistributedCacheHardTimeout); |
|
Assert.True(opt.AllowBackgroundDistributedCacheOperations); |
|
Assert.Equal(TimeSpan.FromSeconds(2), opt.JitterMaxDuration); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_DisabledDistributedCache_DoesNotRegisterBackplaneOrRedis() |
|
{ |
|
var settings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
EnableDistributedCache = false, |
|
}; |
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
|
|
Assert.False(cache.HasDistributedCache); |
|
Assert.False(cache.HasBackplane); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_EmptyCacheName_DoesNothing() |
|
{ |
|
_services.AddExtendedCache(string.Empty, _globalSettings); |
|
|
|
var regs = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList(); |
|
Assert.Empty(regs); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetKeyedService<IFusionCache>(_cacheName); |
|
Assert.Null(cache); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_MultipleCalls_OnlyAddsOneCacheService() |
|
{ |
|
var settings = CreateGlobalSettings(new() |
|
{ |
|
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" } |
|
}); |
|
|
|
// Provide a multiplexer (shared) |
|
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>()); |
|
|
|
_services.AddExtendedCache(_cacheName, settings); |
|
_services.AddExtendedCache(_cacheName, settings); |
|
_services.AddExtendedCache(_cacheName, settings); |
|
|
|
var regs = _services.Where(s => s.ServiceType == typeof(IFusionCache)).ToList(); |
|
Assert.Single(regs); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
Assert.NotNull(cache); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_MultipleDifferentCaches_AddsAll() |
|
{ |
|
_services.AddExtendedCache("Cache1", _globalSettings); |
|
_services.AddExtendedCache("Cache2", _globalSettings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
|
|
var cache1 = provider.GetRequiredKeyedService<IFusionCache>("Cache1"); |
|
var cache2 = provider.GetRequiredKeyedService<IFusionCache>("Cache2"); |
|
|
|
Assert.NotNull(cache1); |
|
Assert.NotNull(cache2); |
|
Assert.NotSame(cache1, cache2); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_WithRedis_EnablesDistributedCacheAndBackplane() |
|
{ |
|
var settings = CreateGlobalSettings(new() |
|
{ |
|
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }, |
|
{ "GlobalSettings:DistributedCache:DefaultExtendedCache:UseSharedDistributedCache", "true" } |
|
}); |
|
|
|
// Provide a multiplexer (shared) |
|
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>()); |
|
|
|
_services.AddExtendedCache(_cacheName, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
|
|
Assert.True(cache.HasDistributedCache); |
|
Assert.True(cache.HasBackplane); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_InvalidRedisConnection_LogsAndThrows() |
|
{ |
|
var settings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
UseSharedDistributedCache = false, |
|
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "invalid:9999" } |
|
}; |
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
Assert.Throws<RedisConnectionException>(() => |
|
{ |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
// Trigger lazy initialization |
|
cache.GetOrDefault<string>("test"); |
|
}); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_WithExistingRedis_UsesExistingDistributedCacheAndBackplane() |
|
{ |
|
var settings = CreateGlobalSettings(new() |
|
{ |
|
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" }, |
|
}); |
|
|
|
_services.AddSingleton(Substitute.For<IConnectionMultiplexer>()); |
|
_services.AddSingleton(Substitute.For<IDistributedCache>()); |
|
|
|
_services.AddExtendedCache(_cacheName, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
|
|
Assert.True(cache.HasDistributedCache); |
|
Assert.True(cache.HasBackplane); |
|
|
|
var existingCache = provider.GetRequiredService<IDistributedCache>(); |
|
Assert.NotNull(existingCache); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_NoRedis_DisablesDistributedCacheAndBackplane() |
|
{ |
|
_services.AddExtendedCache(_cacheName, _globalSettings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
|
|
Assert.False(cache.HasDistributedCache); |
|
Assert.False(cache.HasBackplane); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_NoSharedRedisButNoConnectionString_DisablesDistributedCacheAndBackplane() |
|
{ |
|
var settings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
UseSharedDistributedCache = false, |
|
// No Redis connection string |
|
}; |
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
|
|
Assert.False(cache.HasDistributedCache); |
|
Assert.False(cache.HasBackplane); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_KeyedRedis_UsesSeparateMultiplexers() |
|
{ |
|
var settingsA = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
EnableDistributedCache = true, |
|
UseSharedDistributedCache = false, |
|
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" } |
|
}; |
|
var settingsB = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
EnableDistributedCache = true, |
|
UseSharedDistributedCache = false, |
|
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6380" } |
|
}; |
|
|
|
_services.AddKeyedSingleton("CacheA", Substitute.For<IConnectionMultiplexer>()); |
|
_services.AddKeyedSingleton("CacheB", Substitute.For<IConnectionMultiplexer>()); |
|
|
|
_services.AddExtendedCache("CacheA", _globalSettings, settingsA); |
|
_services.AddExtendedCache("CacheB", _globalSettings, settingsB); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var muxA = provider.GetRequiredKeyedService<IConnectionMultiplexer>("CacheA"); |
|
var muxB = provider.GetRequiredKeyedService<IConnectionMultiplexer>("CacheB"); |
|
|
|
Assert.NotNull(muxA); |
|
Assert.NotNull(muxB); |
|
Assert.NotSame(muxA, muxB); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_WithExistingKeyedDistributedCache_ReusesIt() |
|
{ |
|
var existingCache = Substitute.For<IDistributedCache>(); |
|
_services.AddKeyedSingleton<IDistributedCache>(_cacheName, existingCache); |
|
|
|
var settings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
UseSharedDistributedCache = false, |
|
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" } |
|
}; |
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var resolved = provider.GetRequiredKeyedService<IDistributedCache>(_cacheName); |
|
|
|
Assert.Same(existingCache, resolved); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_SharedNonRedisCache_UsesDistributedCacheWithoutBackplane() |
|
{ |
|
var settings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
UseSharedDistributedCache = true, |
|
EnableDistributedCache = true, |
|
// No Redis.ConnectionString |
|
}; |
|
|
|
// Register non-Redis distributed cache |
|
_services.AddSingleton(Substitute.For<IDistributedCache>()); |
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
|
|
Assert.True(cache.HasDistributedCache); |
|
Assert.False(cache.HasBackplane); // No backplane for non-Redis |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_SharedRedisWithMockedMultiplexer_ReusesExistingMultiplexer() |
|
{ |
|
// Override GlobalSettings to include Redis connection string |
|
var globalSettings = CreateGlobalSettings(new() |
|
{ |
|
{ "GlobalSettings:DistributedCache:Redis:ConnectionString", "localhost:6379" } |
|
}); |
|
|
|
// Custom settings for this cache |
|
var settings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
UseSharedDistributedCache = true, |
|
EnableDistributedCache = true, |
|
}; |
|
|
|
// Pre-register mocked multiplexer (simulates AddDistributedCache already called) |
|
var mockMultiplexer = Substitute.For<IConnectionMultiplexer>(); |
|
_services.AddSingleton(mockMultiplexer); |
|
|
|
_services.AddExtendedCache(_cacheName, globalSettings, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
|
|
Assert.True(cache.HasDistributedCache); |
|
Assert.True(cache.HasBackplane); |
|
|
|
// Verify same multiplexer was reused (TryAdd didn't replace it) |
|
var resolvedMux = provider.GetRequiredService<IConnectionMultiplexer>(); |
|
Assert.Same(mockMultiplexer, resolvedMux); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_KeyedNonRedisCache_UsesKeyedDistributedCacheWithoutBackplane() |
|
{ |
|
var settings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
UseSharedDistributedCache = false, |
|
EnableDistributedCache = true, |
|
// No Redis.ConnectionString |
|
}; |
|
|
|
// Register keyed non-Redis distributed cache |
|
_services.AddKeyedSingleton(_cacheName, Substitute.For<IDistributedCache>()); |
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
|
|
Assert.True(cache.HasDistributedCache); |
|
Assert.False(cache.HasBackplane); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_KeyedRedisWithConnectionString_CreatesIsolatedInfrastructure() |
|
{ |
|
var settings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
UseSharedDistributedCache = false, |
|
EnableDistributedCache = true, |
|
Redis = new GlobalSettings.ConnectionStringSettings |
|
{ |
|
ConnectionString = "localhost:6379" |
|
} |
|
}; |
|
|
|
// Pre-register mocked keyed multiplexer to avoid connection attempt |
|
_services.AddKeyedSingleton(_cacheName, Substitute.For<IConnectionMultiplexer>()); |
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
|
|
Assert.True(cache.HasDistributedCache); |
|
Assert.True(cache.HasBackplane); |
|
|
|
// Verify keyed services exist |
|
var keyedMux = provider.GetRequiredKeyedService<IConnectionMultiplexer>(_cacheName); |
|
Assert.NotNull(keyedMux); |
|
var keyedRedis = provider.GetRequiredKeyedService<IDistributedCache>(_cacheName); |
|
Assert.NotNull(keyedRedis); |
|
var keyedBackplane = provider.GetRequiredKeyedService<IFusionCacheBackplane>(_cacheName); |
|
Assert.NotNull(keyedBackplane); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_NoDistributedCacheRegistered_WorksWithMemoryOnly() |
|
{ |
|
var settings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
UseSharedDistributedCache = true, |
|
EnableDistributedCache = true, |
|
// No Redis connection string, no IDistributedCache registered |
|
// This is technically a misconfiguration, but we handle it without failing |
|
}; |
|
|
|
_services.AddExtendedCache(_cacheName, _globalSettings, settings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
var cache = provider.GetRequiredKeyedService<IFusionCache>(_cacheName); |
|
|
|
Assert.False(cache.HasDistributedCache); |
|
Assert.False(cache.HasBackplane); |
|
|
|
// Verify L1 memory cache still works |
|
cache.Set("key", "value"); |
|
var result = cache.GetOrDefault<string>("key"); |
|
Assert.Equal("value", result); |
|
} |
|
|
|
[Fact] |
|
public void AddExtendedCache_MultipleKeyedCachesWithDifferentTypes_EachHasCorrectConfig() |
|
{ |
|
var redisSettings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
UseSharedDistributedCache = false, |
|
EnableDistributedCache = true, |
|
Redis = new GlobalSettings.ConnectionStringSettings { ConnectionString = "localhost:6379" } |
|
}; |
|
|
|
var nonRedisSettings = new GlobalSettings.ExtendedCacheSettings |
|
{ |
|
UseSharedDistributedCache = false, |
|
EnableDistributedCache = true, |
|
// No Redis connection string |
|
}; |
|
|
|
// Setup Cache1 (Redis) |
|
_services.AddKeyedSingleton("Cache1", Substitute.For<IConnectionMultiplexer>()); |
|
_services.AddExtendedCache("Cache1", _globalSettings, redisSettings); |
|
|
|
// Setup Cache2 (non-Redis) |
|
_services.AddKeyedSingleton("Cache2", Substitute.For<IDistributedCache>()); |
|
_services.AddExtendedCache("Cache2", _globalSettings, nonRedisSettings); |
|
|
|
using var provider = _services.BuildServiceProvider(); |
|
|
|
var cache1 = provider.GetRequiredKeyedService<IFusionCache>("Cache1"); |
|
var cache2 = provider.GetRequiredKeyedService<IFusionCache>("Cache2"); |
|
|
|
Assert.True(cache1.HasDistributedCache); |
|
Assert.True(cache1.HasBackplane); |
|
|
|
Assert.True(cache2.HasDistributedCache); |
|
Assert.False(cache2.HasBackplane); |
|
|
|
Assert.NotSame(cache1, cache2); |
|
} |
|
|
|
private static GlobalSettings CreateGlobalSettings(Dictionary<string, string?> data) |
|
{ |
|
var config = new ConfigurationBuilder() |
|
.AddInMemoryCollection(data) |
|
.Build(); |
|
|
|
var settings = new GlobalSettings(); |
|
config.GetSection("GlobalSettings").Bind(settings); |
|
return settings; |
|
} |
|
}
|
|
|