refactor(db): migrate Identity schema
Complete schema ownership by moving all ASP.NET Identity tables to an additive provider-aware migration. Preserve credentials, security state, preferences, roles, claims, external logins, tokens, indexes, and cascades.
This commit is contained in:
@@ -126,6 +126,11 @@ public sealed class MigrationChainTests
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerProjects`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerCertifications`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `CareerLanguages`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetRoles`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetUserClaims`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetUserLogins`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetUserRoles`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("CREATE TABLE IF NOT EXISTS `AspNetUserTokens`", script, StringComparison.Ordinal);
|
||||
Assert.Contains("`UiLanguage` varchar(16)", script, StringComparison.Ordinal);
|
||||
Assert.All(
|
||||
Regex.Matches(script, "CONSTRAINT `([^`]+)`").Select(match => match.Groups[1].Value),
|
||||
@@ -1015,6 +1020,89 @@ public sealed class MigrationChainTests
|
||||
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Identity_adoption_preserves_accounts_credentials_roles_logins_claims_and_tokens()
|
||||
{
|
||||
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("20260830133000_AdoptCareerProfileSchema");
|
||||
await ExecuteAsync(connection, """
|
||||
CREATE TABLE "AspNetRoles" (
|
||||
"Id" TEXT NOT NULL PRIMARY KEY, "Name" TEXT NULL,
|
||||
"NormalizedName" TEXT NULL, "ConcurrencyStamp" TEXT NULL);
|
||||
CREATE TABLE "AspNetRoleClaims" (
|
||||
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, "RoleId" TEXT NOT NULL,
|
||||
"ClaimType" TEXT NULL, "ClaimValue" TEXT NULL,
|
||||
FOREIGN KEY ("RoleId") REFERENCES "AspNetRoles" ("Id") ON DELETE CASCADE);
|
||||
CREATE TABLE "AspNetUserClaims" (
|
||||
"Id" INTEGER PRIMARY KEY AUTOINCREMENT, "UserId" TEXT NOT NULL,
|
||||
"ClaimType" TEXT NULL, "ClaimValue" TEXT NULL,
|
||||
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
|
||||
CREATE TABLE "AspNetUserLogins" (
|
||||
"LoginProvider" TEXT NOT NULL, "ProviderKey" TEXT NOT NULL,
|
||||
"ProviderDisplayName" TEXT NULL, "UserId" TEXT NOT NULL,
|
||||
PRIMARY KEY ("LoginProvider", "ProviderKey"),
|
||||
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
|
||||
CREATE TABLE "AspNetUserRoles" (
|
||||
"UserId" TEXT NOT NULL, "RoleId" TEXT NOT NULL,
|
||||
PRIMARY KEY ("UserId", "RoleId"),
|
||||
FOREIGN KEY ("RoleId") REFERENCES "AspNetRoles" ("Id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
|
||||
CREATE TABLE "AspNetUserTokens" (
|
||||
"UserId" TEXT NOT NULL, "LoginProvider" TEXT NOT NULL, "Name" TEXT NOT NULL,
|
||||
"Value" TEXT NULL, PRIMARY KEY ("UserId", "LoginProvider", "Name"),
|
||||
FOREIGN KEY ("UserId") REFERENCES "AspNetUsers" ("Id") ON DELETE CASCADE);
|
||||
|
||||
INSERT INTO "AspNetUsers"
|
||||
("Id", "UserName", "NormalizedUserName", "Email", "NormalizedEmail",
|
||||
"EmailConfirmed", "PasswordHash", "PhoneNumberConfirmed", "TwoFactorEnabled",
|
||||
"LockoutEnabled", "AccessFailedCount", "AiEnabled", "ExternalAiProcessingAllowed",
|
||||
"DeletionStatus", "EmailFollowUpRemindersEnabled", "UiLanguage")
|
||||
VALUES ('user-fixture', 'connor', 'CONNOR', 'connor@example.test', 'CONNOR@EXAMPLE.TEST',
|
||||
1, 'preserved-password-hash', 0, 1, 1, 0, 1, 0, 'active', 1, 'nb');
|
||||
INSERT INTO "AspNetRoles" VALUES ('role-admin', 'Admin', 'ADMIN', 'role-stamp');
|
||||
INSERT INTO "AspNetRoleClaims" ("RoleId", "ClaimType", "ClaimValue")
|
||||
VALUES ('role-admin', 'permission', 'admin.manage');
|
||||
INSERT INTO "AspNetUserClaims" ("UserId", "ClaimType", "ClaimValue")
|
||||
VALUES ('user-fixture', 'locale', 'nb');
|
||||
INSERT INTO "AspNetUserLogins" VALUES ('google', 'subject-1', 'Google', 'user-fixture');
|
||||
INSERT INTO "AspNetUserRoles" VALUES ('user-fixture', 'role-admin');
|
||||
INSERT INTO "AspNetUserTokens" VALUES ('user-fixture', 'authenticator', 'refresh', 'encrypted-token');
|
||||
""");
|
||||
|
||||
await migrator.MigrateAsync();
|
||||
Assert.Equal("preserved-password-hash", await ScalarAsync<string>(connection,
|
||||
"SELECT PasswordHash FROM AspNetUsers WHERE Id = 'user-fixture';"));
|
||||
Assert.Equal("nb", await ScalarAsync<string>(connection,
|
||||
"SELECT UiLanguage FROM AspNetUsers WHERE Id = 'user-fixture';"));
|
||||
Assert.Equal("admin.manage", await ScalarAsync<string>(connection,
|
||||
"SELECT ClaimValue FROM AspNetRoleClaims WHERE RoleId = 'role-admin';"));
|
||||
Assert.Equal("encrypted-token", await ScalarAsync<string>(connection,
|
||||
"SELECT Value FROM AspNetUserTokens WHERE UserId = 'user-fixture';"));
|
||||
|
||||
await migrator.MigrateAsync("20260830133000_AdoptCareerProfileSchema");
|
||||
Assert.Equal("subject-1", await ScalarAsync<string>(connection,
|
||||
"SELECT ProviderKey FROM AspNetUserLogins WHERE UserId = 'user-fixture';"));
|
||||
await migrator.MigrateAsync();
|
||||
Assert.Equal(8L, await ScalarAsync<long>(connection, """
|
||||
SELECT COUNT(*) FROM sqlite_master WHERE type = 'index' AND name IN (
|
||||
'RoleNameIndex', 'IX_AspNetRoleClaims_RoleId', 'EmailIndex', 'UserNameIndex',
|
||||
'IX_AspNetUsers_MicrosoftTenantId_MicrosoftObjectId',
|
||||
'IX_AspNetUserClaims_UserId', 'IX_AspNetUserLogins_UserId', 'IX_AspNetUserRoles_RoleId');
|
||||
"""));
|
||||
|
||||
await ExecuteAsync(connection, "DELETE FROM AspNetUsers WHERE Id = 'user-fixture';");
|
||||
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AspNetUserClaims;"));
|
||||
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AspNetUserLogins;"));
|
||||
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AspNetUserRoles;"));
|
||||
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AspNetUserTokens;"));
|
||||
await ExecuteAsync(connection, "DELETE FROM AspNetRoles WHERE Id = 'role-admin';");
|
||||
Assert.Equal(0L, await ScalarAsync<long>(connection, "SELECT COUNT(*) FROM AspNetRoleClaims;"));
|
||||
Assert.Empty(await db.Database.GetPendingMigrationsAsync());
|
||||
}
|
||||
|
||||
private static JobTrackerContext Context(SqliteConnection connection)
|
||||
{
|
||||
var currentUser = new Mock<ICurrentUserService>();
|
||||
|
||||
Reference in New Issue
Block a user