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.Host.Configuration; using ZB.MOM.WW.OtOpcUa.Runtime.Historian; namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; /// /// archreview 06/S-11 — verifies the net-new (built on /// the shared ZB.MOM.WW.Configuration OptionsValidatorBase/ValidationBuilder). /// The fail tier is deliberately narrow: only provably-crashing configs fail — an enabled /// historian (or an enabled AlarmHistorian, which sources its gateway connection from the /// ServerHistorian section) with an empty / non-absolute / non-http(s) Endpoint, i.e. /// exactly the configs that would otherwise throw deep in the /// gateway client factory at startup. Empty ApiKey / non-positive MaxTieClusterOverfetch /// stay operator warnings in (they degrade, not crash). /// public sealed class ServerHistorianOptionsValidatorTests { private static ServerHistorianOptionsValidator Sut(bool alarmHistorianEnabled = false) { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["AlarmHistorian:Enabled"] = alarmHistorianEnabled ? "true" : "false", }) .Build(); return new ServerHistorianOptionsValidator(configuration); } /// Enabled historian with an empty endpoint fails (would crash-loop in the factory). [Fact] public void Enabled_with_empty_endpoint_fails() { var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "" }); result.Failed.ShouldBeTrue(); result.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint")); } /// Enabled with a relative (non-absolute) URI fails. [Fact] public void Enabled_with_relative_uri_fails() { var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "host:5222" }); result.Failed.ShouldBeTrue(); result.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint")); } /// Enabled with a non-http(s) scheme (ftp) fails. [Fact] public void Enabled_with_bad_scheme_fails() { var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "ftp://host:5222" }); result.Failed.ShouldBeTrue(); result.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint")); } /// Enabled with a valid absolute https endpoint succeeds. [Fact] public void Enabled_with_valid_https_endpoint_succeeds() { var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = true, Endpoint = "https://host:5222" }); result.Succeeded.ShouldBeTrue(); } /// A disabled section may legitimately be empty — no failure. [Fact] public void Disabled_with_empty_endpoint_succeeds() { var result = Sut().Validate(null, new ServerHistorianOptions { Enabled = false, Endpoint = "" }); result.Succeeded.ShouldBeTrue(); } /// /// The residual corner: ServerHistorian:Enabled=false but AlarmHistorian:Enabled=true /// (which sources its connection from the ServerHistorian section) with an empty endpoint fails — /// and the message names BOTH sections so the operator understands why a disabled section is required. /// [Fact] public void Disabled_but_alarm_historian_enabled_with_empty_endpoint_fails_naming_both_sections() { var result = Sut(alarmHistorianEnabled: true) .Validate(null, new ServerHistorianOptions { Enabled = false, Endpoint = "" }); result.Failed.ShouldBeTrue(); result.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint") && f.Contains("AlarmHistorian:Enabled")); } /// Alarm-only mode with a valid endpoint succeeds. [Fact] public void Disabled_but_alarm_historian_enabled_with_valid_endpoint_succeeds() { var result = Sut(alarmHistorianEnabled: true) .Validate(null, new ServerHistorianOptions { Enabled = false, Endpoint = "https://host:5222" }); result.Succeeded.ShouldBeTrue(); } /// /// Wiring guard (the "register-AND-consume" trap): resolving IOptions<ServerHistorianOptions>.Value /// after throws /// for an enabled+empty section — proving the validator is /// attached to the options pipeline (not merely defined). ValidateOnStart reaching host start /// is already proven by the LDAP precedent. /// [Fact] public void AddValidatedOptions_wiring_throws_on_enabled_empty_endpoint() { var configuration = new ConfigurationBuilder() .AddInMemoryCollection(new Dictionary { ["ServerHistorian:Enabled"] = "true", ["ServerHistorian:Endpoint"] = "", }) .Build(); var services = new ServiceCollection(); services.AddSingleton(configuration); services.AddValidatedOptions( configuration, ServerHistorianOptions.SectionName); using var provider = services.BuildServiceProvider(); var ex = Should.Throw( () => _ = provider.GetRequiredService>().Value); ex.Failures.ShouldContain(f => f.Contains("ServerHistorian:Endpoint")); } }