120 lines
5.2 KiB
C#
120 lines
5.2 KiB
C#
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();
|
|
}
|
|
}
|