refactor(db): migrate email connections
Move Gmail, Microsoft Graph, and IMAP connection creation into an additive provider-aware migration. Preserve encrypted credentials and retain scoped historical repairs.
This commit is contained in:
@@ -105,6 +105,9 @@ public sealed class MigrationChainTests
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `TwoFactorRecoveryCodes`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `TrustedDevices`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `UserSessions`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `GmailConnections`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `MicrosoftGraphConnections`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `ImapConnections`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
|
||||
Assert.All(
|
||||
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
|
||||
@@ -303,6 +306,87 @@ public sealed class MigrationChainTests
|
||||
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Email_connection_adoption_preserves_encrypted_credentials()
|
||||
{
|
||||
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("20260830123000_AdoptAuthenticationSupportSchema");
|
||||
await ExecuteAsync(connection, """
|
||||
CREATE TABLE "GmailConnections" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_GmailConnections" PRIMARY KEY AUTOINCREMENT,
|
||||
"OwnerUserId" TEXT NOT NULL, "GmailAddress" TEXT NOT NULL,
|
||||
"EncryptedRefreshToken" TEXT NOT NULL, "EncryptedAccessToken" TEXT NULL,
|
||||
"AccessTokenExpiresAt" TEXT NULL, "Scope" TEXT NOT NULL, "ConnectedAt" TEXT NOT NULL,
|
||||
"LastSyncedAt" TEXT NULL, "LastSyncAttemptedAt" TEXT NULL,
|
||||
"LastSyncSucceededAt" TEXT NULL, "LastSyncMode" TEXT NULL,
|
||||
"LastSyncSource" TEXT NULL, "LastSyncStatus" TEXT NULL, "LastSyncError" TEXT NULL
|
||||
);
|
||||
INSERT INTO "GmailConnections"
|
||||
("OwnerUserId", "GmailAddress", "EncryptedRefreshToken", "Scope", "ConnectedAt")
|
||||
VALUES ('owner-fixture', 'gmail@example.invalid', 'gmail-secret', 'mail.read',
|
||||
'2026-08-30T09:00:00+00:00');
|
||||
|
||||
CREATE TABLE "MicrosoftGraphConnections" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_MicrosoftGraphConnections" PRIMARY KEY AUTOINCREMENT,
|
||||
"OwnerUserId" TEXT NOT NULL, "MailAddress" TEXT NOT NULL,
|
||||
"EncryptedRefreshToken" TEXT NOT NULL, "EncryptedAccessToken" TEXT NULL,
|
||||
"AccessTokenExpiresAt" TEXT NULL, "Scope" TEXT NOT NULL, "ConnectedAt" TEXT NOT NULL,
|
||||
"LastSyncedAt" TEXT NULL, "LastSyncAttemptedAt" TEXT NULL,
|
||||
"LastSyncSucceededAt" TEXT NULL, "LastSyncMode" TEXT NULL,
|
||||
"LastSyncSource" TEXT NULL, "LastSyncStatus" TEXT NULL, "LastSyncError" TEXT NULL
|
||||
);
|
||||
INSERT INTO "MicrosoftGraphConnections"
|
||||
("OwnerUserId", "MailAddress", "EncryptedRefreshToken", "Scope", "ConnectedAt")
|
||||
VALUES ('owner-fixture', 'graph@example.invalid', 'graph-secret', 'Mail.Read',
|
||||
'2026-08-30T09:00:00+00:00');
|
||||
|
||||
CREATE TABLE "ImapConnections" (
|
||||
"Id" INTEGER NOT NULL CONSTRAINT "PK_ImapConnections" PRIMARY KEY AUTOINCREMENT,
|
||||
"OwnerUserId" TEXT NOT NULL, "Host" TEXT NOT NULL, "Port" INTEGER NOT NULL,
|
||||
"UseSsl" INTEGER NOT NULL, "Username" TEXT NOT NULL, "EncryptedPassword" TEXT NOT NULL,
|
||||
"ConnectedAt" TEXT NOT NULL, "LastSyncedAt" TEXT NULL,
|
||||
"LastSyncAttemptedAt" TEXT NULL, "LastSyncSucceededAt" TEXT NULL,
|
||||
"LastSyncMode" TEXT NULL, "LastSyncSource" TEXT NULL,
|
||||
"LastSyncStatus" TEXT NULL, "LastSyncError" TEXT NULL
|
||||
);
|
||||
INSERT INTO "ImapConnections"
|
||||
("OwnerUserId", "Host", "Port", "UseSsl", "Username", "EncryptedPassword", "ConnectedAt")
|
||||
VALUES ('owner-fixture', 'imap.example.invalid', 993, 1, 'imap-user', 'imap-secret',
|
||||
'2026-08-30T09:00:00+00:00');
|
||||
""");
|
||||
|
||||
await migrator.MigrateAsync();
|
||||
Assert.Equal("gmail-secret", await ScalarAsync<string>(connection,
|
||||
"SELECT EncryptedRefreshToken FROM GmailConnections WHERE OwnerUserId = 'owner-fixture';"));
|
||||
Assert.Equal("graph-secret", await ScalarAsync<string>(connection,
|
||||
"SELECT EncryptedRefreshToken FROM MicrosoftGraphConnections WHERE OwnerUserId = 'owner-fixture';"));
|
||||
Assert.Equal("imap-secret", await ScalarAsync<string>(connection,
|
||||
"SELECT EncryptedPassword FROM ImapConnections WHERE OwnerUserId = 'owner-fixture';"));
|
||||
|
||||
await migrator.MigrateAsync("20260830123000_AdoptAuthenticationSupportSchema");
|
||||
Assert.Equal("gmail-secret", await ScalarAsync<string>(connection,
|
||||
"SELECT EncryptedRefreshToken FROM GmailConnections WHERE OwnerUserId = 'owner-fixture';"));
|
||||
Assert.Equal("graph-secret", await ScalarAsync<string>(connection,
|
||||
"SELECT EncryptedRefreshToken FROM MicrosoftGraphConnections WHERE OwnerUserId = 'owner-fixture';"));
|
||||
Assert.Equal("imap-secret", await ScalarAsync<string>(connection,
|
||||
"SELECT EncryptedPassword FROM ImapConnections WHERE OwnerUserId = 'owner-fixture';"));
|
||||
|
||||
await migrator.MigrateAsync();
|
||||
Assert.Equal(5L, await ScalarAsync<long>(connection, """
|
||||
SELECT COUNT(*) FROM sqlite_master
|
||||
WHERE type = 'index' AND name IN (
|
||||
'IX_GmailConnections_OwnerUserId',
|
||||
'IX_GmailConnections_OwnerUserId_GmailAddress',
|
||||
'IX_MicrosoftGraphConnections_OwnerUserId',
|
||||
'IX_MicrosoftGraphConnections_OwnerUserId_MailAddress',
|
||||
'IX_ImapConnections_OwnerUserId');
|
||||
"""));
|
||||
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
||||
}
|
||||
|
||||
private static JobTrackerContext Context(SqliteConnection connection)
|
||||
{
|
||||
var currentUser = new Mock<ICurrentUserService>();
|
||||
|
||||
Reference in New Issue
Block a user