using System; using System.IO; using System.Threading.Tasks; using Emby.Server.Implementations.MediaEncoding; using Jellyfin.Server; using Jellyfin.Server.Extensions; using MediaBrowser.Common.Configuration; using MediaBrowser.Controller.MediaEncoding; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Moq; using StackExchange.Redis; using Testcontainers.Redis; using Xunit; namespace Jellyfin.Server.Implementations.Tests.MediaEncoding; /// /// Drives the whole configuration path a deployment uses: a bare Jellyfin__TranscodeStore__* /// environment variable, the server's own configuration builder, the store registration, and a /// session round-trip against a real Valkey server. /// [Trait("Category", "RequiresDocker")] public sealed class TranscodeStoreWiringTests : IAsyncLifetime { private const string RedisConnectionStringVariable = "Jellyfin__TranscodeStore__RedisConnectionString"; private readonly RedisContainer _container; private string _configDirectory = string.Empty; /// /// Initializes a new instance of the class. /// public TranscodeStoreWiringTests() { _container = new RedisBuilder("valkey/valkey:8-alpine").Build(); } /// /// Starts Valkey and lays out the configuration directory the server reads at startup. /// /// A representing the asynchronous operation. public async ValueTask InitializeAsync() { await _container.StartAsync().ConfigureAwait(false); _configDirectory = Directory.CreateTempSubdirectory("jellyfin-wiring-test").FullName; await File.WriteAllTextAsync(Path.Combine(_configDirectory, "logging.default.json"), "{}").ConfigureAwait(false); } /// /// Removes the environment variable, configuration directory and container. /// /// A representing the asynchronous operation. public async ValueTask DisposeAsync() { Environment.SetEnvironmentVariable(RedisConnectionStringVariable, null); if (_configDirectory.Length > 0) { Directory.Delete(_configDirectory, true); } await _container.DisposeAsync().ConfigureAwait(false); } /// /// The variable form deployments set selects the Redis store and that store really talks to Valkey. /// /// A representing the asynchronous operation. [Fact] public async Task ManifestStyleEnvironmentVariable_Should_Reach_Valkey() { Environment.SetEnvironmentVariable( RedisConnectionStringVariable, _container.GetConnectionString() + ",abortConnect=false"); var appPaths = new Mock(); appPaths.Setup(paths => paths.ConfigurationDirectoryPath).Returns(_configDirectory); var configuration = Program.CreateAppConfiguration(new StartupOptions(), appPaths.Object); var services = new ServiceCollection(); services.AddLogging(); services.AddTranscodeSessionStore(configuration, NullLogger.Instance); await using var provider = services.BuildServiceProvider(); var store = provider.GetRequiredService(); Assert.IsType(store); var playSessionId = Guid.NewGuid().ToString("N"); await store.SetAsync( TranscodeSession.CreateForPlaylist(playSessionId, "media-1", "pod-a", "/transcodes/abc.m3u8", TimeSpan.FromSeconds(30)), TestContext.Current.CancellationToken); var stored = await store.TryGetAsync(playSessionId, TestContext.Current.CancellationToken); Assert.NotNull(stored); Assert.Equal("pod-a", stored.OwnerPod); var redis = provider.GetRequiredService(); Assert.True(await redis.GetDatabase().KeyExistsAsync("jellyfin:transcode:" + playSessionId)); } }