Files
slp-modular-cms/src/SlpModularCms.Core.Tests/Hosting/DatabaseMigrationExtensionsTests.cs
T
Sluijsens 33d18ebbf8 Switches the database from SQL Server to MariaDB
The target Pi only has MariaDB, and SQL Server has no ARM64 build at
all - not a config problem, a real gap discovered during deployment
setup. Swapped the EF Core provider, regenerated every migration,
updated connection strings and the backup script everywhere they
appear.

Took two tries to land on a provider that actually works: Pomelo
builds fine against this project's EF Core 10 packages but fails at
runtime (it's compiled against 9's internal API surface, which moved
in 10 wherever Identity/DataProtection force the newer packages).
Oracle's official provider builds and migrates fine but has a real
MariaDB bug in its own migration-lock code, reproduced against a live
database. Kept Oracle's provider and worked around just that one
broken method - everything else it does is correct - rather than
give up more of the stack to chase a workaround.

Verified against a real local MariaDB end to end: all three
migrations applied, both hosts start clean, full suite still green.
2026-07-29 11:59:09 +02:00

96 lines
3.8 KiB
C#

using FluentAssertions;
using MySql.Data.MySqlClient;
using SlpModularCms.Core.Hosting;
using Xunit;
namespace SlpModularCms.Core.Tests.Hosting;
/// <summary>
/// Guards the failure classification of the startup migration.
/// </summary>
/// <remarks>
/// Startup migration distinguishes two failures that mean very different things:
/// a database that is not up yet (normal when the application and the database server start
/// together after a host reboot) versus a migration that is broken. Retrying the first is
/// correct; retrying the second only delays the inevitable and fills the log.
///
/// <c>MigrateCoreDatabase</c> itself needs a composed <c>WebApplication</c>, so the classifier is
/// exercised directly here; the composed startup path is verified at the phase-level Build and
/// Test stage by starting both hosts.
/// </remarks>
public class DatabaseMigrationExtensionsTests
{
[Fact]
public void MaxConnectionAttempts_ShouldAllowForAHostRebootWindow()
{
// Enough attempts that a database server starting alongside the application is tolerated,
// few enough that a genuinely unreachable database still fails promptly.
DatabaseMigrationExtensions.MaxConnectionAttempts.Should().BeGreaterThan(1);
DatabaseMigrationExtensions.MaxConnectionAttempts.Should().BeLessThanOrEqualTo(10);
}
[Theory]
[MemberData(nameof(TransientFailures))]
public void IsTransientConnectionFailure_ShouldRecogniseConnectionProblems(Exception exception)
{
InvokeClassifier(exception).Should().BeTrue();
}
[Theory]
[MemberData(nameof(NonTransientFailures))]
public void IsTransientConnectionFailure_ShouldNotRecogniseMigrationProblems(Exception exception)
{
// A broken migration must fail immediately. Classifying it as transient would hide a
// real fault behind a retry loop.
InvokeClassifier(exception).Should().BeFalse();
}
public static TheoryData<Exception> TransientFailures() =>
[
MakeMySqlException(),
new TimeoutException("Connect Timeout expired."),
new InvalidOperationException("wrapper", MakeMySqlException()),
];
public static TheoryData<Exception> NonTransientFailures() =>
[
new InvalidOperationException("There is already an object named 'Users' in the database."),
new NotSupportedException("The migration cannot be applied."),
new AggregateException(new InvalidOperationException("pending model changes")),
];
private static bool InvokeClassifier(Exception exception)
{
var method = typeof(DatabaseMigrationExtensions).GetMethod(
"IsTransientConnectionFailure",
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
method.Should().NotBeNull("the failure classifier is the behaviour under test");
return (bool)method!.Invoke(null, [exception])!;
}
/// <summary>
/// <see cref="MySqlException"/> has no public constructor, so one is produced through the
/// framework's own factory path via reflection.
/// </summary>
private static Exception MakeMySqlException()
{
try
{
// Deliberately unreachable host and a very short timeout: this genuinely produces a
// MySqlException rather than a hand-built stand-in, so the classifier is tested against
// the real type it will encounter in production.
using var connection = new MySqlConnection(
"Server=localhost;Port=9;Database=none;Uid=none;Pwd=none;Connection Timeout=1");
connection.Open();
}
catch (Exception ex)
{
return ex;
}
throw new InvalidOperationException("Expected the connection attempt to fail.");
}
}