using Microsoft.Extensions.Options; using ZB.MOM.WW.MxGateway.Server.Configuration; namespace ZB.MOM.WW.MxGateway.Tests.Configuration; public sealed class GatewayOptionsValidatorTests { // Constructs the minimal valid GatewayOptions by relying on each sub-option's // design-default values; those defaults are validated separately in GatewayOptionsTests. private static GatewayOptions ValidOptions() => new(); // Returns enabled LDAP options that pass all checks except Port. // The class defaults already satisfy the blank-field checks; we only // override Enabled (must be true to exercise the port check) and Port. private static LdapOptions LdapOptionsWithPort(int port) => new() { Enabled = true, Port = port, }; private static GatewayOptions CloneWithLdap(GatewayOptions source, LdapOptions ldap) => new() { Authentication = source.Authentication, Ldap = ldap, Worker = source.Worker, Sessions = source.Sessions, Events = source.Events, Dashboard = source.Dashboard, Protocol = source.Protocol, Alarms = source.Alarms, Tls = source.Tls, }; private static GatewayOptions CloneWithTls(GatewayOptions source, TlsOptions tls) => new() { Authentication = source.Authentication, Ldap = source.Ldap, Worker = source.Worker, Sessions = source.Sessions, Events = source.Events, Dashboard = source.Dashboard, Protocol = source.Protocol, Alarms = source.Alarms, Tls = tls, }; [Fact] public void Validate_Succeeds_WithDefaultTlsOptions() { ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, ValidOptions()); Assert.True(result.Succeeded); } [Fact] public void Validate_Fails_WhenTlsValidityYearsOutOfRange() { GatewayOptions withBadTls = CloneWithTls(ValidOptions(), new TlsOptions { ValidityYears = 0 }); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, withBadTls); Assert.True(result.Failed); Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears")); } [Fact] public void Validate_Fails_WhenTlsValidityYearsTooLarge() { GatewayOptions withBadTls = CloneWithTls(ValidOptions(), new TlsOptions { ValidityYears = 101 }); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, withBadTls); Assert.True(result.Failed); Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:ValidityYears")); } [Fact] public void Validate_Fails_WhenAdditionalDnsNameBlank() { GatewayOptions options = CloneWithTls(ValidOptions(), new TlsOptions { AdditionalDnsNames = [" "] }); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:AdditionalDnsNames")); } [Fact] public void Validate_Fails_WhenSelfSignedCertPathBlank() { GatewayOptions options = CloneWithTls(ValidOptions(), new TlsOptions { SelfSignedCertPath = " " }); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Tls:SelfSignedCertPath must not be blank.")); } [Fact] public void Validate_Fails_WhenLdapPortIsZero() { GatewayOptions options = CloneWithLdap(ValidOptions(), LdapOptionsWithPort(0)); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains( result.Failures!, f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 0)")); } [Fact] public void Validate_Fails_WhenLdapPortExceedsMaximum() { GatewayOptions options = CloneWithLdap(ValidOptions(), LdapOptionsWithPort(70000)); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains( result.Failures!, f => f.Contains("MxGateway:Ldap:Port must be between 1 and 65535 (was 70000)")); } [Fact] public void Validate_Succeeds_WhenLdapEnabledWithValidPort() { GatewayOptions options = CloneWithLdap(ValidOptions(), LdapOptionsWithPort(389)); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Succeeded); } // ------------------------------------------------------------------------- // AlarmFallbackOptions validation // ------------------------------------------------------------------------- private static AlarmsOptions EnabledAlarmsWithFallback(AlarmFallbackOptions fallback) => new() { Enabled = true, DefaultArea = "Galaxy", Fallback = fallback, }; private static GatewayOptions CloneWithAlarms(GatewayOptions source, AlarmsOptions alarms) => new() { Authentication = source.Authentication, Ldap = source.Ldap, Worker = source.Worker, Sessions = source.Sessions, Events = source.Events, Dashboard = source.Dashboard, Protocol = source.Protocol, Alarms = alarms, Tls = source.Tls, }; [Fact] public void Validate_Succeeds_WhenAlarmsDisabled_FallbackNotValidated() { // Even an invalid Mode is acceptable when Enabled = false. GatewayOptions options = CloneWithAlarms( ValidOptions(), new AlarmsOptions { Enabled = false, Fallback = new AlarmFallbackOptions { Mode = "InvalidMode" }, }); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Succeeded); } [Fact] public void Validate_Succeeds_WhenAlarmsEnabled_DefaultAutoConfig() { // Default AlarmFallbackOptions (Mode="Auto") must pass validation when alarms are enabled. GatewayOptions options = CloneWithAlarms( ValidOptions(), EnabledAlarmsWithFallback(new AlarmFallbackOptions())); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Succeeded); } [Theory] [InlineData("Auto")] [InlineData("ForceAlarmManager")] [InlineData("ForceSubtag")] [InlineData("auto")] [InlineData("FORCESUBTAG")] public void Validate_Succeeds_WhenAlarmsEnabled_RecognisedMode(string mode) { AlarmsOptions alarms = mode.Equals("ForceSubtag", StringComparison.OrdinalIgnoreCase) // ForceSubtag needs either UseGalaxyRepository=true (default) or IncludeAttributes. ? EnabledAlarmsWithFallback(new AlarmFallbackOptions { Mode = mode }) : EnabledAlarmsWithFallback(new AlarmFallbackOptions { Mode = mode }); GatewayOptions options = CloneWithAlarms(ValidOptions(), alarms); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Succeeded); } [Fact] public void Validate_Fails_WhenAlarmsEnabled_InvalidMode() { GatewayOptions options = CloneWithAlarms( ValidOptions(), EnabledAlarmsWithFallback(new AlarmFallbackOptions { Mode = "InvalidMode" })); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains(result.Failures!, f => f.Contains("MxGateway:Alarms:Fallback") && f.Contains("Mode")); } [Fact] public void Validate_Fails_WhenForceSubtag_NoGalaxyRepository_NoIncludes() { // ForceSubtag without galaxy repository and without IncludeAttributes must fail. GatewayOptions options = CloneWithAlarms( ValidOptions(), EnabledAlarmsWithFallback(new AlarmFallbackOptions { Mode = "ForceSubtag", Discovery = new AlarmDiscoveryOptions { UseGalaxyRepository = false, IncludeAttributes = [], }, })); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains( result.Failures!, f => f.Contains("ForceSubtag") && f.Contains("Discovery")); } [Fact] public void Validate_Succeeds_WhenForceSubtag_NoGalaxyRepository_WithIncludes() { // ForceSubtag without galaxy repository is allowed when IncludeAttributes is non-empty. GatewayOptions options = CloneWithAlarms( ValidOptions(), EnabledAlarmsWithFallback(new AlarmFallbackOptions { Mode = "ForceSubtag", Discovery = new AlarmDiscoveryOptions { UseGalaxyRepository = false, IncludeAttributes = ["attr1"], }, })); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Succeeded); } [Fact] public void Validate_Succeeds_WhenForceSubtag_WithGalaxyRepository() { // ForceSubtag + UseGalaxyRepository=true (default) must pass even without IncludeAttributes. GatewayOptions options = CloneWithAlarms( ValidOptions(), EnabledAlarmsWithFallback(new AlarmFallbackOptions { Mode = "ForceSubtag", Discovery = new AlarmDiscoveryOptions { UseGalaxyRepository = true }, })); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Succeeded); } [Theory] [InlineData(0, nameof(AlarmFallbackOptions.ConsecutiveFailureThreshold))] [InlineData(-1, nameof(AlarmFallbackOptions.ConsecutiveFailureThreshold))] public void Validate_Fails_WhenConsecutiveFailureThresholdBelowOne(int value, string keyPart) { GatewayOptions options = CloneWithAlarms( ValidOptions(), EnabledAlarmsWithFallback(new AlarmFallbackOptions { ConsecutiveFailureThreshold = value })); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains(result.Failures!, f => f.Contains(keyPart)); _ = keyPart; // suppress unused-param warning } [Theory] [InlineData(0, nameof(AlarmFallbackOptions.FailbackProbeIntervalSeconds))] [InlineData(-5, nameof(AlarmFallbackOptions.FailbackProbeIntervalSeconds))] public void Validate_Fails_WhenFailbackProbeIntervalSecondsBelowOne(int value, string keyPart) { GatewayOptions options = CloneWithAlarms( ValidOptions(), EnabledAlarmsWithFallback(new AlarmFallbackOptions { FailbackProbeIntervalSeconds = value })); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains(result.Failures!, f => f.Contains(keyPart)); _ = keyPart; } [Theory] [InlineData(0, nameof(AlarmFallbackOptions.FailbackStableProbes))] [InlineData(-1, nameof(AlarmFallbackOptions.FailbackStableProbes))] public void Validate_Fails_WhenFailbackStableProbesBelowOne(int value, string keyPart) { GatewayOptions options = CloneWithAlarms( ValidOptions(), EnabledAlarmsWithFallback(new AlarmFallbackOptions { FailbackStableProbes = value })); ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options); Assert.True(result.Failed); Assert.Contains(result.Failures!, f => f.Contains(keyPart)); _ = keyPart; } }