refactor(db): migrate interview preparation

Move durable user-owned interview preparation into an additive provider-aware migration. Preserve questions, answers, practice state, ordering, and application cascade semantics.
This commit is contained in:
cesnimda
2026-08-30 18:58:55 +02:00
parent 934b35a1e0
commit a906916acc
10 changed files with 148 additions and 55 deletions
@@ -117,6 +117,7 @@ public sealed class MigrationChainTests
Assert.Contains("CREATE TABLE IF NOT EXISTS `CvVariantVersions`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `ApplicationChecklistItems`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `CoverLetterVersions`", script, StringComparison.Ordinal);
Assert.Contains("CREATE TABLE IF NOT EXISTS `InterviewPrepItems`", script, StringComparison.Ordinal);
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
Assert.All(
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
@@ -820,6 +821,63 @@ public sealed class MigrationChainTests
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
[Fact]
public async Task Interview_prep_item_adoption_preserves_user_content_and_practice_state()
{
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("20260830131000_AdoptCoverLetterVersionSchema");
await ExecuteAsync(connection, """
INSERT INTO Companies (Name) VALUES ('Fixture company');
INSERT INTO JobApplications
(CompanyId, JobTitle, DateApplied, SavedAt, Status, ResponseReceived, OwnerUserId,
HasResume, HasCoverLetter, HasPortfolio, HasOtherAttachment, IsDeleted)
VALUES
(1, 'Fixture role', '2026-08-01T09:00:00', '2026-07-20T09:00:00',
'Applied', 0, 'owner-fixture', 0, 0, 0, 0, 0);
CREATE TABLE "InterviewPrepItems" (
"Id" INTEGER NOT NULL CONSTRAINT "PK_InterviewPrepItems" PRIMARY KEY AUTOINCREMENT,
"OwnerUserId" TEXT NOT NULL, "JobApplicationId" INTEGER NOT NULL,
"Category" TEXT NOT NULL, "Title" TEXT NOT NULL, "Content" TEXT NULL,
"Source" TEXT NOT NULL, "IsPrepared" INTEGER NOT NULL, "SortOrder" INTEGER NOT NULL,
"CreatedAtUtc" TEXT NOT NULL, "UpdatedAtUtc" TEXT NOT NULL,
CONSTRAINT "FK_InterviewPrepItems_JobApplications_JobApplicationId"
FOREIGN KEY ("JobApplicationId") REFERENCES "JobApplications" ("Id") ON DELETE CASCADE
);
INSERT INTO "InterviewPrepItems"
("OwnerUserId", "JobApplicationId", "Category", "Title", "Content", "Source",
"IsPrepared", "SortOrder", "CreatedAtUtc", "UpdatedAtUtc")
VALUES
('owner-fixture', 1, 'star', 'Describe a difficult incident',
'Situation, task, action, result', 'user', 1, 10,
'2026-08-30T09:00:00+00:00', '2026-08-30T09:10:00+00:00'),
('owner-fixture', 1, 'technical', 'Explain dependency injection',
'Accepted suggestion, then edited', 'ai', 0, 20,
'2026-08-30T09:00:00+00:00', '2026-08-30T09:15:00+00:00');
""");
await migrator.MigrateAsync();
Assert.Equal("Situation, task, action, result", await ScalarAsync<string>(connection,
"SELECT Content FROM InterviewPrepItems WHERE SortOrder = 10;"));
Assert.Equal(1L, await ScalarAsync<long>(connection,
"SELECT IsPrepared FROM InterviewPrepItems WHERE SortOrder = 10;"));
await migrator.MigrateAsync("20260830131000_AdoptCoverLetterVersionSchema");
Assert.Equal("ai", await ScalarAsync<string>(connection,
"SELECT Source FROM InterviewPrepItems WHERE SortOrder = 20;"));
await migrator.MigrateAsync();
Assert.Equal(1L, await ScalarAsync<long>(connection, """
SELECT COUNT(*) FROM sqlite_master
WHERE type = 'index' AND name = 'IX_InterviewPrepItems_Owner_Job_Sort';
"""));
await ExecuteAsync(connection, "DELETE FROM JobApplications WHERE Id = 1;");
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM InterviewPrepItems;"));
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
}
private static JobTrackerContext Context(SqliteConnection connection)
{
var currentUser = new Mock<ICurrentUserService>();