using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Shouldly; using Xunit; using ZB.MOM.WW.Configuration; using ZB.MOM.WW.OtOpcUa.Core.Abstractions; using ZB.MOM.WW.OtOpcUa.Core.Hosting; using ZB.MOM.WW.OtOpcUa.Driver.AbCip; using ZB.MOM.WW.OtOpcUa.Host.Configuration; using ZB.MOM.WW.OtOpcUa.Host.Drivers; using ZB.MOM.WW.Secrets.Abstractions; namespace ZB.MOM.WW.OtOpcUa.Host.Tests.Configuration; /// /// Final review I1 — the node-level Drivers:AbCip:DefaultTransport: binding (appsettings and the /// __ env-var form), precedence (explicit per-driver key > node default > compiled Native), the /// probe agreeing with the driver factory, and fail-fast validation. Every precedence test runs a pinned /// node (Libplctag) against the compiled default (Native), so a wiring that ignores the node value goes red. /// [Trait("Category", "Unit")] [Collection(AbTransportSharedPoolCollection.Name)] // resolves the registry, which configures the shared pool public sealed class AbCipDriverDefaultsOptionsTests { private const string NoKey = """{"devices":[{"hostAddress":"ab://10.0.0.1/1,0"}],"probe":{"enabled":false}}"""; private const string ExplicitNative = """{"transport":"Native","devices":[{"hostAddress":"ab://10.0.0.1/1,0"}],"probe":{"enabled":false}}"""; private static ServiceProvider Build(IConfiguration configuration) { var services = new ServiceCollection(); services.AddLogging(); services.AddSingleton(new StubSecretResolver()); services.AddAbCipDriverDefaults(configuration); services.AddOtOpcUaDriverFactories(); return services.BuildServiceProvider(); } private static IConfiguration Config(string? value) => new ConfigurationBuilder() .AddInMemoryCollection(value is null ? [] : new Dictionary { ["Drivers:AbCip:DefaultTransport"] = value }) .Build(); private static AbCipDriver Create(ServiceProvider sp, string json) => (AbCipDriver)sp.GetRequiredService().TryGet(DriverTypeNames.AbCip)!("drv-i1", json); /// /// #585 composition-root pin: Program.cs is top-level statements and cannot be booted in a unit test, so /// the registration lives in (which every /// test here builds through) and this asserts Program.cs still CALLS it. Deleting the call silently ignores an /// operator's Drivers:AbCip:DefaultTransport pin — every behaviour test above would stay green. /// [Fact] public void Program_registers_the_node_default_transport() { var dir = new DirectoryInfo(AppContext.BaseDirectory); while (dir is not null && !File.Exists(Path.Combine(dir.FullName, "ZB.MOM.WW.OtOpcUa.slnx"))) dir = dir.Parent; dir.ShouldNotBeNull(); var program = File.ReadAllText(Path.Combine(dir.FullName, "src", "Server", "ZB.MOM.WW.OtOpcUa.Host", "Program.cs")); System.Text.RegularExpressions.Regex.IsMatch( program, @"^builder\.Services\s*\.\s*AddAbCipDriverDefaults\(\s*builder\.Configuration\s*\)\s*;", System.Text.RegularExpressions.RegexOptions.Multiline) .ShouldBeTrue("Program.cs must call builder.Services.AddAbCipDriverDefaults(builder.Configuration), unconditionally " + "(at top level, not inside an if (hasDriver/hasAdmin) block — the probe on admin nodes needs it too)"); } [Fact] public void Absent_section_defaults_to_Native() { using var sp = Build(Config(null)); sp.GetRequiredService>().Value.ResolveDefaultTransport() .ShouldBe(AbCipTransport.Native); Create(sp, NoKey).EffectiveTransport.ShouldBe(AbCipTransport.Native); } [Theory] [InlineData("Libplctag", AbCipTransport.Libplctag)] [InlineData("libplctag", AbCipTransport.Libplctag)] [InlineData("NATIVE", AbCipTransport.Native)] [InlineData("", AbCipTransport.Native)] public void The_appsettings_key_binds(string value, AbCipTransport expected) { using var sp = Build(Config(value)); sp.GetRequiredService>().Value.ResolveDefaultTransport() .ShouldBe(expected); } /// The documented env-var override form, Drivers__AbCip__DefaultTransport. [Fact] public void The_double_underscore_environment_variable_binds() { const string prefix = "OTOPCUA_I1_TEST_"; Environment.SetEnvironmentVariable(prefix + "Drivers__AbCip__DefaultTransport", "Libplctag"); try { var configuration = new ConfigurationBuilder().AddEnvironmentVariables(prefix).Build(); using var sp = Build(configuration); Create(sp, NoKey).EffectiveTransport.ShouldBe(AbCipTransport.Libplctag); } finally { Environment.SetEnvironmentVariable(prefix + "Drivers__AbCip__DefaultTransport", null); } } /// A pinned node moves no-key drivers; an explicit per-driver key still wins over the pin. [Fact] public void Node_default_applies_only_to_drivers_without_a_key() { using var sp = Build(Config("Libplctag")); Create(sp, NoKey).EffectiveTransport.ShouldBe(AbCipTransport.Libplctag); Create(sp, ExplicitNative).EffectiveTransport.ShouldBe(AbCipTransport.Native); } /// The Test Connect probe resolves an absent key to the SAME node default the driver does. [Theory] [InlineData("Libplctag", AbCipTransport.Libplctag)] [InlineData(null, AbCipTransport.Native)] public void The_probe_uses_the_node_default(string? value, AbCipTransport expected) { using var sp = Build(Config(value)); var probe = sp.GetServices().OfType().ShouldHaveSingleItem(); probe.DefaultTransport.ShouldBe(expected); Create(sp, NoKey).EffectiveTransport.ShouldBe(expected); // driver and probe agree } /// The probe stays registered exactly once on a fused node (driver path + admin path). [Fact] public void The_factory_built_probe_is_still_deduplicated() { var services = new ServiceCollection(); services.AddOtOpcUaDriverProbes(); services.AddOtOpcUaDriverProbes(); using var sp = services.BuildServiceProvider(); sp.GetServices().OfType().Count().ShouldBe(1); } [Theory] [InlineData("Bogus")] [InlineData("1")] [InlineData("7")] [InlineData("Native,Libplctag")] public void A_bad_value_fails_validation_with_a_named_message(string value) { var result = new AbCipDriverDefaultsOptionsValidator().Validate( null, new AbCipDriverDefaultsOptions { DefaultTransport = value }); result.Failed.ShouldBeTrue(); result.Failures.ShouldContain(f => f.Contains("Drivers:AbCip:DefaultTransport") && f.Contains(value)); using var sp = Build(Config(value)); Should.Throw( () => sp.GetRequiredService>().Value) .Message.ShouldContain("Drivers:AbCip:DefaultTransport"); } [Theory] [InlineData("Native")] [InlineData("libplctag")] [InlineData(null)] [InlineData(" ")] public void A_good_or_blank_value_validates(string? value) => new AbCipDriverDefaultsOptionsValidator() .Validate(null, new AbCipDriverDefaultsOptions { DefaultTransport = value }) .Succeeded.ShouldBeTrue(); private sealed class StubSecretResolver : ISecretResolver { public Task GetAsync(SecretName name, CancellationToken ct) => Task.FromResult(null); } }