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; } [Fact] public void A_cluster_node_or_witness_that_is_not_up_is_high() { var snapshot = new AvailabilityGroupSnapshot { CapturedAt = Now, Replicas = [], Databases = [], ClusterMembers = [ new ClusterMember("SQL01", "CLUSTER_NODE", 1, "UP", 1), new ClusterMember("SQL02", "CLUSTER_NODE", 0, "DOWN", 1), new ClusterMember("Cloud Witness", "CLOUD_WITNESS", 1, "UP", 1), ], }; var finding = Assert.Single(Run(new ClusterMemberDownRule(), Hadr(snapshot)).Findings); Assert.Equal(Severity.High, finding.Severity); Assert.Equal("cluster member: SQL02", finding.Subject); Assert.Equal("2 af 3", finding.Evidence.Single(e => e.Name == "Medlemmer oppe").Value); } 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); } }