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.
96 lines
3.8 KiB
C#
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.");
|
|
}
|
|
}
|