mirror of
https://github.com/Sea-Haven-Industries/shoc-backend.git
synced 2026-09-30 06:03:12 +00:00
180 lines
6.7 KiB
C#
180 lines
6.7 KiB
C#
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.
|
|
}
|
|
}
|
|
}
|