using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using StackExchange.Redis; using Tiku.Application.Security; using Tiku.Application.Tenancy; using Tiku.Infrastructure; using Tiku.Infrastructure.Caching; using Tiku.Infrastructure.Tenancy; using ZiggyCreatures.Caching.Fusion; namespace Tiku.IntegrationTests; public sealed class BusinessCachingTests { [Fact] public async Task Local_cache_coalesces_concurrent_factories_and_uses_safe_defaults() { await using var provider = BuildLocalProvider("Integration Tests"); var cache = provider.GetRequiredKeyedService( BusinessCachingServiceCollectionExtensions.CacheName); var options = provider.GetRequiredService>() .Get(BusinessCachingServiceCollectionExtensions.CacheName); var calls = 0; var releaseFactory = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var tasks = Enumerable.Range(0, 16).Select(_ => cache.GetOrSetAsync( "stampede", async cancellationToken => { Interlocked.Increment(ref calls); await releaseFactory.Task.WaitAsync(cancellationToken); return 42; }, entry => entry.Duration = TimeSpan.FromMinutes(1)) .AsTask()).ToArray(); Assert.True(SpinWait.SpinUntil(() => Volatile.Read(ref calls) > 0, TimeSpan.FromSeconds(2))); releaseFactory.SetResult(); Assert.All(await Task.WhenAll(tasks), value => Assert.Equal(42, value)); Assert.Equal(1, calls); Assert.Equal(BusinessCachingServiceCollectionExtensions.CacheName, cache.CacheName); Assert.Equal("tiku:integration-tests:business:v2:", options.CacheKeyPrefix); Assert.Equal("tiku:integration-tests:business:v2", options.BackplaneChannelPrefix); Assert.False(cache.DefaultEntryOptions.IsFailSafeEnabled); Assert.False(cache.DefaultEntryOptions.AllowBackgroundDistributedCacheOperations); Assert.False(cache.DefaultEntryOptions.AllowBackgroundBackplaneOperations); } [Fact] public async Task Unavailable_redis_falls_back_to_the_value_factory() { await using var provider = BuildRedisProvider( "localhost:6399,connectTimeout=200,syncTimeout=200,asyncTimeout=200,abortConnect=false", $"business-unavailable-{Guid.NewGuid():N}"); var cache = provider.GetRequiredKeyedService( BusinessCachingServiceCollectionExtensions.CacheName); var value = await cache.GetOrSetAsync( "fallback", _ => Task.FromResult(42), entry => entry.Duration = TimeSpan.FromMinutes(1)); Assert.Equal(42, value); } [Fact] public async Task Tenant_directory_caches_negative_results() { await using var factory = new Api.ApiTestFactory(); _ = factory.Services; using var scope = factory.Services.CreateScope(); var directory = scope.ServiceProvider.GetRequiredService(); var cache = scope.ServiceProvider.GetRequiredKeyedService( BusinessCachingServiceCollectionExtensions.CacheName); var host = $"missing-{Guid.NewGuid():N}.example.test"; Assert.Null(await directory.FindByHostAsync(host)); Assert.Null(await directory.FindByHostAsync(host)); var cached = await cache.TryGetAsync(TenantDirectory.CacheKey("host", host)); Assert.True(cached.HasValue); Assert.False(cached.Value.Found); Assert.Null(cached.Value.Entry); } [Fact] public async Task Redis_backplane_removes_another_nodes_local_entry() { var connectionString = Environment.GetEnvironmentVariable("TIKU_TEST_REDIS"); if (string.IsNullOrWhiteSpace(connectionString)) return; var environment = $"business-backplane-{Guid.NewGuid():N}"; await using var first = BuildRedisProvider(connectionString, environment); await using var second = BuildRedisProvider(connectionString, environment); var firstCache = first.GetRequiredKeyedService( BusinessCachingServiceCollectionExtensions.CacheName); var secondCache = second.GetRequiredKeyedService( BusinessCachingServiceCollectionExtensions.CacheName); const string key = "shared-entry"; try { await firstCache.SetAsync(key, 1, entry => entry.Duration = TimeSpan.FromMinutes(1)); Assert.Equal(1, await secondCache.GetOrSetAsync( key, _ => Task.FromResult(99), entry => entry.Duration = TimeSpan.FromMinutes(1))); await firstCache.RemoveAsync(key); var reloaded = await secondCache.GetOrSetAsync( key, _ => Task.FromResult(2), entry => entry.Duration = TimeSpan.FromMinutes(1)); Assert.Equal(2, reloaded); } finally { var multiplexer = first.GetRequiredService(); var server = multiplexer.GetServer(multiplexer.GetEndPoints().Single()); var keys = server.Keys(pattern: $"tiku:{environment}:business:v2:*").ToArray(); if (keys.Length > 0) await multiplexer.GetDatabase().KeyDeleteAsync(keys); } } private static ServiceProvider BuildLocalProvider(string environment) { var services = new ServiceCollection(); services.AddLogging(); services.AddBusinessCaching(environment, false); return services.BuildServiceProvider(); } private static ServiceProvider BuildRedisProvider(string connectionString, string environment) { var services = new ServiceCollection(); services.AddLogging(); services.Configure(_ => { }); services.AddRedisSecurity(connectionString, environment); services.AddBusinessCaching(environment, true); return services.BuildServiceProvider(); } }