using Microsoft.Data.SqlClient; using Xunit; namespace ScadaLink.ConfigurationDatabase.Tests.Migrations; /// /// Bundle B2 (#22, #23 M3) integration tests for the AddSiteCallsTable /// migration: applies the EF migrations to a freshly-created MSSQL test database /// on the running infra/mssql container and asserts that the resulting /// SiteCalls table carries the expected columns, primary key, and the /// two named operational indexes. /// /// /// Unlike AddAuditLogTable, the SiteCalls table is operational (mutable) /// state — no partition function, no partition scheme, no DB-role restriction. /// Standard [PRIMARY] filegroup. Tests pair /// with Skip.IfNot(...) so the runner reports them as Skipped (not Passed) /// when MSSQL is unreachable. The fixture applies the migration once at /// construction time. /// public class AddSiteCallsTableMigrationTests : IClassFixture { private readonly MsSqlMigrationFixture _fixture; public AddSiteCallsTableMigrationTests(MsSqlMigrationFixture fixture) { _fixture = fixture; } [SkippableFact] public async Task AppliesMigration_CreatesSiteCallsTable_WithExpectedColumns() { Skip.IfNot(_fixture.Available, _fixture.SkipReason); var exists = await ScalarAsync( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES " + "WHERE TABLE_NAME = 'SiteCalls' AND TABLE_SCHEMA = 'dbo';"); Assert.Equal(1, exists); // Every required column from SiteCall + IngestedAtUtc. We don't pin types // here because EF's CreateTable layer already encodes them; the // entity-config tests cover length / unicode / nullability for the // value-converted PK column. Just confirm the schema has all twelve. var expectedColumns = new[] { "TrackedOperationId", "Channel", "Target", "SourceSite", "Status", "RetryCount", "LastError", "HttpStatus", "CreatedAtUtc", "UpdatedAtUtc", "TerminalAtUtc", "IngestedAtUtc", }; foreach (var column in expectedColumns) { var present = await ScalarAsync( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS " + $"WHERE TABLE_NAME = 'SiteCalls' AND COLUMN_NAME = '{column}';"); Assert.True(present == 1, $"Expected SiteCalls.{column} to exist; found {present}."); } } [SkippableFact] public async Task AppliesMigration_CreatesPK_OnTrackedOperationId() { Skip.IfNot(_fixture.Available, _fixture.SkipReason); // Walk sys.indexes for the table's clustered PK index and confirm its // single key column is TrackedOperationId. SiteCalls is non-partitioned // so the PK is a simple single-column clustered index. var pkColumn = await ScalarAsync( "SELECT c.name FROM sys.indexes i " + "INNER JOIN sys.objects o ON i.object_id = o.object_id " + "INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id " + "INNER JOIN sys.columns c ON ic.object_id = c.object_id AND ic.column_id = c.column_id " + "WHERE o.name = 'SiteCalls' AND i.is_primary_key = 1;"); Assert.Equal("TrackedOperationId", pkColumn); } [SkippableFact] public async Task AppliesMigration_CreatesIndex_Source_Created() { Skip.IfNot(_fixture.Available, _fixture.SkipReason); var count = await ScalarAsync( "SELECT COUNT(*) FROM sys.indexes i " + "INNER JOIN sys.objects o ON i.object_id = o.object_id " + "WHERE o.name = 'SiteCalls' AND i.name = 'IX_SiteCalls_Source_Created';"); Assert.Equal(1, count); } [SkippableFact] public async Task AppliesMigration_CreatesIndex_Status_Updated() { Skip.IfNot(_fixture.Available, _fixture.SkipReason); var count = await ScalarAsync( "SELECT COUNT(*) FROM sys.indexes i " + "INNER JOIN sys.objects o ON i.object_id = o.object_id " + "WHERE o.name = 'SiteCalls' AND i.name = 'IX_SiteCalls_Status_Updated';"); Assert.Equal(1, count); } // --- helpers ------------------------------------------------------------ private async Task ScalarAsync(string sql) { await using var conn = _fixture.OpenConnection(); await using var cmd = conn.CreateCommand(); cmd.CommandText = sql; var result = await cmd.ExecuteScalarAsync(); if (result is null || result is DBNull) { return default!; } return (T)Convert.ChangeType(result, typeof(T) == typeof(string) ? typeof(string) : Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T))!; } }