using System; using System.Threading; using System.Threading.Tasks; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.DbConfiguration; using Jellyfin.Database.Implementations.Locking; using Jellyfin.Database.Providers.PostgreSQL; using Jellyfin.Server.Tests.Migrations; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Npgsql; using Xunit; namespace Jellyfin.Database.Tests.PostgreSQL; /// /// Integration tests that validate PostgreSQL migrations against a real server. /// [Xunit.Trait("Category", "RequiresDocker")] public sealed class PostgreSqlMigrationTests : IAsyncLifetime { private static int _databaseSequence; private PostgreSqlTestServer? _server; private NpgsqlDataSource? _dataSource; /// /// Attaches to the test server and hands this test an empty database of its own. /// /// A representing the asynchronous operation. public async ValueTask InitializeAsync() { _server = await PostgreSqlTestServer.StartAsync().ConfigureAwait(false); var databaseName = FormattableString.Invariant($"pg_migration_{Interlocked.Increment(ref _databaseSequence)}"); var connectionString = await _server.CreateDatabaseAsync(databaseName, TestContext.Current.CancellationToken).ConfigureAwait(false); _dataSource = new NpgsqlDataSourceBuilder(connectionString).Build(); } /// /// Releases the data source and the test server. /// /// A representing the asynchronous operation. public async ValueTask DisposeAsync() { // InitializeAsync can fail before the data source exists; its error must not be masked by an NRE here. if (_dataSource is not null) { await _dataSource.DisposeAsync().ConfigureAwait(false); } if (_server is not null) { await _server.DisposeAsync().ConfigureAwait(false); } } /// /// Verifies that the InitialPostgreSql migration applies cleanly to a fresh database. /// /// A representing the asynchronous operation. [Fact] public async Task MigrateAsync_AppliesInitialMigrationCleanly() { var context = CreateContext(_dataSource!); await using (context) { await context.Database.MigrateAsync(TestContext.Current.CancellationToken); var pendingMigrations = await context.Database.GetPendingMigrationsAsync(TestContext.Current.CancellationToken); Assert.Empty(pendingMigrations); } } /// /// Verifies that no pending model changes exist for the PostgreSQL provider, /// acting as a CI gate that fails when model changes are added without a corresponding migration. /// [Fact] public void CheckForUnappliedMigrations_PostgreSql() { using var context = CreateContext(_dataSource!); Assert.False( context.Database.HasPendingModelChanges(), "There are unapplied changes to the EFCore model for PostgreSQL. Please create a Migration."); } private static JellyfinDbContext CreateContext(NpgsqlDataSource dataSource) { var optionsBuilder = new DbContextOptionsBuilder(); var provider = new PostgreSqlDatabaseProvider(dataSource); provider.Initialise(optionsBuilder, new DatabaseConfigurationOptions { DatabaseType = "PostgreSQL" }); return new JellyfinDbContext( optionsBuilder.Options, NullLogger.Instance, provider, new NoLockBehavior(NullLogger.Instance)); } }