fix(db): repair fresh migration chain
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
using System.Data;
|
||||
using System.Text.RegularExpressions;
|
||||
using JobTrackerApi.Data;
|
||||
using JobTrackerApi.Services;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
|
||||
namespace JobTrackerApi.Tests;
|
||||
|
||||
public sealed class MigrationChainTests
|
||||
{
|
||||
private const string SnapshotMigration = "20260711181039_SyncModelSnapshot";
|
||||
|
||||
[Fact]
|
||||
public async Task Blank_sqlite_chain_reaches_latest_and_is_idempotent()
|
||||
{
|
||||
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||
await connection.OpenAsync();
|
||||
await using var db = Context(connection);
|
||||
|
||||
await db.Database.MigrateAsync();
|
||||
await db.Database.MigrateAsync();
|
||||
|
||||
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
||||
Assert.Equal(3, await ScalarAsync<long>(connection, """
|
||||
SELECT COUNT(*) FROM sqlite_master
|
||||
WHERE type = 'table' AND name IN ('AspNetUsers', 'AiInteractions', 'AiUsageRecords');
|
||||
"""));
|
||||
Assert.Equal(10, await ScalarAsync<long>(connection, """
|
||||
SELECT COUNT(*) FROM pragma_table_info('JobApplications')
|
||||
WHERE name IN ('OwnerUserId', 'ShortSummary', 'TailoredCvText', 'TailoredCvUpdatedAt',
|
||||
'LastReminderEmailSentAt', 'RecruiterMessageDraft', 'SalaryMin', 'SalaryMax',
|
||||
'SalaryCurrency', 'SalaryPeriod');
|
||||
"""));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Populated_pre_job_split_database_preserves_rows_through_latest()
|
||||
{
|
||||
await using var connection = new SqliteConnection("Data Source=:memory:");
|
||||
await connection.OpenAsync();
|
||||
await using var db = Context(connection);
|
||||
var migrator = db.GetService<IMigrator>();
|
||||
await migrator.MigrateAsync(SnapshotMigration);
|
||||
await ExecuteAsync(connection, """
|
||||
INSERT INTO Companies (Name) VALUES ('Migration fixture');
|
||||
INSERT INTO JobApplications
|
||||
(CompanyId, JobTitle, DateApplied, Status, ResponseReceived, OwnerUserId, ShortSummary)
|
||||
VALUES
|
||||
(1, 'Preserved role', '2026-07-01 09:30:00', 'Applied', 0, 'owner-1', 'Preserved summary');
|
||||
""");
|
||||
|
||||
await migrator.MigrateAsync();
|
||||
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = """
|
||||
SELECT JobTitle, DateApplied, SavedAt, OwnerUserId, ShortSummary
|
||||
FROM JobApplications WHERE Id = 1;
|
||||
""";
|
||||
await using var reader = await command.ExecuteReaderAsync();
|
||||
Assert.True(await reader.ReadAsync());
|
||||
Assert.Equal("Preserved role", reader.GetString(0));
|
||||
Assert.Equal(reader.GetString(1), reader.GetString(2));
|
||||
Assert.Equal("owner-1", reader.GetString(3));
|
||||
Assert.Equal("Preserved summary", reader.GetString(4));
|
||||
Assert.False(await reader.ReadAsync());
|
||||
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MariaDb_script_keeps_new_identifiers_and_bootstrap_types_provider_safe()
|
||||
{
|
||||
var currentUser = new Mock<ICurrentUserService>();
|
||||
currentUser.SetupGet(service => service.UserId).Returns((string?)null);
|
||||
var options = new DbContextOptionsBuilder<JobTrackerContext>()
|
||||
.UseMySql(
|
||||
"Server=127.0.0.1;Database=script_only;User=none;Password=none;",
|
||||
new MariaDbServerVersion(new Version(11, 0, 0)))
|
||||
.Options;
|
||||
await using var db = new JobTrackerContext(options, currentUser.Object);
|
||||
|
||||
var script = db.GetService<IMigrator>().GenerateScript();
|
||||
|
||||
Assert.Contains("`OwnerUserId` varchar(255)", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CONSTRAINT `FK_AccountDeletionFiles_Request`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetUsers`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `AiInteractions`", script, StringComparison.Ordinal);
|
||||
Assert.All(
|
||||
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
|
||||
identifier => Assert.True(identifier.Length <= 64, $"MariaDB constraint identifier exceeds 64 characters: {identifier}"));
|
||||
}
|
||||
|
||||
private static JobTrackerContext Context(SqliteConnection connection)
|
||||
{
|
||||
var currentUser = new Mock<ICurrentUserService>();
|
||||
currentUser.SetupGet(service => service.UserId).Returns((string?)null);
|
||||
return new JobTrackerContext(
|
||||
new DbContextOptionsBuilder<JobTrackerContext>().UseSqlite(connection).Options,
|
||||
currentUser.Object);
|
||||
}
|
||||
|
||||
private static async Task<T> ScalarAsync<T>(SqliteConnection connection, string sql)
|
||||
{
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
return (T)Convert.ChangeType(await command.ExecuteScalarAsync() ?? throw new DataException(), typeof(T));
|
||||
}
|
||||
|
||||
private static async Task ExecuteAsync(SqliteConnection connection, string sql)
|
||||
{
|
||||
await using var command = connection.CreateCommand();
|
||||
command.CommandText = sql;
|
||||
await command.ExecuteNonQueryAsync();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user