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 Phase4SearchIndexesSqlServerTests { private const string LocalDbMaster = @"Server=(localdb)\MSSQLLocalDB;Database=master;Trusted_Connection=True;TrustServerCertificate=True;Connect Timeout=3"; [Fact] public void WorkOrder_IndexedStringColumns_HaveBoundedMaxLengths() { var options = new DbContextOptionsBuilder() .UseSqlServer(LocalDbMaster) .Options; using var context = new ApplicationDbContext(options); var entity = context.Model.FindEntityType(typeof(WorkOrder)); Assert.NotNull(entity); Assert.Equal(32, entity.FindProperty(nameof(WorkOrder.SiteCode))!.GetMaxLength()); Assert.Equal(11, entity.FindProperty(nameof(WorkOrder.InternalWONumber))!.GetMaxLength()); } [Fact] public async Task Phase4_SearchIndexes_ApplyOnSqlServer_WhenLocalDbAvailable() { if (!await IsLocalDbAvailableAsync()) return; var dbName = $"Phase4SearchIndexes_{Guid.NewGuid():N}"; var connectionString = $@"Server=(localdb)\MSSQLLocalDB;Database={dbName};Trusted_Connection=True;TrustServerCertificate=True"; try { await using var connection = new SqlConnection(connectionString); await connection.OpenAsync(); await using (var createTable = connection.CreateCommand()) { createTable.CommandText = """ CREATE TABLE workOrders ( Id int NOT NULL IDENTITY PRIMARY KEY, SiteCode nvarchar(max) NULL, InternalWONumber nvarchar(max) NULL, LifecycleStatus int NULL, ScheduledDate datetime2 NULL, istemplate bit NULL ); """; await createTable.ExecuteNonQueryAsync(); } var options = new DbContextOptionsBuilder() .UseSqlServer(connection) .Options; await using var context = new ApplicationDbContext(options); await ApplyMigrationUpAsync(context, new Phase4_SearchIndexes()); await using (var assertCmd = connection.CreateCommand()) { assertCmd.CommandText = """ SELECT c.name, t.name AS type_name, c.max_length FROM sys.columns c INNER JOIN sys.types t ON c.user_type_id = t.user_type_id WHERE c.object_id = OBJECT_ID(N'workOrders') AND c.name IN (N'SiteCode', N'InternalWONumber') ORDER BY c.name; """; await using var reader = await assertCmd.ExecuteReaderAsync(); var columns = new Dictionary(StringComparer.OrdinalIgnoreCase); while (await reader.ReadAsync()) { columns[reader.GetString(0)] = (reader.GetString(1), reader.GetInt16(2)); } Assert.Equal("nvarchar", columns["InternalWONumber"].Type); Assert.Equal(22, columns["InternalWONumber"].MaxLength); // nvarchar(11) => 22 bytes Assert.Equal("nvarchar", columns["SiteCode"].Type); Assert.Equal(64, columns["SiteCode"].MaxLength); // nvarchar(32) => 64 bytes } await using (var indexCmd = connection.CreateCommand()) { indexCmd.CommandText = """ SELECT name FROM sys.indexes WHERE object_id = OBJECT_ID(N'workOrders') AND name IN ( N'IX_workOrders_SiteCode', N'IX_workOrders_InternalWONumber', N'IX_workOrders_LifecycleStatus_ScheduledDate' ); """; await using var reader = await indexCmd.ExecuteReaderAsync(); var indexes = new List(); while (await reader.ReadAsync()) indexes.Add(reader.GetString(0)); Assert.Contains("IX_workOrders_SiteCode", indexes); Assert.Contains("IX_workOrders_InternalWONumber", indexes); Assert.Contains("IX_workOrders_LifecycleStatus_ScheduledDate", indexes); } } finally { await DropDatabaseAsync(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 IsLocalDbAvailableAsync() { if (!OperatingSystem.IsWindows()) return false; try { await using var connection = new SqlConnection(LocalDbMaster); await connection.OpenAsync(); return true; } catch (SqlException) { return false; } catch (InvalidOperationException) { return false; } } private static async Task DropDatabaseAsync(string dbName) { try { await using var connection = new SqlConnection(LocalDbMaster); await connection.OpenAsync(); await using var command = connection.CreateCommand(); command.CommandText = $""" IF DB_ID(N'{dbName}') IS NOT NULL BEGIN ALTER DATABASE [{dbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{dbName}]; END """; await command.ExecuteNonQueryAsync(); } catch { // Best-effort cleanup for ephemeral LocalDB databases. } } }