using Shouldly; using Xunit; using ZB.MOM.WW.OtOpcUa.Host.Configuration; using ZB.MOM.WW.OtOpcUa.Security.Ldap; using LdapTransport = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapTransport; namespace ZB.MOM.WW.OtOpcUa.Host.IntegrationTests; /// /// Task 3 — verifies the net-new (built on the shared /// ZB.MOM.WW.Configuration OptionsValidatorBase/ValidationBuilder) gates on /// , and that when enabled it requires Server, /// SearchBase, and a valid Port. Failure messages carry the real "Ldap:" /// section prefix so they read correctly when surfaced at host startup. Also verifies the /// insecure-transport startup guard: a real-LDAP config selecting plaintext transport without /// fails fast at boot. /// public sealed class LdapOptionsValidatorTests { private static readonly LdapOptionsValidator Sut = new(); private const string InsecureTransportFailure = "LDAP transport is None (plaintext) but AllowInsecure is false — set Transport to Ldaps/StartTls or set AllowInsecure for dev."; /// Valid enabled options (a TLS transport) pass validation. [Fact] public void Valid_enabled_options_succeed() { var options = new LdapOptions { Enabled = true, Server = "ldap", SearchBase = "dc=x", Port = 389, Transport = LdapTransport.Ldaps, }; Sut.Validate(null, options).Succeeded.ShouldBeTrue(); } /// /// Insecure-transport guard: an enabled real-LDAP config that selects plaintext /// without fails /// startup validation with the guard message. /// [Fact] public void Enabled_with_plaintext_transport_and_not_allow_insecure_fails() { var options = new LdapOptions { Enabled = true, Server = "ldap", SearchBase = "dc=x", Port = 389, Transport = LdapTransport.None, AllowInsecure = false, }; var result = Sut.Validate(null, options); result.Failed.ShouldBeTrue(); result.Failures.ShouldContain(InsecureTransportFailure); } /// A TLS transport () satisfies the guard. [Fact] public void Enabled_with_ldaps_transport_passes_guard() { var options = new LdapOptions { Enabled = true, Server = "ldap", SearchBase = "dc=x", Port = 636, Transport = LdapTransport.Ldaps, }; Sut.Validate(null, options).Succeeded.ShouldBeTrue(); } /// /// Explicit opt-in: plaintext transport with set is /// permitted (dev/test escape hatch), so the guard does not trip. /// [Fact] public void Enabled_plaintext_with_allow_insecure_passes_guard() { var options = new LdapOptions { Enabled = true, Server = "ldap", SearchBase = "dc=x", Port = 389, Transport = LdapTransport.None, AllowInsecure = true, }; Sut.Validate(null, options).Succeeded.ShouldBeTrue(); } /// /// DevStubMode is exempt from the insecure-transport guard: the dev stub bypasses the real /// bind, so plaintext transport is irrelevant and must not block boot. /// [Fact] public void DevStubMode_with_plaintext_transport_passes_guard() { var options = new LdapOptions { Enabled = true, DevStubMode = true, Transport = LdapTransport.None, AllowInsecure = false, }; Sut.Validate(null, options).Succeeded.ShouldBeTrue(); } /// /// A disabled config is exempt from the insecure-transport guard even with plaintext /// transport — LDAP login never runs, so the guard must not trip. /// [Fact] public void Disabled_with_plaintext_transport_passes_guard() { var options = new LdapOptions { Enabled = false, Transport = LdapTransport.None, AllowInsecure = false, }; Sut.Validate(null, options).Succeeded.ShouldBeTrue(); } /// When LDAP is disabled all checks are skipped, so a blank config still passes. [Fact] public void Disabled_options_succeed_even_when_blank() { var options = new LdapOptions { Enabled = false, Server = string.Empty, SearchBase = string.Empty, Port = 0, }; Sut.Validate(null, options).Succeeded.ShouldBeTrue(); } /// /// When the dev stub is active the real LDAP fields are irrelevant (the bind is bypassed), so /// the gate skips the Server/SearchBase/Port checks even though LDAP is nominally enabled. /// [Fact] public void DevStubMode_options_succeed_even_when_server_blank() { var options = new LdapOptions { Enabled = true, DevStubMode = true, Server = string.Empty, SearchBase = string.Empty, Port = 0, }; Sut.Validate(null, options).Succeeded.ShouldBeTrue(); } /// Enabled with a blank server reports the required-server failure. [Fact] public void Enabled_with_blank_server_fails() { var options = new LdapOptions { Enabled = true, Server = string.Empty, SearchBase = "dc=x", Port = 389, }; var result = Sut.Validate(null, options); result.Failed.ShouldBeTrue(); result.Failures.ShouldContain("Ldap:Server is required when LDAP login is enabled."); } /// Enabled with a blank search base reports the required-search-base failure. [Fact] public void Enabled_with_blank_search_base_fails() { var options = new LdapOptions { Enabled = true, Server = "ldap", SearchBase = string.Empty, Port = 389, }; var result = Sut.Validate(null, options); result.Failed.ShouldBeTrue(); result.Failures.ShouldContain("Ldap:SearchBase is required when LDAP login is enabled."); } /// Enabled with port 0 reports the port-range failure using the shared primitive wording. [Fact] public void Enabled_with_zero_port_fails() { var options = new LdapOptions { Enabled = true, Server = "ldap", SearchBase = "dc=x", Port = 0, }; var result = Sut.Validate(null, options); result.Failed.ShouldBeTrue(); result.Failures.ShouldContain("Ldap:Port must be between 1 and 65535 (was 0)"); } }