using Microsoft.Data.Sqlite; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; using ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Migrations; using Xunit; namespace ZB.MOM.WW.ScadaBridge.ConfigurationDatabase.Tests.Migrations; /// /// Idempotency tests for the ResyncLdapGroupMappingSeed migration (#70). /// /// The original migration did an UNCONDITIONAL InsertData of the /// Id=5 (SCADA-Viewers → Viewer) seed row. On any pre-existing DB that already /// carried Id=5 that threw a PK violation and crash-looped startup (dev /// auto-applies migrations). The fix makes the insert provider-aware and /// idempotent (SQLite INSERT OR IGNORE / SQL Server guarded /// IF NOT EXISTS … SET IDENTITY_INSERT … INSERT). /// /// These tests drive the migration's Up() through the EF migrations SQL /// pipeline configured for SQLite, so they exercise the exact /// migrationBuilder.Sql(...) text the migration emits on the SQLite /// branch — the branch a CI box without an MSSQL container can actually run. /// The SQL Server branch is the same migrationBuilder.Sql text shape and /// is exercised by the full-history MSSQL migration fixture /// () used by the other migration tests. /// public class ResyncLdapGroupMappingSeedMigrationTests { private const string SqliteProvider = "Microsoft.EntityFrameworkCore.Sqlite"; [Fact] public void Up_OnSqlite_InsertsRow_WhenIdFiveMissing() { using var connection = OpenSeededConnection(seedIdFive: false); ApplyMigrationUp(connection); Assert.Equal(1, CountId5(connection)); Assert.Equal(("SCADA-Viewers", "Viewer"), ReadId5(connection)); } [Fact] public void Up_OnSqlite_IsIdempotent_WhenIdFiveAlreadyExists() { // Pre-existing DB already carries Id=5 — the #70 crash scenario. using var connection = OpenSeededConnection(seedIdFive: true); // Applying the migration MUST NOT throw on the PK clash... var ex = Record.Exception(() => ApplyMigrationUp(connection)); Assert.Null(ex); // ...and MUST NOT duplicate the row. Assert.Equal(1, CountId5(connection)); Assert.Equal(("SCADA-Viewers", "Viewer"), ReadId5(connection)); } [Fact] public void Up_OnSqlite_ReplayingTwice_IsStillIdempotent() { using var connection = OpenSeededConnection(seedIdFive: false); ApplyMigrationUp(connection); var ex = Record.Exception(() => ApplyMigrationUp(connection)); Assert.Null(ex); Assert.Equal(1, CountId5(connection)); } // --- harness ----------------------------------------------------------- /// /// Opens a shared in-memory SQLite connection holding only the columns this /// migration touches and (optionally) a pre-existing Id=5 row. /// private static SqliteConnection OpenSeededConnection(bool seedIdFive) { var connection = new SqliteConnection("DataSource=:memory:"); connection.Open(); using (var create = connection.CreateCommand()) { // Mirror the production PK shape: Id is the primary key. Re-inserting // an existing Id raises a UNIQUE/PK violation unless guarded. create.CommandText = "CREATE TABLE \"LdapGroupMappings\" (" + " \"Id\" INTEGER NOT NULL CONSTRAINT \"PK_LdapGroupMappings\" PRIMARY KEY," + " \"LdapGroupName\" TEXT NOT NULL," + " \"Role\" TEXT NOT NULL);"; create.ExecuteNonQuery(); } if (seedIdFive) { using var seed = connection.CreateCommand(); seed.CommandText = "INSERT INTO \"LdapGroupMappings\" (\"Id\", \"LdapGroupName\", \"Role\") " + "VALUES (5, 'SCADA-Viewers', 'Viewer');"; seed.ExecuteNonQuery(); } return connection; } /// /// Generates the real migration's Up() SQL via the EF migrations /// pipeline configured for SQLite (so migrationBuilder.ActiveProvider /// is the SQLite provider and the SQLite branch is taken), then runs every /// emitted command against the supplied connection. /// private static void ApplyMigrationUp(SqliteConnection connection) { var options = new DbContextOptionsBuilder() .UseSqlite(connection) .Options; using var context = new ScadaBridgeDbContext(options); var migration = new ResyncLdapGroupMappingSeed { ActiveProvider = SqliteProvider }; var sqlGenerator = context.GetService(); var commands = sqlGenerator.Generate(migration.UpOperations); foreach (var command in commands) { using var cmd = connection.CreateCommand(); cmd.CommandText = command.CommandText; cmd.ExecuteNonQuery(); } } private static int CountId5(SqliteConnection connection) { using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT COUNT(*) FROM \"LdapGroupMappings\" WHERE \"Id\" = 5;"; return Convert.ToInt32(cmd.ExecuteScalar()); } private static (string LdapGroupName, string Role) ReadId5(SqliteConnection connection) { using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT \"LdapGroupName\", \"Role\" FROM \"LdapGroupMappings\" WHERE \"Id\" = 5;"; using var reader = cmd.ExecuteReader(); Assert.True(reader.Read()); return (reader.GetString(0), reader.GetString(1)); } }