using System; using System.Collections.Generic; using System.Linq; using Emby.Server.Implementations.ScheduledTasks; using Jellyfin.Server.Extensions; using MediaBrowser.Controller.ScheduledTasks; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; namespace Jellyfin.Server.Tests.ScanLeader; /// /// Without gating every replica runs the timer-driven library tasks, which is what the lease exists /// to prevent, and nothing in the running server reveals which way the switch went. Both the default /// and the startup log line are pinned here. /// public sealed class ScanLeaderRegistrationTests { private const string RedisKey = "Jellyfin:TranscodeStore:RedisConnectionString"; [Fact] public void AddScanLeaderLease_Should_Gate_By_Default_When_Redis_Is_Configured() { var services = new ServiceCollection(); var logger = new LogRecorder(); services.AddScanLeaderLease(Configuration(redis: "valkey-cheeztv-valkey:6379,abortConnect=false"), logger); Assert.Equal(typeof(RedisScanLeaderLease), LeaseImplementation(services)); Assert.True(EffectiveOptions(services).Enabled); Assert.True(logger.Has(LogLevel.Information, "active")); } [Fact] public void AddScanLeaderLease_Should_Not_Gate_Without_Redis() { var services = new ServiceCollection(); var logger = new LogRecorder(); services.AddScanLeaderLease(Configuration(redis: null), logger); Assert.Equal(typeof(NullScanLeaderLease), LeaseImplementation(services)); Assert.False(EffectiveOptions(services).Enabled); Assert.True(logger.Has(LogLevel.Information, "off")); } [Fact] public void AddScanLeaderLease_Should_Honour_Explicit_Opt_Out() { var services = new ServiceCollection(); var logger = new LogRecorder(); services.AddScanLeaderLease(Configuration(redis: "valkey:6379", enabled: "false"), logger); Assert.Equal(typeof(NullScanLeaderLease), LeaseImplementation(services)); Assert.False(EffectiveOptions(services).Enabled); Assert.DoesNotContain(logger.Entries, entry => entry.Level >= LogLevel.Warning); } [Fact] public void AddScanLeaderLease_Should_Warn_When_Enabled_Without_Redis() { var services = new ServiceCollection(); var logger = new LogRecorder(); services.AddScanLeaderLease(Configuration(redis: null, enabled: "true"), logger); Assert.Equal(typeof(NullScanLeaderLease), LeaseImplementation(services)); Assert.False(EffectiveOptions(services).Enabled); Assert.True(logger.Has(LogLevel.Warning, "cannot run")); } [Fact] public void AddScanLeaderLease_Should_Keep_Bound_Options() { var services = new ServiceCollection(); var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { [RedisKey] = "valkey:6379", ["Jellyfin:ScanLeader:LeaseDurationSeconds"] = "90" }) .Build(); services.AddScanLeaderLease(configuration, new LogRecorder()); var options = EffectiveOptions(services); Assert.Equal(90, options.LeaseDurationSeconds); Assert.Contains("RefreshLibrary", options.GatedTaskKeys); } private static IConfiguration Configuration(string? redis, string? enabled = null) => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { [RedisKey] = redis, [ScanLeaderOptions.EnabledKey] = enabled }) .Build(); private static Type? LeaseImplementation(IServiceCollection services) => services.Single(descriptor => descriptor.ServiceType == typeof(IScanLeaderLease)).ImplementationType; private static ScanLeaderOptions EffectiveOptions(IServiceCollection services) { using var provider = services.BuildServiceProvider(); return provider.GetRequiredService>().Value; } private sealed class LogRecorder : ILogger { private readonly List<(LogLevel Level, string Message)> _entries = new(); public IReadOnlyList<(LogLevel Level, string Message)> Entries => _entries; public IDisposable BeginScope(TState state) where TState : notnull => NoopScope.Instance; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { ArgumentNullException.ThrowIfNull(formatter); _entries.Add((logLevel, formatter(state, exception))); } public bool Has(LogLevel level, string substring) => _entries.Any(entry => entry.Level == level && entry.Message.Contains(substring, StringComparison.Ordinal)); private sealed class NoopScope : IDisposable { public static readonly NoopScope Instance = new(); public void Dispose() { } } } }