using Microsoft.Extensions.Options; using Shouldly; using Xunit; namespace ZB.MOM.WW.OtOpcUa.Cluster.Tests; /// /// A FetchAndCache node with no endpoint or no key does not error — it simply never /// applies a deployment, and the deploy times out naming a node that is otherwise healthy. These /// tests make that misconfiguration a loud host-start failure instead. /// public class ConfigSourceOptionsValidatorTests { private static ValidateOptionsResult Validate(ConfigSourceOptions o) => new ConfigSourceOptionsValidator().Validate(ConfigSourceOptions.SectionName, o); private static ConfigSourceOptions ValidFetch() => new() { Mode = ConfigSourceOptions.ModeFetchAndCache, CentralFetchEndpoints = ["http://central-1:4055", "http://central-2:4055"], ApiKey = "shared-key", FetchTimeoutSeconds = 30, }; [Fact] public void Default_options_are_valid() { // The default is Direct with everything empty — the pre-Phase-3 behaviour. A node that never // sets this section must validate, which is the premise of the dark switch. Validate(new ConfigSourceOptions()).Succeeded.ShouldBeTrue(); } [Fact] public void Unknown_mode_fails() { var result = Validate(new ConfigSourceOptions { Mode = "cache" }); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain("cache"); } [Fact] public void Mode_matching_is_case_insensitive() { Validate(new ConfigSourceOptions { Mode = "fetchandcache", CentralFetchEndpoints = ["http://central-1:4055"], ApiKey = "k", }).Succeeded.ShouldBeTrue(); } [Fact] public void FetchAndCache_with_no_endpoints_fails() { var o = ValidFetch(); o.CentralFetchEndpoints = []; var result = Validate(o); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain(nameof(ConfigSourceOptions.CentralFetchEndpoints)); } [Fact] public void FetchAndCache_with_a_non_http_endpoint_fails() { var o = ValidFetch(); o.CentralFetchEndpoints = ["central-1:4055"]; var result = Validate(o); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain("central-1:4055"); } [Fact] public void FetchAndCache_with_an_empty_key_fails() { var o = ValidFetch(); o.ApiKey = ""; var result = Validate(o); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain(nameof(ConfigSourceOptions.ApiKey)); } [Fact] public void FetchAndCache_with_a_non_positive_timeout_fails() { var o = ValidFetch(); o.FetchTimeoutSeconds = 0; var result = Validate(o); result.Failed.ShouldBeTrue(); result.FailureMessage.ShouldContain(nameof(ConfigSourceOptions.FetchTimeoutSeconds)); } [Fact] public void A_fully_populated_FetchAndCache_config_is_valid() { Validate(ValidFetch()).Succeeded.ShouldBeTrue(); } [Fact] public void Direct_ignores_the_empty_fetch_surface() { // Under Direct the endpoints/key are never read, so their emptiness must not fail validation — // otherwise every admin-only and every Direct node would be forced to carry fetch config. Validate(new ConfigSourceOptions { Mode = ConfigSourceOptions.ModeDirect, CentralFetchEndpoints = [], ApiKey = "", FetchTimeoutSeconds = 0, }).Succeeded.ShouldBeTrue(); } }