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;
///
/// The agent's real pipeline end to end: least-privilege connection, all collectors, all rules, repository.
///
[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(),
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);
}
}