using System.Threading.Tasks; using DotNet.Testcontainers.Builders; using Jellyfin.Database.Implementations; using Jellyfin.Database.Implementations.DbConfiguration; using Jellyfin.Database.Implementations.Locking; using Jellyfin.Database.Providers.PostgreSQL; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging.Abstractions; using Npgsql; using Testcontainers.PostgreSql; using Xunit; namespace Jellyfin.Database.Tests.PostgreSQL; /// /// Integration tests that validate PostgreSQL migrations against a real container. /// [Xunit.Trait("Category", "RequiresDocker")] public sealed class PostgreSqlMigrationTests : IAsyncLifetime { private readonly PostgreSqlContainer _container; /// /// Initializes a new instance of the class. /// public PostgreSqlMigrationTests() { _container = new PostgreSqlBuilder("postgres:16-alpine") .WithWaitStrategy(Wait.ForUnixContainer().UntilCommandIsCompleted("pg_isready")) .Build(); } /// /// Starts the PostgreSQL container before any tests in the class run. /// /// A representing the asynchronous operation. public async ValueTask InitializeAsync() { await _container.StartAsync().ConfigureAwait(false); } /// /// Stops and removes the PostgreSQL container after all tests in the class have run. /// /// A representing the asynchronous operation. public async ValueTask DisposeAsync() { await _container.DisposeAsync().ConfigureAwait(false); } /// /// Verifies that the InitialPostgreSql migration applies cleanly to a fresh PostgreSQL 16 container. /// /// A representing the asynchronous operation. [Fact] public async Task MigrateAsync_AppliesInitialMigrationCleanly() { await using var dataSource = new NpgsqlDataSourceBuilder(_container.GetConnectionString()).Build(); 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() { // Use a dummy connection string; HasPendingModelChanges() is a purely in-memory check // that compares the current compiled model with the migration snapshots — no real DB needed. const string dummyConnectionString = "Host=localhost;Database=jellyfin;Username=postgres;Password=postgres"; using var dataSource = new NpgsqlDataSourceBuilder(dummyConnectionString).Build(); 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)); } }