using System; using System.Collections.Generic; using System.Linq; using Emby.Server.Implementations.MediaEncoding; using Jellyfin.Server.Extensions; using MediaBrowser.Controller.MediaEncoding; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Xunit; namespace Jellyfin.Server.Tests.HighAvailability; /// /// Which transcode session store was selected is invisible at runtime — the Redis client does not /// abort on an unreachable server and every call site swallows failures — so the selection is /// asserted here together with the startup log line that reports it. /// public sealed class TranscodeStoreRegistrationTests { [Fact] public void AddTranscodeSessionStore_Should_Select_RedisStore_From_UnprefixedEnvironmentKeyShape() { var services = new ServiceCollection(); var logger = new RecordingLogger(); services.AddTranscodeSessionStore(BuildConfiguration("valkey-cheeztv-valkey:6379,abortConnect=false"), logger); Assert.Equal(typeof(RedisTranscodeSessionStore), StoreImplementation(services)); Assert.True(logger.HasEntry(LogLevel.Information, nameof(RedisTranscodeSessionStore))); } [Fact] public void AddTranscodeSessionStore_Should_Select_NullStore_Without_ConnectionString() { var services = new ServiceCollection(); var logger = new RecordingLogger(); services.AddTranscodeSessionStore(BuildConfiguration(null), logger); Assert.Equal(typeof(NullTranscodeSessionStore), StoreImplementation(services)); Assert.True(logger.HasEntry(LogLevel.Information, nameof(NullTranscodeSessionStore))); } [Fact] public void AddTranscodeSessionStore_Should_Log_Endpoints_Without_Password() { var services = new ServiceCollection(); var logger = new RecordingLogger(); services.AddTranscodeSessionStore(BuildConfiguration("valkey:6379,password=hunter2"), logger); var message = Assert.Single(logger.Entries, entry => entry.Level == LogLevel.Information).Message; Assert.Contains("valkey:6379", message, StringComparison.Ordinal); Assert.DoesNotContain("hunter2", message, StringComparison.Ordinal); } [Fact] public void AddTranscodeSessionStore_Should_Register_ConnectivityProbe_Only_With_Redis() { var withRedis = new ServiceCollection(); withRedis.AddTranscodeSessionStore(BuildConfiguration("valkey:6379"), new RecordingLogger()); var withoutRedis = new ServiceCollection(); withoutRedis.AddTranscodeSessionStore(BuildConfiguration(null), new RecordingLogger()); Assert.Contains(withRedis, descriptor => descriptor.ImplementationType == typeof(TranscodeStoreConnectivityProbe)); Assert.DoesNotContain(withoutRedis, descriptor => descriptor.ServiceType == typeof(IHostedService)); } [Fact] public void AddTranscodeSessionStore_Should_Bind_Options() { var services = new ServiceCollection(); var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { [TranscodeStoreOptions.RedisConnectionStringKey] = "valkey:6379", ["Jellyfin:TranscodeStore:LeaseDurationSeconds"] = "45" }) .Build(); services.AddTranscodeSessionStore(configuration, new RecordingLogger()); using var provider = services.BuildServiceProvider(); var options = provider.GetRequiredService>().Value; Assert.Equal(45, options.LeaseDurationSeconds); Assert.Equal("valkey:6379", options.RedisConnectionString); } private static IConfiguration BuildConfiguration(string? redisConnectionString) => new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { [TranscodeStoreOptions.RedisConnectionStringKey] = redisConnectionString }) .Build(); private static Type? StoreImplementation(IServiceCollection services) => services.Single(descriptor => descriptor.ServiceType == typeof(ITranscodeSessionStore)).ImplementationType; }