using FluentAssertions;
using MySql.Data.MySqlClient;
using SlpModularCms.Core.Hosting;
using Xunit;
namespace SlpModularCms.Core.Tests.Hosting;
///
/// Guards the failure classification of the startup migration.
///
///
/// 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.
///
/// MigrateCoreDatabase itself needs a composed WebApplication, 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.
///
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 TransientFailures() =>
[
MakeMySqlException(),
new TimeoutException("Connect Timeout expired."),
new InvalidOperationException("wrapper", MakeMySqlException()),
];
public static TheoryData 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])!;
}
///
/// has no public constructor, so one is produced through the
/// framework's own factory path via reflection.
///
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.");
}
}