using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Emby.Server.Implementations; using Emby.Server.Implementations.Configuration; using Emby.Server.Implementations.Serialization; using Jellyfin.Database.Implementations; using Jellyfin.Server.Implementations.DatabaseConfiguration; using Jellyfin.Server.Implementations.Extensions; using Jellyfin.Server.Migrations; using Jellyfin.Server.ServerSetupApp; using MediaBrowser.Common.Configuration; using MediaBrowser.Model.Configuration; using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging.Abstractions; using Xunit; namespace Jellyfin.Server.Tests.Migrations; /// /// Covers how the migration service treats the database of a server that has been set up before. /// public sealed class JellyfinMigrationServiceTests : IDisposable { private readonly string _root; private readonly ServerApplicationPaths _paths; private readonly List _serviceProviders = []; public JellyfinMigrationServiceTests() { _root = Path.Combine(Path.GetTempPath(), "jellyfin-migration-service-tests", Guid.NewGuid().ToString("N")); _paths = new ServerApplicationPaths( Path.Combine(_root, "data"), Path.Combine(_root, "log"), Path.Combine(_root, "config"), Path.Combine(_root, "cache"), Path.Combine(_root, "web")); Directory.CreateDirectory(_paths.DataPath); Directory.CreateDirectory(_paths.LogDirectoryPath); Directory.CreateDirectory(_paths.ConfigurationDirectoryPath); Directory.CreateDirectory(_paths.CachePath); } private string DatabasePath => Path.Combine(_paths.DataPath, "jellyfin.db"); [Fact] public async Task CheckFirstTimeRunOrMigration_SetUpServerWithoutDatabase_ThrowsWithoutCreatingIt() { WriteServerConfiguration(wizardCompleted: true); var service = CreateService(); var systemConfiguration = await File.ReadAllBytesAsync(_paths.SystemConfigurationFilePath, TestContext.Current.CancellationToken); var exception = await Assert.ThrowsAsync(() => service.CheckFirstTimeRunOrMigration(_paths, new StartupOptions())); Assert.Contains("the database does not exist", exception.Message, StringComparison.Ordinal); Assert.Contains(_paths.SystemConfigurationFilePath, exception.Message, StringComparison.Ordinal); Assert.Empty(Directory.GetFiles(_paths.DataPath, "jellyfin.db*")); Assert.Equal(systemConfiguration, await File.ReadAllBytesAsync(_paths.SystemConfigurationFilePath, TestContext.Current.CancellationToken)); } [Fact] public async Task CheckFirstTimeRunOrMigration_SetUpServerWithEmptyDatabaseFile_Throws() { WriteServerConfiguration(wizardCompleted: true); await File.WriteAllBytesAsync(DatabasePath, [], TestContext.Current.CancellationToken); var service = CreateService(); var exception = await Assert.ThrowsAsync(() => service.CheckFirstTimeRunOrMigration(_paths, new StartupOptions())); Assert.Contains("the database has no migration history", exception.Message, StringComparison.Ordinal); } [Fact] public async Task CheckFirstTimeRunOrMigration_SetUpServerWithEmptyHistory_Throws() { WriteServerConfiguration(wizardCompleted: true); var service = CreateService(); await using (var context = await CreateDbContextAsync()) { await context.GetService().CreateIfNotExistsAsync(TestContext.Current.CancellationToken); } var exception = await Assert.ThrowsAsync(() => service.CheckFirstTimeRunOrMigration(_paths, new StartupOptions())); Assert.Contains("the migration history of the database is empty", exception.Message, StringComparison.Ordinal); } [Fact] public async Task CheckFirstTimeRunOrMigration_SetUpServerWithHistory_Passes() { WriteServerConfiguration(wizardCompleted: false); await CreateService().CheckFirstTimeRunOrMigration(_paths, new StartupOptions()); var applied = await GetAppliedMigrationIdsAsync(); WriteServerConfiguration(wizardCompleted: true); await CreateService().CheckFirstTimeRunOrMigration(_paths, new StartupOptions()); Assert.NotEmpty(applied); Assert.Equal(applied, await GetAppliedMigrationIdsAsync()); } [Fact] public async Task CheckFirstTimeRunOrMigration_SeedSystemOnSetUpServerWithoutDatabase_SeedsCodeMigrations() { WriteServerConfiguration(wizardCompleted: true); await CreateService().CheckFirstTimeRunOrMigration(_paths, new StartupOptions { StartupMode = Configuration.StartupMode.SeedSystem }); Assert.NotEmpty(await GetAppliedMigrationIdsAsync()); } [Fact] public async Task CheckFirstTimeRunOrMigration_MigrateSystemOnSetUpServerWithoutDatabase_Passes() { WriteServerConfiguration(wizardCompleted: true); await CreateService().CheckFirstTimeRunOrMigration(_paths, new StartupOptions { StartupMode = Configuration.StartupMode.MigrateSystem }); } [Fact] public async Task CheckFirstTimeRunOrMigration_RestoreArchiveOnSetUpServerWithoutDatabase_Passes() { WriteServerConfiguration(wizardCompleted: true); await CreateService().CheckFirstTimeRunOrMigration(_paths, new StartupOptions { RestoreArchive = Path.Combine(_root, "backup.zip") }); } public void Dispose() { foreach (var serviceProvider in _serviceProviders) { serviceProvider.Dispose(); } SqliteConnection.ClearAllPools(); try { Directory.Delete(_root, true); } catch (IOException) { // Best effort, a locked file must not fail the test. } } private void WriteServerConfiguration(bool wizardCompleted) { new MyXmlSerializer().SerializeToFile(new ServerConfiguration { IsStartupWizardCompleted = wizardCompleted }, _paths.SystemConfigurationFilePath); } private JellyfinMigrationService CreateService() { var configurationManager = new ServerConfigurationManager(_paths, NullLoggerFactory.Instance, new MyXmlSerializer()); configurationManager.AddParts([new DatabaseConfigurationFactory()]); var serviceProvider = new ServiceCollection() .AddLogging() .AddJellyfinDbContext(configurationManager, new ConfigurationBuilder().Build()) .AddSingleton(_paths) .RegisterStartupLogger() .BuildServiceProvider(); _serviceProviders.Add(serviceProvider); var factory = serviceProvider.GetRequiredService>(); serviceProvider.GetRequiredService().DbContextFactory = factory; return ActivatorUtilities.CreateInstance(serviceProvider); } private async Task CreateDbContextAsync() { var factory = _serviceProviders[^1].GetRequiredService>(); return await factory.CreateDbContextAsync(TestContext.Current.CancellationToken); } private async Task GetAppliedMigrationIdsAsync() { await using var context = await CreateDbContextAsync(); var applied = await context.Database.GetAppliedMigrationsAsync(TestContext.Current.CancellationToken); return applied.Order(StringComparer.Ordinal).ToArray(); } }