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
- Availability group collector (replica and database replica states, queues, commit times) and rules: SQLI-AG-001 unhealthy/disconnected replica or suspended data movement (scripts HADR RESUME), SQLI-AG-002 secondary behind (estimated data loss and redo time) - SQL Agent collector using only granted msdb tables (no EXECUTE on msdb functions): SQLI-JOB-001 failed job with the failing step's message (backup/CHECKDB jobs are High), SQLI-JOB-002 SQL Agent not running - Integration tests create a real availability group (CLUSTER_TYPE NONE) and a failing job in the test container (HADR and Agent enabled) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E3EExwndy8Z22EK2Qpt6BK
117 lines
5.1 KiB
C#
117 lines
5.1 KiB
C#
using SQLInsight.Core.Data;
|
|
using SQLInsight.Core.Findings;
|
|
using SQLInsight.Core.Instances;
|
|
using SQLInsight.Rules.Availability;
|
|
using SQLInsight.Rules.Engine;
|
|
using static SQLInsight.Rules.Tests.TestInstances;
|
|
|
|
namespace SQLInsight.Rules.Tests;
|
|
|
|
public class AvailabilityRuleTests
|
|
{
|
|
private static RuleResult Run(IRule rule, RuleContext context) => Assert.Single(new RuleEngine([rule]).Evaluate(context));
|
|
|
|
private static RuleContext Hadr(AvailabilityGroupSnapshot snapshot)
|
|
{
|
|
var info = new InstanceInfo { ServerName = "n1", Version = SqlServerVersion.Parse("16.0.4295.3"), Edition = "Enterprise", EngineEdition = EngineEdition.Enterprise, IsHadrEnabled = true };
|
|
var context = new RuleContext(info, InstanceCapabilities.From(info), Now);
|
|
context.Data.Set(snapshot);
|
|
return context;
|
|
}
|
|
|
|
private static DatabaseReplicaState Db(string replica, bool primary, DateTime? commit = null, long redoKb = 0, long redoRate = 1000, bool suspended = false, string health = "HEALTHY") => new()
|
|
{
|
|
Group = "ag1", Database = "Sales", Replica = replica, IsPrimaryReplica = primary, LastCommitTime = commit,
|
|
RedoQueueKb = redoKb, RedoRateKbPerSecond = redoRate, IsSuspended = suspended, SuspendReason = suspended ? "SUSPEND_FROM_USER" : null,
|
|
SynchronizationHealth = health, SynchronizationState = "SYNCHRONIZED", LogSendQueueKb = 0,
|
|
};
|
|
|
|
[Fact]
|
|
public void Not_measurable_without_hadr()
|
|
{
|
|
Assert.Equal(RuleOutcome.NotMeasurable, Run(new AvailabilityGroupHealthRule(), Context("16.0.4295.3")).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Healthy_group_passes()
|
|
{
|
|
var snapshot = new AvailabilityGroupSnapshot
|
|
{
|
|
CapturedAt = Now,
|
|
Replicas = [new ReplicaState { Group = "ag1", Replica = "n1", Role = "PRIMARY", ConnectedState = "CONNECTED", SynchronizationHealth = "HEALTHY" }],
|
|
Databases = [Db("n1", true, Now.UtcDateTime), Db("n2", false, Now.UtcDateTime)],
|
|
};
|
|
|
|
Assert.Equal(RuleOutcome.Passed, Run(new AvailabilityGroupHealthRule(), Hadr(snapshot)).Outcome);
|
|
Assert.Equal(RuleOutcome.Passed, Run(new AvailabilityGroupLagRule(), Hadr(snapshot)).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Disconnected_replica_and_suspended_database_are_high()
|
|
{
|
|
var snapshot = new AvailabilityGroupSnapshot
|
|
{
|
|
CapturedAt = Now,
|
|
Replicas = [new ReplicaState { Group = "ag1", Replica = "n2", Role = "SECONDARY", ConnectedState = "DISCONNECTED", SynchronizationHealth = "NOT_HEALTHY" }],
|
|
Databases = [Db("n2", false, suspended: true, health: "NOT_HEALTHY")],
|
|
};
|
|
|
|
var findings = Run(new AvailabilityGroupHealthRule(), Hadr(snapshot)).Findings;
|
|
|
|
Assert.Equal(2, findings.Count);
|
|
Assert.All(findings, f => Assert.Equal(Severity.High, f.Severity));
|
|
Assert.Contains(findings, f => f.FixScript == "ALTER DATABASE [Sales] SET HADR RESUME;");
|
|
}
|
|
|
|
[Fact]
|
|
public void Lag_reports_data_loss_and_redo_time()
|
|
{
|
|
var snapshot = new AvailabilityGroupSnapshot
|
|
{
|
|
CapturedAt = Now,
|
|
Replicas = [],
|
|
Databases = [Db("n1", true, Now.UtcDateTime), Db("n2", false, Now.UtcDateTime.AddMinutes(-5), redoKb: 2_000_000, redoRate: 1000)],
|
|
};
|
|
|
|
var finding = Assert.Single(Run(new AvailabilityGroupLagRule(), Hadr(snapshot)).Findings);
|
|
|
|
Assert.Contains("300 s datatab", finding.Title, StringComparison.Ordinal);
|
|
Assert.Contains("redo ca. 33 min", finding.Title, StringComparison.Ordinal);
|
|
}
|
|
|
|
private static AgentJobSnapshot Jobs(bool running, params AgentJob[] jobs) => new() { CapturedAt = Now, IsAgentRunning = running, Jobs = jobs };
|
|
|
|
[Fact]
|
|
public void Failed_backup_job_is_high_with_step_message()
|
|
{
|
|
var job = new AgentJob { Name = "DatabaseBackup - USER_DATABASES - FULL", IsEnabled = true, LastRunStatus = 0, LastFailedStepName = "Backup", LastFailedStepMessage = "Cannot open backup device 'X:\\bak'" };
|
|
|
|
var finding = Assert.Single(Run(new FailedJobRule(), With(Jobs(true, job))).Findings);
|
|
|
|
Assert.Equal(Severity.High, finding.Severity);
|
|
Assert.Contains(finding.Evidence, e => e.Value.Contains("Cannot open backup device", StringComparison.Ordinal));
|
|
}
|
|
|
|
[Fact]
|
|
public void Disabled_or_succeeding_jobs_pass()
|
|
{
|
|
var jobs = Jobs(true,
|
|
new AgentJob { Name = "Old", IsEnabled = false, LastRunStatus = 0 },
|
|
new AgentJob { Name = "Fine", IsEnabled = true, LastRunStatus = 1 },
|
|
new AgentJob { Name = "Never run", IsEnabled = true });
|
|
|
|
Assert.Equal(RuleOutcome.Passed, Run(new FailedJobRule(), With(jobs)).Outcome);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stopped_agent_is_high_when_jobs_exist_but_not_on_express()
|
|
{
|
|
var jobs = Jobs(false, new AgentJob { Name = "Backup", IsEnabled = true });
|
|
|
|
Assert.Equal(Severity.High, Assert.Single(Run(new AgentStoppedRule(), With(jobs)).Findings).Severity);
|
|
|
|
var express = Context("16.0.4295.3", edition: EngineEdition.Express);
|
|
express.Data.Set(jobs);
|
|
Assert.Equal(RuleOutcome.Passed, Run(new AgentStoppedRule(), express).Outcome);
|
|
}
|
|
}
|