sqlinsight-pro/tests/SQLInsight.IntegrationTests/AgentPipelineTests.cs
Claude 5bb1c37fef
Some checks are pending
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:2017-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
Fase 1: on-prem web UI (IIS or Windows service), light design system
- ASP.NET Core Blazor static SSR: overview with KPI tiles, instance page
  (findings, CPU/batch/PLE trends, top waits, file latency, blocking,
  deadlocks with .xdl download, not-measurable rules, resolved), finding
  detail with explanation, evidence, fix script and references
- Light-first design (sidebar, cards, self-hosted Inter, teal accent),
  optional dark theme via toggle; strict CSP, no inline styles
- Windows authentication (Negotiate/IIS) with optional group restriction;
  refuses to run without authentication outside Development
- Hosting: IIS (in-process ASP.NET Core Module), Windows service with
  Kestrel, or systemd; web reads the repository with db_datareader only
- docs/installation.md for repository, permissions, agent and web
- Fix: CPU used by other processes is not derived on Linux (SystemIdle is
  unreliable there); deadlocks are not re-reported or stored twice after
  an agent restart; Dapper sends datetime2 so time comparisons are exact

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
2026-09-29 06:58:51 +00:00

114 lines
4.9 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 Restarted_agent_does_not_report_or_store_deadlocks_again()
{
var now = DateTimeOffset.UtcNow;
await Monitor("restart").RunCycleAsync(now, CancellationToken.None);
var instance = await _repository.GetInstanceAsync("restart");
var stored = (await _repository.GetDeadlocksAsync(instance!.InstanceId, now.AddDays(-2))).Count;
// A new monitor is what the agent has after a restart: no memory of where it stopped.
var results = await Monitor("restart").RunCycleAsync(now.AddMinutes(1), CancellationToken.None);
Assert.Equal(stored, (await _repository.GetDeadlocksAsync(instance.InstanceId, now.AddDays(-2))).Count);
if (stored > 0)
{
Assert.Equal(RuleOutcome.Passed, results!.Single(r => r.RuleId == "SQLI-DLK-001").Outcome);
}
}
[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);
}
}