using Mbproxy.Options; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Shouldly; using Xunit; namespace Mbproxy.Tests.Options; /// /// Verifies that binds correctly from /// and that schema-level validation fires. /// [Trait("Category", "Unit")] public sealed class MbproxyOptionsBindingTests { // ------------------------------------------------------------------------- // Helper: build MbproxyOptions directly from an in-memory configuration. // We configure the DI container with IConfiguration so BindConfiguration works. // ------------------------------------------------------------------------- private static MbproxyOptions BindOptions(Dictionary values) { var config = new ConfigurationBuilder() .AddInMemoryCollection(values) .Build(); var services = new ServiceCollection(); // Register IConfiguration so BindConfiguration("Mbproxy") can resolve it. services.AddSingleton(config); services .AddOptions() .BindConfiguration("Mbproxy"); var provider = services.BuildServiceProvider(); return provider.GetRequiredService>().CurrentValue; } // ------------------------------------------------------------------------- // Test 1 — global BCD tags bind correctly // ------------------------------------------------------------------------- [Fact] public void MbproxyOptionsBinding_BindsGlobalBcdTags_From_appsettings() { var options = BindOptions(new Dictionary { ["Mbproxy:BcdTags:Global:0:Address"] = "1072", ["Mbproxy:BcdTags:Global:0:Width"] = "16", ["Mbproxy:BcdTags:Global:1:Address"] = "1080", ["Mbproxy:BcdTags:Global:1:Width"] = "32", }); options.BcdTags.Global.Count.ShouldBe(2); options.BcdTags.Global[0].Address.ShouldBe((ushort)1072); options.BcdTags.Global[0].Width.ShouldBe((byte)16); options.BcdTags.Global[1].Address.ShouldBe((ushort)1080); options.BcdTags.Global[1].Width.ShouldBe((byte)32); } // ------------------------------------------------------------------------- // Test 2 — per-PLC Add and Remove override lists bind correctly // ------------------------------------------------------------------------- [Fact] public void MbproxyOptionsBinding_BindsPerPlcAddAndRemove() { var options = BindOptions(new Dictionary { ["Mbproxy:Plcs:0:Name"] = "Line1-Mixer", ["Mbproxy:Plcs:0:ListenPort"] = "5020", ["Mbproxy:Plcs:0:Host"] = "10.0.1.1", ["Mbproxy:Plcs:0:BcdTags:Add:0:Address"] = "1200", ["Mbproxy:Plcs:0:BcdTags:Add:0:Width"] = "32", ["Mbproxy:Plcs:0:BcdTags:Remove:0"] = "1080", }); options.Plcs.Count.ShouldBe(1); var plc = options.Plcs[0]; plc.Name.ShouldBe("Line1-Mixer"); plc.ListenPort.ShouldBe(5020); plc.Host.ShouldBe("10.0.1.1"); plc.BcdTags.ShouldNotBeNull(); plc.BcdTags!.Add.Count.ShouldBe(1); plc.BcdTags.Add[0].Address.ShouldBe((ushort)1200); plc.BcdTags.Add[0].Width.ShouldBe((byte)32); plc.BcdTags.Remove.Count.ShouldBe(1); plc.BcdTags.Remove[0].ShouldBe((ushort)1080); } // ------------------------------------------------------------------------- // Test 3 — defaults apply when the "Mbproxy" section is absent // ------------------------------------------------------------------------- [Fact] public void MbproxyOptionsBinding_DefaultsAreApplied_WhenSectionMissing() { var options = BindOptions(new Dictionary()); options.AdminPort.ShouldBe(8080); options.Connection.BackendConnectTimeoutMs.ShouldBe(3000); options.Connection.BackendRequestTimeoutMs.ShouldBe(3000); options.Resilience.BackendConnect.MaxAttempts.ShouldBe(3); options.Resilience.BackendConnect.BackoffMs.ShouldBe([100, 500, 2000]); options.Resilience.ListenerRecovery.SteadyStateMs.ShouldBe(30000); options.Resilience.ListenerRecovery.InitialBackoffMs.ShouldBe([1000, 2000, 5000, 15000, 30000]); options.Plcs.ShouldBeEmpty(); options.BcdTags.Global.ShouldBeEmpty(); } // ------------------------------------------------------------------------- // Test 4 — validator rejects Width != 16 && != 32 (schema-level only) // ------------------------------------------------------------------------- [Fact] public void MbproxyOptionsBinding_RejectsInvalidWidth() { // Build options with an invalid Width=8. var config = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["Mbproxy:BcdTags:Global:0:Address"] = "1072", ["Mbproxy:BcdTags:Global:0:Width"] = "8", // invalid: not 16 or 32 }) .Build(); // Get creates a new instance and populates it — works with init-only properties. var options = config.GetSection("Mbproxy").Get() ?? new MbproxyOptions(); // Call the validator directly to check schema-level rejection. var validator = new MbproxyOptionsValidator(); var result = validator.Validate(null, options); result.Failed.ShouldBeTrue("Width=8 should fail schema validation"); result.Failures.ShouldNotBeEmpty(); } }