fix(database): qualify RD21 saves against pinned MariaDB 12.2.2
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
using System.Net;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Models;
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Diagnostics;
|
||||
using MySqlConnector;
|
||||
using Moq;
|
||||
|
||||
namespace JobTrackerApi.MariaDb.Tests;
|
||||
|
||||
// Each test owns its schema. Never connect contexts to the administrator's selected database.
|
||||
internal sealed class MariaDbDatabase : IAsyncDisposable
|
||||
{
|
||||
private readonly string _adminConnection;
|
||||
private readonly string _root;
|
||||
private bool _created;
|
||||
public string Name { get; } = $"jt_it_{Guid.NewGuid():N}";
|
||||
public string ConnectionString { get; private set; } = "";
|
||||
public MariaDbServerVersion ServerVersion { get; private set; } = null!;
|
||||
public string ActualVersion { get; private set; } = "";
|
||||
public WebApplication App { get; private set; } = null!;
|
||||
|
||||
private MariaDbDatabase(string adminConnection)
|
||||
{
|
||||
_adminConnection = adminConnection;
|
||||
_root = Path.GetFullPath(Path.Combine(Path.GetTempPath(), Name));
|
||||
}
|
||||
|
||||
internal static MySqlConnectionStringBuilder ValidateAdminConnection(string? value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value))
|
||||
throw new InvalidOperationException("Explicit MariaDB test lane requires JOBTRACKER_MARIADB_TEST_ADMIN; no skip or SQLite fallback is allowed.");
|
||||
var builder = new MySqlConnectionStringBuilder(value);
|
||||
if (!(builder.Server.Equals("localhost", StringComparison.OrdinalIgnoreCase)
|
||||
|| IPAddress.TryParse(builder.Server, out var address) && IPAddress.IsLoopback(address)))
|
||||
throw new InvalidOperationException("MariaDB integration tests require a loopback server.");
|
||||
if (builder.ConnectionProtocol != MySqlConnectionProtocol.Sockets)
|
||||
throw new InvalidOperationException("MariaDB integration tests require loopback TCP.");
|
||||
builder.Database = "";
|
||||
builder.Pooling = false;
|
||||
builder.ConnectionTimeout = 5;
|
||||
builder.DefaultCommandTimeout = 30;
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static async Task<MariaDbDatabase> CreateAsync()
|
||||
{
|
||||
var admin = ValidateAdminConnection(Environment.GetEnvironmentVariable("JOBTRACKER_MARIADB_TEST_ADMIN"));
|
||||
var database = new MariaDbDatabase(admin.ConnectionString);
|
||||
try
|
||||
{
|
||||
await using var connection = new MySqlConnection(admin.ConnectionString);
|
||||
await connection.OpenAsync();
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = "SELECT VERSION();";
|
||||
database.ActualVersion = (string)(await command.ExecuteScalarAsync())!;
|
||||
var expected = Environment.GetEnvironmentVariable("JOBTRACKER_MARIADB_TEST_VERSION") ?? "12.2.2";
|
||||
if (!Version.TryParse(expected, out var pin) || pin.Build < 0 || pin.Revision >= 0 || pin.Major < 11)
|
||||
throw new InvalidOperationException("JOBTRACKER_MARIADB_TEST_VERSION must pin a supported MariaDB major.minor.patch version.");
|
||||
if (!database.ActualVersion.StartsWith(expected + "-", StringComparison.Ordinal)
|
||||
|| !database.ActualVersion.Contains("MariaDB", StringComparison.OrdinalIgnoreCase))
|
||||
throw new InvalidOperationException($"Expected MariaDB {expected}; actual server is {database.ActualVersion}.");
|
||||
database.ServerVersion = new MariaDbServerVersion(pin);
|
||||
command.CommandText = $"CREATE DATABASE `{database.Name}` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
database._created = true;
|
||||
admin.Database = database.Name;
|
||||
database.ConnectionString = admin.ConnectionString;
|
||||
var builder = WebApplication.CreateBuilder(new WebApplicationOptions
|
||||
{
|
||||
EnvironmentName = "Production",
|
||||
ApplicationName = typeof(StartupInitializationExtensions).Assembly.GetName().Name,
|
||||
});
|
||||
builder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
|
||||
{
|
||||
["Data:Root"] = database._root,
|
||||
["Database:Provider"] = "mariadb",
|
||||
["ConnectionStrings:JobTracker"] = database.ConnectionString,
|
||||
["Auth:AdminEmail"] = "",
|
||||
["Auth:AdminPassword"] = "",
|
||||
});
|
||||
builder.Logging.ClearProviders();
|
||||
builder.Services.AddSingleton(Mock.Of<ICurrentUserService>(user => user.UserId == "rd21-owner"));
|
||||
builder.Services.AddSingleton<AppPaths>();
|
||||
builder.Services.AddSingleton<IStartupReadiness, StartupReadiness>();
|
||||
builder.Services.AddDbContext<JobTrackerContext>(options => database.Configure(options));
|
||||
builder.Services.AddIdentityCore<ApplicationUser>().AddRoles<IdentityRole>().AddEntityFrameworkStores<JobTrackerContext>();
|
||||
database.App = builder.Build();
|
||||
await database.App.InitializeJobTrackerAsync();
|
||||
return database;
|
||||
}
|
||||
catch
|
||||
{
|
||||
await database.DisposeAsync();
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private void Configure(DbContextOptionsBuilder options) => options.UseMySql(ConnectionString, ServerVersion)
|
||||
.ConfigureWarnings(warnings => warnings.Ignore(RelationalEventId.PendingModelChangesWarning));
|
||||
|
||||
public JobTrackerContext Context(string owner = "rd21-owner", IInterceptor? interceptor = null)
|
||||
{
|
||||
var options = new DbContextOptionsBuilder<JobTrackerContext>();
|
||||
Configure(options);
|
||||
if (interceptor is not null) options.AddInterceptors(interceptor);
|
||||
return new(options.Options, Mock.Of<ICurrentUserService>(user => user.UserId == owner));
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (App is not null) await App.DisposeAsync();
|
||||
if (_created)
|
||||
{
|
||||
await using var connection = new MySqlConnection(_adminConnection);
|
||||
await connection.OpenAsync();
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = $"DROP DATABASE `{Name}`;";
|
||||
await command.ExecuteNonQueryAsync();
|
||||
_created = false;
|
||||
}
|
||||
var temp = Path.GetFullPath(Path.GetTempPath()).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
|
||||
if (!_root.StartsWith(temp, StringComparison.OrdinalIgnoreCase) || Path.GetFileName(_root) != Name)
|
||||
throw new InvalidOperationException("Refusing cleanup outside this fixture's temporary directory.");
|
||||
if (Directory.Exists(_root)) Directory.Delete(_root, recursive: true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user