using Microsoft.Extensions.Options; namespace ZB.MOM.WW.ScadaBridge.HealthMonitoring.Tests; /// /// HealthMonitoring-014 regression: 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); [Fact] public void DefaultOptions_AreValid() { var result = Validate(new HealthMonitoringOptions()); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void ZeroReportInterval_IsRejected() { var result = Validate(new HealthMonitoringOptions { ReportInterval = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("ReportInterval", result.FailureMessage); } [Fact] public void NegativeReportInterval_IsRejected() { var result = Validate(new HealthMonitoringOptions { ReportInterval = TimeSpan.FromSeconds(-1) }); Assert.True(result.Failed); Assert.Contains("ReportInterval", result.FailureMessage); } [Fact] public void ZeroOfflineTimeout_IsRejected() { var result = Validate(new HealthMonitoringOptions { OfflineTimeout = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("OfflineTimeout", result.FailureMessage); } [Fact] public void ZeroCentralOfflineTimeout_IsRejected() { var result = Validate(new HealthMonitoringOptions { CentralOfflineTimeout = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("CentralOfflineTimeout", result.FailureMessage); } [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); } [Fact] public void ZeroMetricsStaleTimeout_IsRejected() { var result = Validate(new HealthMonitoringOptions { MetricsStaleTimeout = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("MetricsStaleTimeout", result.FailureMessage); } [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) // ------------------------------------------------------------------ [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); } [Fact] public void ZeroLocalDbBacklogSustainedFor_IsRejected() { var result = Validate(new HealthMonitoringOptions { LocalDbBacklogSustainedFor = TimeSpan.Zero, }); Assert.True(result.Failed); Assert.Contains("LocalDbBacklogSustainedFor", result.FailureMessage); } [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); } [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); } }