using System.Reflection; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Migrations; using Microsoft.Data.SqlClient; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Migrations; namespace SeaHavenIndustries.Tests; public class SH138SiteContactsSqlServerTests { private const string LocalDbMaster = @"Server=(localdb)\MSSQLLocalDB;Database=master;Trusted_Connection=True;TrustServerCertificate=True;Connect Timeout=3"; [Fact] public async Task SH138_SiteContacts_MigrationAddsNullableIntOrderColumn_WhenLocalDbAvailable() { var masterConnectionString = await ResolveMasterConnectionStringAsync(); if (masterConnectionString == null) return; var dbName = $"SH138Migration_{Guid.NewGuid():N}"; var connectionString = WithDatabase(masterConnectionString, dbName); try { await CreateDatabaseAsync(masterConnectionString, dbName); await using var connection = new SqlConnection(connectionString); await connection.OpenAsync(); await using (var createTables = connection.CreateCommand()) { createTables.CommandText = """ CREATE TABLE Contacts ( Id int NOT NULL IDENTITY PRIMARY KEY, FirstName nvarchar(max) NULL, PhoneNumber nvarchar(max) NULL ); """; await createTables.ExecuteNonQueryAsync(); } var options = new DbContextOptionsBuilder() .UseSqlServer(connection) .Options; await using var context = new ApplicationDbContext(options); await ApplyMigrationUpAsync(context, new SH138_SiteContacts()); await using (var columnCmd = connection.CreateCommand()) { columnCmd.CommandText = """ SELECT t.name FROM sys.columns c JOIN sys.types t ON c.user_type_id = t.user_type_id WHERE c.object_id = OBJECT_ID(N'Contacts') AND c.name = N'SiteContactOrder'; """; var columnType = await columnCmd.ExecuteScalarAsync(); Assert.Equal("int", columnType); } await using (var nullableCmd = connection.CreateCommand()) { nullableCmd.CommandText = """ SELECT c.is_nullable FROM sys.columns c WHERE c.object_id = OBJECT_ID(N'Contacts') AND c.name = N'SiteContactOrder'; """; var isNullable = await nullableCmd.ExecuteScalarAsync(); Assert.Equal(true, isNullable); } } finally { await DropDatabaseAsync(masterConnectionString, dbName); } } private static async Task ApplyMigrationUpAsync(ApplicationDbContext context, Migration migration) { var builder = new MigrationBuilder(context.Database.ProviderName!); var up = typeof(Migration).GetMethod("Up", BindingFlags.Instance | BindingFlags.NonPublic) ?? throw new InvalidOperationException("Migration.Up not found."); up.Invoke(migration, [builder]); var sqlGenerator = context.GetService(); var commands = sqlGenerator.Generate(builder.Operations, model: null); foreach (var command in commands) await context.Database.ExecuteSqlRawAsync(command.CommandText); } private static async Task ResolveMasterConnectionStringAsync() { var configured = Environment.GetEnvironmentVariable("SH138_SQLSERVER_MASTER"); var candidate = !string.IsNullOrWhiteSpace(configured) ? WithDatabase(configured, "master") : OperatingSystem.IsWindows() ? LocalDbMaster : null; if (candidate == null) return null; try { await using var connection = new SqlConnection(candidate); await connection.OpenAsync(); return candidate; } catch (SqlException) { return null; } catch (InvalidOperationException) { return null; } } private static async Task CreateDatabaseAsync(string masterConnectionString, string dbName) { var quoted = QuoteSqlServerIdentifier(dbName); await using var connection = new SqlConnection(masterConnectionString); await connection.OpenAsync(); await using var command = connection.CreateCommand(); command.CommandText = $"CREATE DATABASE {quoted};"; await command.ExecuteNonQueryAsync(); } private static async Task DropDatabaseAsync(string masterConnectionString, string dbName) { try { var quoted = QuoteSqlServerIdentifier(dbName); await using var connection = new SqlConnection(masterConnectionString); await connection.OpenAsync(); await using var command = connection.CreateCommand(); command.CommandText = $""" IF DB_ID(@dbName) IS NOT NULL BEGIN ALTER DATABASE {quoted} SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE {quoted}; END """; command.Parameters.AddWithValue("@dbName", dbName); await command.ExecuteNonQueryAsync(); } catch { // Best-effort cleanup for ephemeral LocalDB databases. } } private static string QuoteSqlServerIdentifier(string name) => "[" + name.Replace("]", "]]", StringComparison.Ordinal) + "]"; private static string WithDatabase(string connectionString, string database) { var builder = new SqlConnectionStringBuilder(connectionString) { InitialCatalog = database }; return builder.ConnectionString; } }