Files
jobtrackingapp/JobTrackerApi.MariaDb.Tests/MariaDbDatabase.cs
T

131 lines
6.6 KiB
C#

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