shoc-backend/SeaHavenIndustries.Tests/Phase4SearchIndexesSqlServerTests.cs

181 lines
6.7 KiB
C#
Raw Permalink Normal View History

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<ApplicationDbContext>()
.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<ApplicationDbContext>()
.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<string, (string Type, short MaxLength)>(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<string>();
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<IMigrationsSqlGenerator>();
var commands = sqlGenerator.Generate(builder.Operations, model: null);
foreach (var command in commands)
await context.Database.ExecuteSqlRawAsync(command.CommandText);
}
private static async Task<bool> 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.
}
}
}