using Microsoft.Extensions.Options; namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests; /// /// Regression tests: intervals /// are fed straight into new PeriodicTimer(...), which throws /// for a zero/negative period. A /// misconfigured appsettings.json must be rejected by an /// with a clear, key-naming message /// rather than crashing the hosted service with an opaque exception. /// public class HealthMonitoringOptionsValidatorTests { private static ValidateOptionsResult Validate(HealthMonitoringOptions options) => new HealthMonitoringOptionsValidator().Validate(Options.DefaultName, options); /// Verifies that the default options pass validation. [Fact] public void DefaultOptions_AreValid() { var result = Validate(new HealthMonitoringOptions()); Assert.True(result.Succeeded, result.FailureMessage); } /// Verifies that a zero report interval is rejected. [Fact] public void ZeroReportInterval_IsRejected() { var result = Validate(new HealthMonitoringOptions { ReportInterval = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("ReportInterval", result.FailureMessage); } /// Verifies that a negative report interval is rejected. [Fact] public void NegativeReportInterval_IsRejected() { var result = Validate(new HealthMonitoringOptions { ReportInterval = TimeSpan.FromSeconds(-1) }); Assert.True(result.Failed); Assert.Contains("ReportInterval", result.FailureMessage); } /// Verifies that a zero offline timeout is rejected. [Fact] public void ZeroOfflineTimeout_IsRejected() { var result = Validate(new HealthMonitoringOptions { OfflineTimeout = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("OfflineTimeout", result.FailureMessage); } /// Verifies that a zero central offline timeout is rejected. [Fact] public void ZeroCentralOfflineTimeout_IsRejected() { var result = Validate(new HealthMonitoringOptions { CentralOfflineTimeout = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("CentralOfflineTimeout", result.FailureMessage); } /// Verifies that a central offline timeout shorter than the offline timeout is rejected. [Fact] public void CentralOfflineTimeout_ShorterThanOfflineTimeout_IsRejected() { var result = Validate(new HealthMonitoringOptions { OfflineTimeout = TimeSpan.FromSeconds(60), CentralOfflineTimeout = TimeSpan.FromSeconds(30) }); Assert.True(result.Failed); Assert.Contains("CentralOfflineTimeout", result.FailureMessage); } /// Verifies that a zero metrics stale timeout is rejected. [Fact] public void ZeroMetricsStaleTimeout_IsRejected() { var result = Validate(new HealthMonitoringOptions { MetricsStaleTimeout = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("MetricsStaleTimeout", result.FailureMessage); } /// Verifies that a metrics stale timeout shorter than the report interval is rejected. [Fact] public void MetricsStaleTimeout_ShorterThanReportInterval_IsRejected() { // A stale window shorter than one report interval flags every site. var result = Validate(new HealthMonitoringOptions { ReportInterval = TimeSpan.FromSeconds(30), MetricsStaleTimeout = TimeSpan.FromSeconds(10) }); Assert.True(result.Failed); Assert.Contains("MetricsStaleTimeout", result.FailureMessage); } // ------------------------------------------------------------------ // LocalDb sustained-backlog badge (register row 33) // ------------------------------------------------------------------ /// Verifies that a non-positive LocalDb backlog threshold is rejected. /// The invalid backlog threshold under test. [Theory] [InlineData(0)] [InlineData(-1)] public void NonPositiveLocalDbBacklogThreshold_IsRejected(long threshold) { // An idle backlog of 0 already sits at or above a zero/negative threshold, so this // would badge every replicated site permanently. var result = Validate(new HealthMonitoringOptions { LocalDbBacklogThreshold = threshold }); Assert.True(result.Failed); Assert.Contains("LocalDbBacklogThreshold", result.FailureMessage); } /// Verifies that a zero LocalDb backlog sustained-for window is rejected. [Fact] public void ZeroLocalDbBacklogSustainedFor_IsRejected() { var result = Validate(new HealthMonitoringOptions { LocalDbBacklogSustainedFor = TimeSpan.Zero, }); Assert.True(result.Failed); Assert.Contains("LocalDbBacklogSustainedFor", result.FailureMessage); } /// Verifies that a LocalDb backlog sustained-for window shorter than the report interval is rejected. [Fact] public void LocalDbBacklogSustainedFor_ShorterThanReportInterval_IsRejected() { // A dwell shorter than one report interval can be satisfied by a single sample, which // collapses the sustained check back into the bare threshold it replaced — and that // fires on every daily purge burst. var result = Validate(new HealthMonitoringOptions { ReportInterval = TimeSpan.FromSeconds(30), LocalDbBacklogSustainedFor = TimeSpan.FromSeconds(10), }); Assert.True(result.Failed); Assert.Contains("LocalDbBacklogSustainedFor", result.FailureMessage); } /// Verifies that the default sustained window comfortably outlasts a purge drain. [Fact] public void DefaultSustainedWindow_ComfortablyOutlastsAPurgeDrain() { // Guards the intent of the default rather than its digits: the dwell must be long // enough that an expected purge spike drains inside it, or the badge becomes daily // noise and gets ignored. var options = new HealthMonitoringOptions(); Assert.True(options.LocalDbBacklogSustainedFor >= TimeSpan.FromMinutes(5)); Assert.True(options.LocalDbBacklogSustainedFor >= options.ReportInterval); } }