shoc-backend/SeaHavenIndustries.Tests/SH221LocationAccountScopeSqlServerTests.cs

189 lines
6.9 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 SH221LocationAccountScopeSqlServerTests
{
private const string LocalDbMaster =
@"Server=(localdb)\MSSQLLocalDB;Database=master;Trusted_Connection=True;TrustServerCertificate=True;Connect Timeout=3";
[Fact]
public async Task SH221_LocationAccountScope_ApplyOnSqlServer_WhenLocalDbAvailable()
{
if (!await IsLocalDbAvailableAsync())
return;
var dbName = $"SH221LocationAccount_{Guid.NewGuid():N}";
var connectionString =
$@"Server=(localdb)\MSSQLLocalDB;Database={dbName};Trusted_Connection=True;TrustServerCertificate=True";
try
{
await CreateDatabaseAsync(dbName);
await using var connection = new SqlConnection(connectionString);
await connection.OpenAsync();
await using (var createTables = connection.CreateCommand())
{
createTables.CommandText =
"""
CREATE TABLE Accounts (
Id int NOT NULL IDENTITY PRIMARY KEY
);
CREATE TABLE Locations (
Id int NOT NULL IDENTITY PRIMARY KEY
);
CREATE TABLE workOrders (
Id int NOT NULL IDENTITY PRIMARY KEY,
LocationId int NULL,
AccountId int NULL
);
SET IDENTITY_INSERT Accounts ON;
INSERT INTO Accounts (Id) VALUES (1);
SET IDENTITY_INSERT Accounts OFF;
SET IDENTITY_INSERT Locations ON;
INSERT INTO Locations (Id) VALUES (11);
SET IDENTITY_INSERT Locations OFF;
INSERT INTO workOrders (LocationId, AccountId) VALUES (11, 1);
""";
await createTables.ExecuteNonQueryAsync();
}
var options = new DbContextOptionsBuilder<ApplicationDbContext>()
.UseSqlServer(connection)
.Options;
await using var context = new ApplicationDbContext(options);
await ApplyMigrationUpAsync(context, new SH221_LocationAccountScope());
await using (var assertCmd = connection.CreateCommand())
{
assertCmd.CommandText =
"""
SELECT c.name
FROM sys.columns c
WHERE c.object_id = OBJECT_ID(N'Locations')
AND c.name = N'AccountId';
""";
var column = await assertCmd.ExecuteScalarAsync();
Assert.Equal("AccountId", column);
}
await using (var indexCmd = connection.CreateCommand())
{
indexCmd.CommandText =
"""
SELECT name
FROM sys.indexes
WHERE object_id = OBJECT_ID(N'Locations')
AND name = N'IX_Locations_AccountId';
""";
var index = await indexCmd.ExecuteScalarAsync();
Assert.Equal("IX_Locations_AccountId", index);
}
await using (var fkCmd = connection.CreateCommand())
{
fkCmd.CommandText =
"""
SELECT name
FROM sys.foreign_keys
WHERE parent_object_id = OBJECT_ID(N'Locations')
AND name = N'FK_Locations_Accounts_AccountId';
""";
var fk = await fkCmd.ExecuteScalarAsync();
Assert.Equal("FK_Locations_Accounts_AccountId", fk);
}
await using (var backfillCmd = connection.CreateCommand())
{
backfillCmd.CommandText = "SELECT AccountId FROM Locations WHERE Id = 11;";
var stamped = await backfillCmd.ExecuteScalarAsync();
Assert.Equal(1, Convert.ToInt32(stamped));
}
}
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 CreateDatabaseAsync(string dbName)
{
var quoted = QuoteSqlServerIdentifier(dbName);
await using var connection = new SqlConnection(LocalDbMaster);
await connection.OpenAsync();
await using var command = connection.CreateCommand();
command.CommandText = $"CREATE DATABASE {quoted};";
await command.ExecuteNonQueryAsync();
}
private static async Task DropDatabaseAsync(string dbName)
{
try
{
var quoted = QuoteSqlServerIdentifier(dbName);
await using var connection = new SqlConnection(LocalDbMaster);
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) + "]";
}