using Microsoft.Extensions.Options; namespace ZB.MOM.WW.ScadaBridge.Communication.Tests; /// /// Regression: timeouts feed per-pattern Ask /// deadlines and gRPC keepalive/stream-lifetime settings; a zero/negative value /// (or a non-positive GrpcMaxConcurrentStreams) must be rejected at startup /// by an with a clear, key-naming message /// rather than surfacing at first Ask/gRPC use. /// public class CommunicationOptionsValidatorTests { private static ValidateOptionsResult Validate(CommunicationOptions options) => new CommunicationOptionsValidator().Validate(Options.DefaultName, options); [Fact] public void DefaultOptions_AreValid() { var result = Validate(new CommunicationOptions()); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void ZeroDeploymentTimeout_IsRejected() { var result = Validate(new CommunicationOptions { DeploymentTimeout = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("DeploymentTimeout", result.FailureMessage); } [Fact] public void NonPositiveGrpcMaxConcurrentStreams_IsRejected() { var result = Validate(new CommunicationOptions { GrpcMaxConcurrentStreams = 0 }); Assert.True(result.Failed); Assert.Contains("GrpcMaxConcurrentStreams", result.FailureMessage); } // ── Aggregated live alarm cache options (plan #10, Task 6) ─────────────────── [Fact] public void ZeroLiveAlarmCacheLinger_IsValid() { // Zero linger = stop the aggregator immediately when the last viewer leaves. var result = Validate(new CommunicationOptions { LiveAlarmCacheLinger = TimeSpan.Zero }); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void NegativeLiveAlarmCacheLinger_IsRejected() { var result = Validate(new CommunicationOptions { LiveAlarmCacheLinger = TimeSpan.FromSeconds(-1) }); Assert.True(result.Failed); Assert.Contains("LiveAlarmCacheLinger", result.FailureMessage); } [Fact] public void ZeroLiveAlarmCacheReconcileInterval_IsRejected() { var result = Validate(new CommunicationOptions { LiveAlarmCacheReconcileInterval = TimeSpan.Zero }); Assert.True(result.Failed); Assert.Contains("LiveAlarmCacheReconcileInterval", result.FailureMessage); } [Fact] public void ZeroLiveAlarmCacheSeedConcurrency_IsRejected() { var result = Validate(new CommunicationOptions { LiveAlarmCacheSeedConcurrency = 0 }); Assert.True(result.Failed); Assert.Contains("LiveAlarmCacheSeedConcurrency", result.FailureMessage); } [Fact] public void ExcessiveLiveAlarmCacheSeedConcurrency_IsRejected() { var result = Validate(new CommunicationOptions { LiveAlarmCacheSeedConcurrency = 65 }); Assert.True(result.Failed); Assert.Contains("LiveAlarmCacheSeedConcurrency", result.FailureMessage); } [Fact] public void ZeroLiveAlarmCacheMaxSubscribersPerSite_IsRejected() { var result = Validate(new CommunicationOptions { LiveAlarmCacheMaxSubscribersPerSite = 0 }); Assert.True(result.Failed); Assert.Contains("LiveAlarmCacheMaxSubscribersPerSite", result.FailureMessage); } // ── R2 T10: live-delta publish-coalescing window (N6) ──────────────────────── [Fact] public void ZeroLiveAlarmCachePublishCoalesce_IsValid() { // Zero = publish per delta (legacy behavior). var result = Validate(new CommunicationOptions { LiveAlarmCachePublishCoalesce = TimeSpan.Zero }); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void NegativeLiveAlarmCachePublishCoalesce_IsRejected() { var result = Validate(new CommunicationOptions { LiveAlarmCachePublishCoalesce = TimeSpan.FromMilliseconds(-1) }); Assert.True(result.Failed); Assert.Contains("LiveAlarmCachePublishCoalesce", result.FailureMessage); } // ── gRPC central transport endpoints ───────────────────────────────────────── // gRPC (CentralControlService) is the only site→central transport after the // ClusterClient→gRPC migration's Phase 4. This role-agnostic validator only rejects BLANK // entries; an EMPTY list is valid (central nodes legitimately declare none). The role-aware // "a Site must list at least one endpoint" rule lives in StartupValidator, tested there. [Fact] public void EmptyEndpoints_IsValid() { // A central node hosts CentralControlService; it does not dial it, so it declares none. var result = Validate(new CommunicationOptions { CentralGrpcEndpoints = new List(), }); Assert.True(result.Succeeded, result.FailureMessage); } [Fact] public void BlankEndpoint_IsRejected() { var result = Validate(new CommunicationOptions { CentralGrpcEndpoints = new List { " " }, }); Assert.True(result.Failed); Assert.Contains("CentralGrpcEndpoints", result.FailureMessage); } [Fact] public void Endpoints_IsValid() { var result = Validate(new CommunicationOptions { CentralGrpcEndpoints = new List { "http://scadabridge-central-a:8083", "http://scadabridge-central-b:8083", }, }); Assert.True(result.Succeeded, result.FailureMessage); } }