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));
}
}