using JobTrackerApi.Data; using JobTrackerApi.Services; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging.Abstractions; using Moq; using Pomelo.EntityFrameworkCore.MySql.Infrastructure.Internal; using Xunit; namespace JobTrackerApi.Tests; public sealed class DatabaseConfigurationTests { private const string UnreachableMariaDb = "Server=127.0.0.1;Port=1;Database=jt_config_test;User=config_test;Password=synthetic;Connection Timeout=1"; private static IConfiguration Config(string? provider = null, string? connection = null, string? version = null) => new ConfigurationBuilder().AddInMemoryCollection(new Dictionary { ["Database:Provider"] = provider, ["ConnectionStrings:JobTracker"] = connection, ["Database:ServerVersion"] = version, }).Build(); [Fact] public void Default_registration_uses_pinned_MariaDb_without_connecting_to_a_server() { var configuration = Config(connection: UnreachableMariaDb); var options = new DbContextOptionsBuilder(); DatabaseConfiguration.Configure(options, configuration, "unused.db"); Assert.Equal("mariadb", DatabaseConfiguration.GetProvider(configuration)); var relational = Assert.Single(options.Options.Extensions.OfType()); Assert.Equal(UnreachableMariaDb, relational.ConnectionString); // Inspect the provider's configured version without opening a connection or generating SQL. #pragma warning disable EF1001 var mysql = Assert.IsType(relational); Assert.IsType(mysql.ServerVersion); Assert.Equal(new Version(12, 2, 2), mysql.ServerVersion!.Version); #pragma warning restore EF1001 } [Theory] [InlineData(null)] [InlineData("mariadb")] [InlineData("mysql")] public void Missing_MariaDb_connection_fails_closed_instead_of_creating_a_Sqlite_database(string? provider) { var options = new DbContextOptionsBuilder(); Assert.Throws(() => DatabaseConfiguration.Configure(options, Config(provider), "must-not-create.db")); Assert.Empty(options.Options.Extensions.OfType()); } [Theory] [InlineData("postgres")] [InlineData("")] [InlineData(" ")] [InlineData("maria-db")] public void Unknown_provider_is_rejected_even_when_a_connection_is_configured(string provider) { Assert.Throws(() => DatabaseConfiguration.Configure( new DbContextOptionsBuilder(), Config(provider, "Data Source=must-not-create.db"), "unused.db")); Assert.Throws(() => DatabaseConfiguration.GetProvider(Config(provider))); } [Theory] [InlineData("mariadb")] [InlineData(" MYSQL ")] public void MariaDb_server_version_is_configurable_without_auto_detection(string provider) { var options = new DbContextOptionsBuilder(); DatabaseConfiguration.Configure(options, Config(provider, UnreachableMariaDb, "11.4.10"), "unused.db"); #pragma warning disable EF1001 Assert.Equal(new Version(11, 4, 10), options.Options.Extensions.OfType().Single().ServerVersion!.Version); #pragma warning restore EF1001 } [Fact] public void Invalid_server_version_fails_during_configuration() { Assert.Throws(() => DatabaseConfiguration.Configure( new DbContextOptionsBuilder(), Config("mariadb", UnreachableMariaDb, "latest"), "unused.db")); } [Theory] [InlineData(null, "Data Source=preserved.db")] [InlineData("Data Source=explicit.db", "Data Source=explicit.db")] public void Explicit_Sqlite_preserves_existing_file_selection(string? connection, string expected) { var options = new DbContextOptionsBuilder(); DatabaseConfiguration.Configure(options, Config(" SQLITE ", connection), "preserved.db"); var relational = Assert.Single(options.Options.Extensions.OfType()); Assert.Equal("SqliteOptionsExtension", relational.GetType().Name); Assert.Equal(expected, relational.ConnectionString); } [Fact] public async Task Default_MariaDb_backup_selection_cannot_run_the_Sqlite_backup_path() { var root = Path.Combine(Path.GetTempPath(), $"jt-config-backup-{Guid.NewGuid():N}"); var configuration = new ConfigurationBuilder().AddConfiguration(Config(connection: UnreachableMariaDb)) .AddInMemoryCollection(new Dictionary { ["Data:Root"] = root }).Build(); var environment = new Mock(); environment.SetupGet(x => x.ContentRootPath).Returns(root); try { var runner = new SqliteDatabaseBackupRunner(configuration, new AppPaths(configuration, environment.Object), NullLogger.Instance); Assert.False(runner.IsSupported); Assert.Null(await runner.RunOnceAsync(default)); Assert.False(Directory.Exists(runner.BackupsRoot)); } finally { if (Directory.Exists(root)) Directory.Delete(root, true); } } }