diff --git a/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.Ldap.Tests/Integration/GLAuthIntegrationTests.cs b/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.Ldap.Tests/Integration/GLAuthIntegrationTests.cs index fe94dee..be86a05 100644 --- a/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.Ldap.Tests/Integration/GLAuthIntegrationTests.cs +++ b/ZB.MOM.WW.Auth/tests/ZB.MOM.WW.Auth.Ldap.Tests/Integration/GLAuthIntegrationTests.cs @@ -99,4 +99,70 @@ public sealed class GLAuthIntegrationTests $"Authentication failed: {result.Failure} (server={server}:{port}, user={user})"); Assert.NotEmpty(result.Groups); } + + /// + /// Failover: primary endpoint points at a closed local port (refused immediately), + /// fallback at the real GLAuth. The login must succeed via the fallback, and a second + /// login must also succeed (sticky preference — no re-walk through the dead primary). + /// Skipped unless ZB_LDAP_IT=1 is set; skipped again if the server is unreachable. + /// + [SkippableFact] + public async Task Authenticate_FailsOverFromDeadPrimary_ToRealGLAuth() + { + // ------------------------------------------------------------------ opt-in gate + Skip.IfNot( + Environment.GetEnvironmentVariable("ZB_LDAP_IT") == "1", + "Set ZB_LDAP_IT=1 and a reachable GLAuth to run."); + + // ------------------------------------------------------------------ read config + var server = Environment.GetEnvironmentVariable("ZB_LDAP_SERVER") ?? "localhost"; + var port = int.TryParse(Environment.GetEnvironmentVariable("ZB_LDAP_PORT"), out var p) ? p : 3893; + var baseDn = Environment.GetEnvironmentVariable("ZB_LDAP_BASE") ?? "dc=lmxopcua,dc=local"; + var svcDn = Environment.GetEnvironmentVariable("ZB_LDAP_SVC_DN") ?? ""; + var svcPw = Environment.GetEnvironmentVariable("ZB_LDAP_SVC_PW") ?? ""; + var user = Environment.GetEnvironmentVariable("ZB_LDAP_USER") ?? ""; + var pw = Environment.GetEnvironmentVariable("ZB_LDAP_PW") ?? ""; + var userAttr = Environment.GetEnvironmentVariable("ZB_LDAP_USERATTR") ?? "cn"; + + // ------------------------------------------------------------------ reachability probe + try + { + using var tcp = new TcpClient(); + // 3-second connect timeout to keep the test suite snappy when the server is absent + var connectTask = tcp.ConnectAsync(server, port); + if (!connectTask.Wait(TimeSpan.FromSeconds(3))) + Skip.If(true, $"GLAuth not reachable at {server}:{port} (connect timed out)."); + } + catch (Exception ex) + { + Skip.If(true, $"GLAuth not reachable at {server}:{port}: {ex.Message}"); + } + + // ------------------------------------------------------------------ build options + var options = new LdapOptions + { + Enabled = true, + Server = "127.0.0.1", + Port = 39999, // closed port -> immediate refusal + Transport = LdapTransport.None, + AllowInsecure = true, + SearchBase = baseDn, + ServiceAccountDn = svcDn, + ServiceAccountPassword = svcPw, + UserNameAttribute = userAttr, + GroupAttribute = "memberOf", + ConnectionTimeoutMs = 3000, + FallbackServers = new[] { $"{server}:{port}" }, + }; + + // ------------------------------------------------------------------ exercise the real service + var svc = new LdapAuthService(options); + + var first = await svc.AuthenticateAsync(user, pw, CancellationToken.None); + Assert.True(first.Succeeded, $"first login failed: {first.Failure}"); + + // Second login exercises the sticky preference: it must not re-walk the dead primary. + var second = await svc.AuthenticateAsync(user, pw, CancellationToken.None); + Assert.True(second.Succeeded, $"second login failed: {second.Failure}"); + } }