131 lines
6.6 KiB
C#
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);
|
|
}
|
|
}
|