using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using ZB.MOM.WW.MxGateway.Server.Configuration; namespace ZB.MOM.WW.MxGateway.Tests.Configuration; public sealed class GatewayOptionsTests { /// Verifies that options binding uses design defaults when no configuration is provided. [Fact] public void OptionsBinding_UsesDesignDefaults() { GatewayOptions options = BindOptions(new Dictionary()); Assert.Equal(AuthenticationMode.ApiKey, options.Authentication.Mode); Assert.Equal(@"C:\ProgramData\MxGateway\gateway-auth.db", options.Authentication.SqlitePath); Assert.Equal("MxGateway:ApiKeyPepper", options.Authentication.PepperSecretName); Assert.True(options.Authentication.RunMigrationsOnStartup); Assert.Equal(@"src\ZB.MOM.WW.MxGateway.Worker\bin\x86\Release\ZB.MOM.WW.MxGateway.Worker.exe", options.Worker.ExecutablePath); Assert.Equal(WorkerArchitecture.X86, options.Worker.RequiredArchitecture); Assert.Equal(30, options.Worker.StartupTimeoutSeconds); Assert.Equal(3, options.Worker.StartupProbeRetryAttempts); Assert.Equal(250, options.Worker.StartupProbeRetryDelayMilliseconds); Assert.Equal(2000, options.Worker.PipeConnectAttemptTimeoutMilliseconds); Assert.Equal(10, options.Worker.ShutdownTimeoutSeconds); Assert.Equal(5, options.Worker.HeartbeatIntervalSeconds); Assert.Equal(15, options.Worker.HeartbeatGraceSeconds); Assert.Equal(16 * 1024 * 1024, options.Worker.MaxMessageBytes); Assert.Equal(30, options.Sessions.DefaultCommandTimeoutSeconds); Assert.Equal(64, options.Sessions.MaxSessions); Assert.Equal(1800, options.Sessions.DefaultLeaseSeconds); Assert.Equal(30, options.Sessions.LeaseSweepIntervalSeconds); Assert.False(options.Sessions.AllowMultipleEventSubscribers); Assert.Equal(10_000, options.Events.QueueCapacity); Assert.Equal(EventBackpressurePolicy.FailFast, options.Events.BackpressurePolicy); Assert.True(options.Dashboard.Enabled); Assert.True(options.Dashboard.AllowAnonymousLocalhost); Assert.Equal(1_000, options.Dashboard.SnapshotIntervalMilliseconds); Assert.Equal(100, options.Dashboard.RecentFaultLimit); Assert.Equal(200, options.Dashboard.RecentSessionLimit); Assert.False(options.Dashboard.ShowTagValues); Assert.Equal(1u, options.Protocol.WorkerProtocolVersion); Assert.Equal(16 * 1024 * 1024, options.Protocol.MaxGrpcMessageBytes); } /// Verifies that options binding applies configuration overrides. [Fact] public void OptionsBinding_AppliesConfigurationOverrides() { GatewayOptions options = BindOptions( new Dictionary { ["MxGateway:Authentication:Mode"] = "Disabled", ["MxGateway:Worker:ExecutablePath"] = @"C:\Gateway\ZB.MOM.WW.MxGateway.Worker.exe", ["MxGateway:Sessions:MaxSessions"] = "12", ["MxGateway:Sessions:DefaultLeaseSeconds"] = "900", ["MxGateway:Events:QueueCapacity"] = "256", ["MxGateway:Dashboard:Enabled"] = "false", ["MxGateway:Protocol:MaxGrpcMessageBytes"] = "8388608" }); Assert.Equal(AuthenticationMode.Disabled, options.Authentication.Mode); Assert.Equal(@"C:\Gateway\ZB.MOM.WW.MxGateway.Worker.exe", options.Worker.ExecutablePath); Assert.Equal(12, options.Sessions.MaxSessions); Assert.Equal(900, options.Sessions.DefaultLeaseSeconds); Assert.Equal(256, options.Events.QueueCapacity); Assert.False(options.Dashboard.Enabled); Assert.Equal(8 * 1024 * 1024, options.Protocol.MaxGrpcMessageBytes); } /// Verifies that invalid configuration values fail with expected error messages. /// Configuration key being validated. /// Configuration value being tested. /// Expected validation error message. [Theory] [InlineData("MxGateway:Worker:ExecutablePath", "worker.dll", "MxGateway:Worker:ExecutablePath must point to a .exe file.")] [InlineData("MxGateway:Worker:StartupProbeRetryAttempts", "0", "MxGateway:Worker:StartupProbeRetryAttempts must be greater than zero.")] [InlineData("MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds", "0", "MxGateway:Worker:PipeConnectAttemptTimeoutMilliseconds must be greater than zero.")] [InlineData("MxGateway:Sessions:DefaultLeaseSeconds", "0", "MxGateway:Sessions:DefaultLeaseSeconds must be greater than zero.")] [InlineData("MxGateway:Sessions:LeaseSweepIntervalSeconds", "0", "MxGateway:Sessions:LeaseSweepIntervalSeconds must be greater than zero.")] [InlineData("MxGateway:Events:QueueCapacity", "0", "MxGateway:Events:QueueCapacity must be greater than zero.")] [InlineData("MxGateway:Protocol:MaxGrpcMessageBytes", "0", "MxGateway:Protocol:MaxGrpcMessageBytes must be between")] [InlineData("MxGateway:Authentication:PepperSecretName", "", "MxGateway:Authentication:PepperSecretName is required")] [InlineData("MxGateway:Dashboard:GroupToRole:GwAdmin", "Sysadmin", "MxGateway:Dashboard:GroupToRole['GwAdmin'] must be 'Admin' or 'Viewer'.")] public void Validation_InvalidConfiguration_FailsClearly(string key, string value, string expectedFailure) { OptionsValidationException exception = Assert.Throws(() => _ = BindOptions(new Dictionary { [key] = value })); Assert.Contains(exception.Failures, failure => failure.Contains(expectedFailure, StringComparison.Ordinal)); } /// Verifies that pepper secret names are redacted in the effective configuration. [Fact] public void EffectiveConfiguration_RedactsPepperSecretName() { using ServiceProvider services = BuildServices( new Dictionary { ["MxGateway:Authentication:PepperSecretName"] = "RawPepperSecretName" }); IGatewayConfigurationProvider provider = services.GetRequiredService(); EffectiveGatewayConfiguration configuration = provider.GetEffectiveConfiguration(); Assert.Equal(GatewayConfigurationProvider.RedactedValue, configuration.Authentication.PepperSecretName); Assert.DoesNotContain( "RawPepperSecretName", System.Text.Json.JsonSerializer.Serialize(configuration), StringComparison.Ordinal); } private static GatewayOptions BindOptions(IReadOnlyDictionary configurationValues) { using ServiceProvider services = BuildServices(configurationValues); return services.GetRequiredService>().Value; } private static ServiceProvider BuildServices(IReadOnlyDictionary configurationValues) { IConfigurationRoot configuration = new ConfigurationBuilder() .AddInMemoryCollection(configurationValues) .Build(); ServiceCollection services = new(); services.AddSingleton(configuration); services.AddGatewayConfiguration(configuration); return services.BuildServiceProvider(validateScopes: true); } }