Some checks are pending
CI / Integration (mcr.microsoft.com/mssql/server:2017-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2019-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2022-latest) (push) Blocked by required conditions
CI / Integration (mcr.microsoft.com/mssql/server:2025-latest) (push) Blocked by required conditions
CI / Build og unit tests (push) Waiting to run
Collectors (SQL Server 2000+, version-specific SQL, least privilege): server, configuration, trace flags, databases (settings, backups in UTC, CHECKDB, VLFs, Query Store, files, tempdb), suspect pages, plan cache, wait stats and file I/O as deltas with restart/clear detection, performance counters, CPU from the scheduler monitor ring buffer, blocking (sleeping head blockers with open transactions), and deadlocks from system_health continuing from the last file offset. Rules: configuration (memory, MAXDOP per Microsoft NUMA guidance, cost threshold, ad hoc bloat, risky options, pending changes), databases (state, settings, backups, log backups, CHECKDB, suspect pages, VLFs, autogrowth, max size, Query Store, tempdb), waits (THREADPOOL, signal waits, locks, I/O, WRITELOG, parallelism, PAGELATCH), I/O latency, CPU, memory grants, blocking and deadlocks. Missing data makes a rule not measurable with the collector's reason. Integration tests run every collector as the least-privilege login and reproduce real blocking and a real deadlock; green on 2017 and 2022. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
77 lines
3 KiB
C#
77 lines
3 KiB
C#
using Microsoft.Data.SqlClient;
|
||
using Testcontainers.MsSql;
|
||
|
||
namespace SQLInsight.IntegrationTests;
|
||
|
||
/// <summary>
|
||
/// One SQL Server container for the test run, with the agent login set up by the real least-privilege script.
|
||
/// The image is chosen with <c>SQLINSIGHT_MSSQL_IMAGE</c> so CI can run the same tests against 2017–2025.
|
||
/// </summary>
|
||
public sealed class SqlServerFixture : IAsyncLifetime
|
||
{
|
||
public const string DefaultImage = "mcr.microsoft.com/mssql/server:2022-latest";
|
||
public const string AgentLogin = "sqlinsight_agent";
|
||
public const string AgentPassword = "Agent-Pa55word!";
|
||
public const string TestDatabase = "SQLInsightQsTest";
|
||
|
||
public static string Image => Environment.GetEnvironmentVariable("SQLINSIGHT_MSSQL_IMAGE") ?? DefaultImage;
|
||
|
||
private readonly MsSqlContainer _container = new MsSqlBuilder(Image).Build();
|
||
|
||
public string SaConnectionString => _container.GetConnectionString();
|
||
|
||
public string Server => new SqlConnectionStringBuilder(SaConnectionString).DataSource;
|
||
|
||
public string SaPassword => new SqlConnectionStringBuilder(SaConnectionString).Password;
|
||
|
||
public string AgentConnectionString => new SqlConnectionStringBuilder(SaConnectionString)
|
||
{
|
||
UserID = AgentLogin,
|
||
Password = AgentPassword,
|
||
InitialCatalog = "master",
|
||
}.ConnectionString;
|
||
|
||
public string DatabaseConnectionString(string database) =>
|
||
new SqlConnectionStringBuilder(SaConnectionString) { InitialCatalog = database }.ConnectionString;
|
||
|
||
public async Task InitializeAsync()
|
||
{
|
||
await _container.StartAsync();
|
||
|
||
await ExecuteAsSaAsync($"""
|
||
CREATE LOGIN [{AgentLogin}] WITH PASSWORD = N'{AgentPassword}', CHECK_POLICY = OFF;
|
||
CREATE DATABASE [{TestDatabase}];
|
||
""");
|
||
await ExecuteAsSaAsync($"""
|
||
USE [{TestDatabase}];
|
||
ALTER DATABASE [{TestDatabase}] SET QUERY_STORE = ON;
|
||
CREATE TABLE dbo.CustomerData (Id int NOT NULL PRIMARY KEY, Secret nvarchar(50) NOT NULL);
|
||
INSERT dbo.CustomerData VALUES (1, N'fortroligt'), (2, N'hemmeligt');
|
||
""");
|
||
await ExecuteAsSaAsync(await GrantScriptAsync());
|
||
}
|
||
|
||
public static async Task<string> GrantScriptAsync()
|
||
{
|
||
var script = await File.ReadAllTextAsync(Path.Combine(AppContext.BaseDirectory, "Scripts", "grant-least-privilege.sql"));
|
||
return script.Replace(@"N'DOMÆNE\svc-sqlinsight$'", $"N'{AgentLogin}'", StringComparison.Ordinal);
|
||
}
|
||
|
||
public Task DisposeAsync() => _container.DisposeAsync().AsTask();
|
||
|
||
public async Task ExecuteAsSaAsync(string sql)
|
||
{
|
||
await using var connection = new SqlConnection(SaConnectionString);
|
||
await connection.OpenAsync();
|
||
await using var command = connection.CreateCommand();
|
||
command.CommandText = sql;
|
||
command.CommandTimeout = 120;
|
||
await command.ExecuteNonQueryAsync();
|
||
}
|
||
}
|
||
|
||
[CollectionDefinition(Name)]
|
||
public sealed class SqlServerTestGroup : ICollectionFixture<SqlServerFixture>
|
||
{
|
||
public const string Name = "SQL Server";
|
||
}
|