using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using MxGateway.Server.Configuration; namespace MxGateway.Tests.Configuration; public sealed class GatewayOptionsTests { [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\MxGateway.Worker\bin\x86\Release\MxGateway.Worker.exe", options.Worker.ExecutablePath); Assert.Equal(WorkerArchitecture.X86, options.Worker.RequiredArchitecture); Assert.Equal(30, options.Worker.StartupTimeoutSeconds); 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.False(options.Sessions.AllowMultipleEventSubscribers); Assert.Equal(10_000, options.Events.QueueCapacity); Assert.Equal(EventBackpressurePolicy.FailFast, options.Events.BackpressurePolicy); Assert.True(options.Dashboard.Enabled); Assert.Equal("/dashboard", options.Dashboard.PathBase); Assert.True(options.Dashboard.RequireAdminScope); Assert.False(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); } [Fact] public void OptionsBinding_AppliesConfigurationOverrides() { GatewayOptions options = BindOptions( new Dictionary { ["MxGateway:Authentication:Mode"] = "Disabled", ["MxGateway:Worker:ExecutablePath"] = @"C:\Gateway\MxGateway.Worker.exe", ["MxGateway:Sessions:MaxSessions"] = "12", ["MxGateway:Events:QueueCapacity"] = "256", ["MxGateway:Dashboard:Enabled"] = "false" }); Assert.Equal(AuthenticationMode.Disabled, options.Authentication.Mode); Assert.Equal(@"C:\Gateway\MxGateway.Worker.exe", options.Worker.ExecutablePath); Assert.Equal(12, options.Sessions.MaxSessions); Assert.Equal(256, options.Events.QueueCapacity); Assert.False(options.Dashboard.Enabled); } [Theory] [InlineData("MxGateway:Worker:ExecutablePath", "worker.dll", "MxGateway:Worker:ExecutablePath must point to a .exe file.")] [InlineData("MxGateway:Events:QueueCapacity", "0", "MxGateway:Events:QueueCapacity must be greater than zero.")] [InlineData("MxGateway:Authentication:PepperSecretName", "", "MxGateway:Authentication:PepperSecretName is required")] [InlineData("MxGateway:Dashboard:PathBase", "dashboard", "MxGateway:Dashboard:PathBase must start with '/'.")] 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)); } [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(); return services.BuildServiceProvider(validateScopes: true); } }