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();
}
}