Some checks are pending
CI / Build og unit tests (push) Waiting to run
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
Each instance runs its due collectors over one hardened connection per cycle, keeps previous results for deltas, evaluates all rules and stores findings, rule states and time series. Instances run in parallel with a limit; a slow or failing instance never delays the others. The repository is migrated at startup with retry, and maintenance runs hourly. End-to-end test against SQL Server with the least-privilege login. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
96 lines
4 KiB
C#
96 lines
4 KiB
C#
using Microsoft.Extensions.Logging.Abstractions;
|
|
using SQLInsight.Agent;
|
|
using SQLInsight.Agent.Monitoring;
|
|
using SQLInsight.Collectors;
|
|
using SQLInsight.Connectivity;
|
|
using SQLInsight.Core.Findings;
|
|
using SQLInsight.Repository;
|
|
using SQLInsight.Rules;
|
|
using SQLInsight.Rules.Engine;
|
|
|
|
namespace SQLInsight.IntegrationTests;
|
|
|
|
/// <summary>
|
|
/// The agent's real pipeline end to end: least-privilege connection, all collectors, all rules, repository.
|
|
/// </summary>
|
|
[Collection(SqlServerTestGroup.Name)]
|
|
[Trait("Category", "Integration")]
|
|
public class AgentPipelineTests(SqlServerFixture sql) : IAsyncLifetime
|
|
{
|
|
private readonly string _database = "SQLInsightAgent_" + Guid.NewGuid().ToString("N")[..8];
|
|
private InsightRepository _repository = null!;
|
|
|
|
public async Task InitializeAsync()
|
|
{
|
|
await sql.ExecuteAsSaAsync($"CREATE DATABASE [{_database}];");
|
|
_repository = new InsightRepository(sql.DatabaseConnectionString(_database));
|
|
await _repository.MigrateAsync();
|
|
}
|
|
|
|
public Task DisposeAsync() => sql.ExecuteAsSaAsync($"ALTER DATABASE [{_database}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; DROP DATABASE [{_database}];");
|
|
|
|
private InstanceMonitor Monitor(string name, string? password = null) => new(
|
|
new MonitoredInstance
|
|
{
|
|
Name = name,
|
|
Server = sql.Server,
|
|
Authentication = AuthenticationMode.SqlLogin,
|
|
UserName = SqlServerFixture.AgentLogin,
|
|
Password = password ?? SqlServerFixture.AgentPassword,
|
|
Provider = ConnectionProvider.SqlClient,
|
|
TrustServerCertificate = true,
|
|
},
|
|
BuiltInCollectors.Create(),
|
|
new Dictionary<string, TimeSpan>(),
|
|
new SqlConnectionFactory(),
|
|
new RuleEngine(BuiltInRules.Create()),
|
|
_repository,
|
|
NullLogger.Instance);
|
|
|
|
[Fact]
|
|
public async Task Two_cycles_store_findings_rule_states_and_time_series()
|
|
{
|
|
var monitor = Monitor("container");
|
|
var start = DateTimeOffset.UtcNow;
|
|
|
|
var first = await monitor.RunCycleAsync(start, CancellationToken.None);
|
|
await sql.ExecuteAsSaAsync("SELECT COUNT(*) FROM sys.objects a CROSS JOIN sys.objects b;");
|
|
var nothingDue = await monitor.RunCycleAsync(start.AddSeconds(10), CancellationToken.None);
|
|
var second = await monitor.RunCycleAsync(start.AddMinutes(16), CancellationToken.None);
|
|
|
|
Assert.NotNull(first);
|
|
Assert.Null(nothingDue);
|
|
Assert.NotNull(second);
|
|
Assert.Equal(BuiltInRules.Create().Count, second.Count);
|
|
Assert.DoesNotContain(second, r => r.Outcome == RuleOutcome.Error);
|
|
|
|
var instance = await _repository.GetInstanceAsync("container");
|
|
Assert.NotNull(instance);
|
|
Assert.False(instance.IsFailing);
|
|
|
|
var findings = await _repository.GetActiveFindingsAsync(instance.InstanceId);
|
|
// A fresh container has well-known problems: default max server memory and master never backed up.
|
|
Assert.Contains(findings, f => f.RuleId == "SQLI-CFG-001" && f.Severity == Severity.High);
|
|
Assert.Contains(findings, f => f.RuleId == "SQLI-DB-007" && f.Subject == "database: master");
|
|
|
|
var states = await _repository.GetRuleStatesAsync(instance.InstanceId);
|
|
Assert.Equal(BuiltInRules.Create().Count, states.Count);
|
|
|
|
// Wait stats need two samples; after the second cycle they are measurable.
|
|
Assert.NotEqual(RuleOutcome.NotMeasurable, states.Single(s => s.RuleId == "SQLI-WAIT-003").Outcome);
|
|
Assert.NotEmpty(await _repository.GetTopWaitsAsync(instance.InstanceId, start.AddHours(-1)));
|
|
}
|
|
|
|
[Fact]
|
|
public async Task Unreachable_instance_is_recorded_as_failing()
|
|
{
|
|
var monitor = Monitor("wrong-password", password: "not-the-password");
|
|
|
|
var result = await monitor.RunCycleAsync(DateTimeOffset.UtcNow, CancellationToken.None);
|
|
|
|
Assert.Null(result);
|
|
var instance = await _repository.GetInstanceAsync("wrong-password");
|
|
Assert.True(instance!.IsFailing);
|
|
Assert.Contains("Login failed", instance.LastError, StringComparison.Ordinal);
|
|
}
|
|
}
|