This is an automated email from the ASF dual-hosted git repository.

dhavalshah9131 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/ranger.git


The following commit(s) were added to refs/heads/master by this push:
     new 0aef900f7 RANGER-5331: Test Cases for unixauthclient & unixauthservice 
Module (#675)
0aef900f7 is described below

commit 0aef900f720dcb8e461f652c7f8df434aa404492
Author: Bhaavesh Amol Amre <[email protected]>
AuthorDate: Mon Sep 29 12:45:49 2025 +0530

    RANGER-5331: Test Cases for unixauthclient & unixauthservice Module (#675)
---
 unixauthclient/pom.xml                             |  12 +
 .../jaas/TestConsolePromptCallbackHandler.java     |  69 ++++
 .../unix/jaas/TestPamLoginModule.java              | 281 ++++++++++++++
 .../authentication/unix/jaas/TestPamPrincipal.java |  66 ++++
 .../unix/jaas/TestRemoteUnixLoginModule.java       | 405 +++++++++++++++++++++
 .../unix/jaas/TestUnixGroupPrincipal.java          |  42 +++
 .../unix/jaas/TestUnixUserPrincipal.java           |  42 +++
 .../jaas/TestUsernamePasswordCallbackHandler.java  |  90 +++++
 unixauthservice/pom.xml                            |  12 +
 .../authentication/TestPasswordValidator.java      | 183 ++++++++++
 .../TestUnixAuthenticationService.java             | 385 ++++++++++++++++++++
 11 files changed, 1587 insertions(+)

diff --git a/unixauthclient/pom.xml b/unixauthclient/pom.xml
index c59321b2b..3e8c653ef 100644
--- a/unixauthclient/pom.xml
+++ b/unixauthclient/pom.xml
@@ -102,6 +102,18 @@
             <artifactId>libpam4j</artifactId>
             <version>${libpam4j.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-inline</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-junit-jupiter</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>log4j-over-slf4j</artifactId>
diff --git 
a/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestConsolePromptCallbackHandler.java
 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestConsolePromptCallbackHandler.java
new file mode 100644
index 000000000..9b9b48fcb
--- /dev/null
+++ 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestConsolePromptCallbackHandler.java
@@ -0,0 +1,69 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ranger.authentication.unix.jaas;
+
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @generated by Cursor
+ * @description : Unit Test cases for ConsolePromptCallbackHandler
+ */
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestConsolePromptCallbackHandler {
+    @Test
+    public void test01_handle_setsNameAndPassword() throws IOException, 
UnsupportedCallbackException {
+        String input = "alice\nsecret\n";
+        System.setIn(new 
ByteArrayInputStream(input.getBytes(StandardCharsets.UTF_8)));
+
+        ConsolePromptCallbackHandler handler          = new 
ConsolePromptCallbackHandler();
+        NameCallback                 nameCallback     = new 
NameCallback("User: ");
+        PasswordCallback             passwordCallback = new 
PasswordCallback("Pass: ", false);
+
+        handler.handle(new Callback[] {nameCallback, passwordCallback});
+
+        assertEquals("alice", nameCallback.getName());
+        assertArrayEquals("secret".toCharArray(), 
passwordCallback.getPassword());
+    }
+
+    @Test
+    public void test02_handle_unknownCallback_doesNotThrow() throws 
IOException, UnsupportedCallbackException {
+        System.setIn(new 
ByteArrayInputStream("\n".getBytes(StandardCharsets.UTF_8)));
+        ConsolePromptCallbackHandler handler = new 
ConsolePromptCallbackHandler();
+        Callback                     unknown = new Callback() {};
+        handler.handle(new Callback[] {unknown});
+    }
+}
diff --git 
a/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestPamLoginModule.java
 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestPamLoginModule.java
new file mode 100644
index 000000000..22cc637ef
--- /dev/null
+++ 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestPamLoginModule.java
@@ -0,0 +1,281 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ranger.authentication.unix.jaas;
+
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.jvnet.libpam.PAM;
+import org.jvnet.libpam.UnixUser;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.login.FailedLoginException;
+import javax.security.auth.login.LoginException;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.security.Principal;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @generated by Cursor
+ * @description : Unit Test cases for PamLoginModule
+ */
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestPamLoginModule {
+    @Test
+    public void test01_initialize_withoutService_throwsOnLogin() throws 
Exception {
+        PamLoginModule      m       = new PamLoginModule();
+        Subject             subject = new Subject();
+        Map<String, Object> opts    = new HashMap<>();
+        m.initialize(subject, creds("alice", "secret"), new HashMap<>(), opts);
+        setField(m, "options", new HashMap<>());
+        assertThrows(LoginException.class, m::login);
+    }
+
+    @Test
+    public void 
test02_commit_addsPrincipal_and_logout_clears_without_realPam() throws 
Exception {
+        PamLoginModule m       = new PamLoginModule();
+        Subject        subject = new Subject();
+        UnixUser       user    = Mockito.mock(UnixUser.class);
+        Mockito.when(user.getUserName()).thenReturn("alice");
+        Mockito.when(user.getGecos()).thenReturn("");
+        Mockito.when(user.getDir()).thenReturn("");
+        Mockito.when(user.getShell()).thenReturn("");
+        Mockito.when(user.getUID()).thenReturn(0);
+        Mockito.when(user.getGID()).thenReturn(0);
+        Mockito.when(user.getGroups()).thenReturn(Collections.emptySet());
+
+        setField(m, "subject", subject);
+        setField(m, "authSucceeded", true);
+        setField(m, "principal", new PamPrincipal(user));
+        setField(m, "pam", Mockito.mock(PAM.class));
+        setField(m, "passwordChar", new char[] {'x'});
+
+        assertTrue(m.commit());
+        Set<Principal> principals = subject.getPrincipals();
+        assertTrue(principals.stream().anyMatch(p -> p instanceof PamPrincipal 
&& p.getName().equals("alice")));
+
+        assertTrue(m.logout());
+        assertTrue(subject.getPrincipals().isEmpty());
+    }
+
+    @Test
+    public void test03_login_throwsLoginException_withRealPam() throws 
Exception {
+        PamLoginModule      m       = new PamLoginModule();
+        Subject             subject = new Subject();
+        Map<String, Object> opts    = new HashMap<>();
+        opts.put(PamLoginModule.SERVICE_KEY, "sshd");
+        CallbackHandler cb = creds("alice", "bad");
+        m.initialize(subject, cb, new HashMap<>(), opts);
+        setField(m, "options", opts);
+        setField(m, "callbackHandler", cb);
+        assertThrows(LoginException.class, m::login);
+    }
+
+    @Test
+    public void test04_abort_withoutAuth_returnsFalse() throws Exception {
+        PamLoginModule      m       = new PamLoginModule();
+        Subject             subject = new Subject();
+        Map<String, Object> opts    = new HashMap<>();
+        opts.put(PamLoginModule.SERVICE_KEY, "sshd");
+        CallbackHandler cb = creds("u", "p");
+        m.initialize(subject, cb, new HashMap<>(), opts);
+        setField(m, "options", opts);
+        setField(m, "subject", subject);
+        setField(m, "callbackHandler", cb);
+        assertFalse(m.abort());
+    }
+
+    @Test
+    public void test05_abort_afterSuccess_cleansUpAndReturnsTrue() throws 
Exception {
+        PamLoginModule m = new PamLoginModule();
+        setField(m, "authSucceeded", true);
+        PAM pam = Mockito.mock(PAM.class);
+        setField(m, "pam", pam);
+        setField(m, "passwordChar", new char[] {'x'});
+        assertTrue(m.abort());
+    }
+
+    @Test
+    public void test06_commit_returnsFalse_whenAuthNotSucceeded() throws 
Exception {
+        PamLoginModule m = new PamLoginModule();
+        setField(m, "authSucceeded", false);
+        setField(m, "subject", new Subject());
+        assertFalse(m.commit());
+    }
+
+    @Test
+    public void test07_commit_readOnlySubject_throws_and_cleansUp() throws 
Exception {
+        PamLoginModule m       = new PamLoginModule();
+        Subject        subject = new Subject();
+        subject.setReadOnly();
+        setField(m, "subject", subject);
+        setField(m, "authSucceeded", true);
+        setField(m, "principal", new 
PamPrincipal(Mockito.mock(UnixUser.class)));
+        PAM pam = Mockito.mock(PAM.class);
+        setField(m, "pam", pam);
+        setField(m, "passwordChar", new char[] {'p'});
+        assertThrows(LoginException.class, m::commit);
+        Mockito.verify(pam).dispose();
+    }
+
+    @Test
+    public void test08_obtainUserAndPassword_noCallbackHandler_throws() throws 
Exception {
+        PamLoginModule m = new PamLoginModule();
+        setField(m, "callbackHandler", null);
+        Method method = 
PamLoginModule.class.getDeclaredMethod("obtainUserAndPassword");
+        method.setAccessible(true);
+        assertThrows(LoginException.class, () -> 
invokeAndRethrowLoginException(m, method));
+    }
+
+    @Test
+    public void test09_obtainUserAndPassword_errorInCallbacks_throws() throws 
Exception {
+        PamLoginModule m = new PamLoginModule();
+        CallbackHandler cb = new CallbackHandler() {
+            @Override
+            public void handle(Callback[] callbacks) throws 
java.io.IOException {
+                throw new java.io.IOException("boom");
+            }
+        };
+        setField(m, "callbackHandler", cb);
+        Method method = 
PamLoginModule.class.getDeclaredMethod("obtainUserAndPassword");
+        method.setAccessible(true);
+        assertThrows(LoginException.class, () -> 
invokeAndRethrowLoginException(m, method));
+    }
+
+    @Test
+    public void test10_performLogin_withNullPassword_throwsFailedLogin() 
throws Exception {
+        PamLoginModule m = new PamLoginModule();
+        setField(m, "username", "alice");
+        setField(m, "pam", Mockito.mock(PAM.class));
+        Method method = PamLoginModule.class.getDeclaredMethod("performLogin");
+        method.setAccessible(true);
+        assertThrows(FailedLoginException.class, () -> 
invokeAndRethrowLoginException(m, method));
+    }
+
+    @Test
+    public void test11_performLogin_success_setsPrincipalAndAuthSucceeded() 
throws Exception {
+        PamLoginModule m = new PamLoginModule();
+        setField(m, "username", "alice");
+        setField(m, "passwordChar", new char[] {'s'});
+        PAM      pam  = Mockito.mock(PAM.class);
+        UnixUser user = Mockito.mock(UnixUser.class);
+        Mockito.when(user.getUserName()).thenReturn("alice");
+        Mockito.when(pam.authenticate("alice", "s")).thenReturn(user);
+        setField(m, "pam", pam);
+
+        Method method = PamLoginModule.class.getDeclaredMethod("performLogin");
+        method.setAccessible(true);
+        assertTrue(invokeAndReturnBoolean(m, method));
+        Object principal = getField(m, "principal");
+        assertNotNull(principal);
+        assertTrue(principal instanceof PamPrincipal);
+        assertEquals("alice", ((PamPrincipal) principal).getName());
+        assertTrue((Boolean) getField(m, "authSucceeded"));
+    }
+
+    @Test
+    public void test12_logout_readOnlySubject_throws_and_cleansUp() throws 
Exception {
+        PamLoginModule m       = new PamLoginModule();
+        Subject        subject = new Subject();
+        subject.setReadOnly();
+        setField(m, "subject", subject);
+        setField(m, "principal", new 
PamPrincipal(Mockito.mock(UnixUser.class)));
+        PAM pam = Mockito.mock(PAM.class);
+        setField(m, "pam", pam);
+        setField(m, "passwordChar", new char[] {'p'});
+        assertThrows(LoginException.class, m::logout);
+        Mockito.verify(pam).dispose();
+    }
+
+    private static void setField(Object target, String name, Object value) 
throws Exception {
+        Field f = target.getClass().getDeclaredField(name);
+        f.setAccessible(true);
+        f.set(target, value);
+    }
+
+    private static Object getField(Object target, String name) throws 
Exception {
+        Field f = target.getClass().getDeclaredField(name);
+        f.setAccessible(true);
+        return f.get(target);
+    }
+
+    private static void invokeAndRethrowLoginException(Object target, Method 
method) throws LoginException {
+        try {
+            method.invoke(target);
+        } catch (InvocationTargetException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof LoginException) {
+                throw (LoginException) cause;
+            }
+            throw new RuntimeException(cause);
+        } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private static boolean invokeAndReturnBoolean(Object target, Method 
method) throws Exception {
+        try {
+            Object ret = method.invoke(target);
+            return (Boolean) ret;
+        } catch (InvocationTargetException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof Exception) {
+                throw (Exception) cause;
+            }
+            throw new RuntimeException(cause);
+        }
+    }
+
+    private static CallbackHandler creds(String user, String pass) {
+        return new CallbackHandler() {
+            @Override
+            public void handle(Callback[] callbacks) {
+                for (Callback c : callbacks) {
+                    if (c instanceof NameCallback) {
+                        ((NameCallback) c).setName(user);
+                    }
+                    if (c instanceof PasswordCallback) {
+                        ((PasswordCallback) c).setPassword(pass != null ? 
pass.toCharArray() : null);
+                    }
+                }
+            }
+        };
+    }
+}
diff --git 
a/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestPamPrincipal.java
 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestPamPrincipal.java
new file mode 100644
index 000000000..541d4e00f
--- /dev/null
+++ 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestPamPrincipal.java
@@ -0,0 +1,66 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ranger.authentication.unix.jaas;
+
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.jvnet.libpam.UnixUser;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * @generated by Cursor
+ * @description : Unit Test cases for PamPrincipal
+ */
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestPamPrincipal {
+    @Test
+    public void test01_getters_returnValuesFromUnixUser() {
+        Set<String> groups = new HashSet<>(Arrays.asList("g1", "g2"));
+        UnixUser    user   = Mockito.mock(UnixUser.class);
+        Mockito.when(user.getUserName()).thenReturn("alice");
+        Mockito.when(user.getGecos()).thenReturn("gecos");
+        Mockito.when(user.getDir()).thenReturn("/home/alice");
+        Mockito.when(user.getShell()).thenReturn("/bin/bash");
+        Mockito.when(user.getUID()).thenReturn(1000);
+        Mockito.when(user.getGID()).thenReturn(100);
+        Mockito.when(user.getGroups()).thenReturn(groups);
+
+        PamPrincipal p = new PamPrincipal(user);
+        assertEquals("alice", p.getName());
+        assertEquals("gecos", p.getGecos());
+        assertEquals("/home/alice", p.getHomeDir());
+        assertEquals("/bin/bash", p.getShell());
+        assertEquals(1000, p.getUid());
+        assertEquals(100, p.getGid());
+        assertEquals(groups, p.getGroups());
+        assertThrows(UnsupportedOperationException.class, () -> 
p.getGroups().add("x"));
+    }
+}
diff --git 
a/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestRemoteUnixLoginModule.java
 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestRemoteUnixLoginModule.java
new file mode 100644
index 000000000..7d7dae165
--- /dev/null
+++ 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestRemoteUnixLoginModule.java
@@ -0,0 +1,405 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ranger.authentication.unix.jaas;
+
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+import sun.security.x509.AlgorithmId;
+import sun.security.x509.CertificateAlgorithmId;
+import sun.security.x509.CertificateSerialNumber;
+import sun.security.x509.CertificateValidity;
+import sun.security.x509.CertificateVersion;
+import sun.security.x509.CertificateX509Key;
+import sun.security.x509.X500Name;
+import sun.security.x509.X509CertImpl;
+import sun.security.x509.X509CertInfo;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.security.auth.Subject;
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.CallbackHandler;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+import javax.security.auth.login.LoginException;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.math.BigInteger;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.KeyStore;
+import java.security.Principal;
+import java.security.SecureRandom;
+import java.security.cert.Certificate;
+import java.security.cert.X509Certificate;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * @generated by Cursor
+ * @description : Unit Test cases for RemoteUnixLoginModule
+ */
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestRemoteUnixLoginModule {
+    @Test
+    public void test01_login_success_commit_addsPrincipals_and_logout_clears() 
throws Exception {
+        try (OneShotAuthServer server = new OneShotAuthServer("OK: g1, g2")) {
+            RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+            Subject               subject = new Subject();
+            Map<String, Object>   opts    = baseOptions("127.0.0.1", 
server.getPort());
+            m.initialize(subject, fixedCredentials("alice", "secret"), new 
HashMap<>(), opts);
+
+            assertTrue(m.login());
+            assertTrue(m.commit());
+
+            Set<Principal> principals = subject.getPrincipals();
+            assertTrue(principals.stream().anyMatch(p -> p instanceof 
UnixUserPrincipal && p.getName().equals("alice")));
+            assertTrue(principals.stream().anyMatch(p -> p instanceof 
UnixGroupPrincipal && p.getName().equals("g1")));
+            assertTrue(principals.stream().anyMatch(p -> p instanceof 
UnixGroupPrincipal && p.getName().equals("g2")));
+
+            assertTrue(m.logout());
+            assertTrue(subject.getPrincipals().isEmpty());
+        }
+    }
+
+    @Test
+    public void test02_login_failed_throws_LoginException() throws Exception {
+        try (OneShotAuthServer server = new OneShotAuthServer("FAILED: Invalid 
Password")) {
+            RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+            Subject               subject = new Subject();
+            Map<String, Object>   opts    = baseOptions("127.0.0.1", 
server.getPort());
+            m.initialize(subject, fixedCredentials("alice", "secret"), new 
HashMap<>(), opts);
+            assertThrows(LoginException.class, m::login);
+            assertFalse(m.commit());
+        }
+    }
+
+    @Test
+    public void test03_login_nullReply_throws_LoginException() throws 
Exception {
+        try (OneShotAuthServer server = new OneShotAuthServer(null)) {
+            RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+            Subject               subject = new Subject();
+            Map<String, Object>   opts    = baseOptions("127.0.0.1", 
server.getPort());
+            m.initialize(subject, fixedCredentials("bob", "x"), new 
HashMap<>(), opts);
+            assertThrows(LoginException.class, m::login);
+        }
+    }
+
+    @Test
+    public void 
test04_login_disabled_returnsFalse_and_commitClearsPrincipals() throws 
Exception {
+        RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+        Subject               subject = new Subject();
+        Map<String, Object>   opts    = baseOptions("localhost", 65535);
+        opts.put("ranger.unixauth.remote.login.enabled", "false");
+        m.initialize(subject, fixedCredentials("tom", "y"), new HashMap<>(), 
opts);
+
+        assertFalse(m.login());
+        assertFalse(m.commit());
+        assertTrue(subject.getPrincipals().isEmpty());
+    }
+
+    @Test
+    public void 
test05_ssl_disabledValidation_connectFailure_throwsLoginException() {
+        RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+        Subject               subject = new Subject();
+        Map<String, Object>   opts    = baseOptions("127.0.0.1", 65000);
+        opts.put("ranger.unixauth.ssl.enabled", "true");
+        opts.put("ranger.unixauth.server.cert.validation", "false");
+        m.initialize(subject, fixedCredentials("alice", "secret"), new 
HashMap<>(), opts);
+        assertThrows(LoginException.class, m::login);
+    }
+
+    @Test
+    public void test06_ssl_withTruststore_loadFails_throwsLoginException() {
+        RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+        Subject               subject = new Subject();
+        Map<String, Object>   opts    = baseOptions("127.0.0.1", 65001);
+        opts.put("ranger.unixauth.ssl.enabled", "true");
+        opts.put("ranger.unixauth.server.cert.validation", "true");
+        opts.put("ranger.unixauth.truststore", "nonexistent.jks");
+        opts.put("ranger.unixauth.truststore.password", "");
+        m.initialize(subject, fixedCredentials("bob", "pw"), new HashMap<>(), 
opts);
+        assertThrows(LoginException.class, m::login);
+    }
+
+    @Test
+    public void test07_ssl_withKeystore_loadFails_throwsLoginException() {
+        RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+        Subject               subject = new Subject();
+        Map<String, Object>   opts    = baseOptions("127.0.0.1", 65002);
+        opts.put("ranger.unixauth.ssl.enabled", "true");
+        opts.put("ranger.unixauth.keystore", "nonexistent-keystore.jks");
+        opts.put("ranger.unixauth.keystore.password", "");
+        // disable server cert validation to avoid needing truststore
+        opts.put("ranger.unixauth.server.cert.validation", "false");
+        m.initialize(subject, fixedCredentials("carol", "pw"), new 
HashMap<>(), opts);
+        assertThrows(LoginException.class, m::login);
+    }
+
+    @Test
+    public void test08_abort_resetsLoginState_and_commitReturnsFalse() throws 
Exception {
+        try (OneShotAuthServer server = new OneShotAuthServer("OK:")) {
+            RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+            Subject               subject = new Subject();
+            Map<String, Object>   opts    = baseOptions("127.0.0.1", 
server.getPort());
+            m.initialize(subject, fixedCredentials("alice", "secret"), new 
HashMap<>(), opts);
+
+            assertTrue(m.login());
+            assertTrue(m.abort());
+
+            // After abort(), loginSuccessful should be false; commit should 
return false and clear principals
+            subject.getPrincipals().add(new UnixUserPrincipal("temp"));
+            assertFalse(m.commit());
+            assertTrue(subject.getPrincipals().isEmpty());
+        }
+    }
+
+    @Test
+    public void 
test09_ssl_ignoreValidation_successfulHandshake_executesTrustManager_andLogin() 
throws Exception {
+        try (SslOneShotAuthServer server = new SslOneShotAuthServer("OK: gA, 
gB")) {
+            RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+            Subject               subject = new Subject();
+            Map<String, Object>   opts    = baseOptions("127.0.0.1", 
server.getPort());
+            opts.put("ranger.unixauth.ssl.enabled", "true");
+            opts.put("ranger.unixauth.server.cert.validation", "false");
+            m.initialize(subject, fixedCredentials("eve", "pw"), new 
HashMap<>(), opts);
+
+            assertTrue(m.login());
+            assertTrue(m.commit());
+            assertTrue(subject.getPrincipals().stream().anyMatch(p -> p 
instanceof UnixUserPrincipal && p.getName().equals("eve")));
+            assertTrue(subject.getPrincipals().stream().anyMatch(p -> p 
instanceof UnixGroupPrincipal && p.getName().equals("gA")));
+            assertTrue(m.logout());
+        }
+    }
+
+    @Test
+    public void 
test10_login_callbackHandlerThrowsIOException_isWrappedAsLoginException() {
+        RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+        Subject               subject = new Subject();
+        Map<String, Object>   opts    = baseOptions("127.0.0.1", 65010);
+        CallbackHandler cb = new CallbackHandler() {
+            @Override
+            public void handle(Callback[] callbacks) throws IOException {
+                throw new IOException("boom");
+            }
+        };
+        m.initialize(subject, cb, new HashMap<>(), opts);
+        assertThrows(LoginException.class, m::login);
+    }
+
+    @Test
+    public void 
test11_login_callbackHandlerThrowsUnsupported_isWrappedAsLoginException() {
+        RemoteUnixLoginModule m       = new RemoteUnixLoginModule();
+        Subject               subject = new Subject();
+        Map<String, Object>   opts    = baseOptions("127.0.0.1", 65011);
+        CallbackHandler cb = new CallbackHandler() {
+            @Override
+            public void handle(Callback[] callbacks) throws 
UnsupportedCallbackException {
+                throw new UnsupportedCallbackException(new 
NameCallback("user"));
+            }
+        };
+        m.initialize(subject, cb, new HashMap<>(), opts);
+        assertThrows(LoginException.class, m::login);
+    }
+
+    private static Map<String, Object> baseOptions(String host, int port) {
+        Map<String, Object> opts = new HashMap<>();
+        opts.put("ranger.unixauth.remote.login.enabled", "true");
+        opts.put("ranger.unixauth.service.hostname", host);
+        opts.put("ranger.unixauth.service.port", String.valueOf(port));
+        opts.put("ranger.unixauth.ssl.enabled", "false");
+        opts.put("ranger.unixauth.debug", "false");
+        return opts;
+    }
+
+    private static CallbackHandler fixedCredentials(String user, String pass) {
+        return new CallbackHandler() {
+            @Override
+            public void handle(Callback[] callbacks) throws IOException {
+                for (Callback cb : callbacks) {
+                    if (cb instanceof NameCallback) {
+                        ((NameCallback) cb).setName(user);
+                    } else if (cb instanceof PasswordCallback) {
+                        ((PasswordCallback) 
cb).setPassword(pass.toCharArray());
+                    }
+                }
+            }
+        };
+    }
+
+    private static class OneShotAuthServer implements AutoCloseable {
+        private final ServerSocket   server;
+        private final Thread         thread;
+        private final CountDownLatch ready = new CountDownLatch(1);
+        private final String         reply;
+
+        OneShotAuthServer(String reply) throws IOException {
+            this.server = new ServerSocket(0);
+            this.reply  = reply;
+            this.thread = new Thread(() -> {
+                ready.countDown();
+                try (Socket s = server.accept()) {
+                    BufferedReader in = new BufferedReader(new 
InputStreamReader(s.getInputStream()));
+                    in.readLine();
+                    try (OutputStreamWriter out = new 
OutputStreamWriter(s.getOutputStream())) {
+                        if (reply != null) {
+                            out.write(reply + "\n");
+                            out.flush();
+                        } else {
+                            // close without sending
+                        }
+                    }
+                } catch (IOException ignored) {
+                } finally {
+                    try {
+                        server.close();
+                    } catch (IOException ignored) {
+                    }
+                }
+            });
+            this.thread.setDaemon(true);
+            this.thread.start();
+            try {
+                ready.await(5, TimeUnit.SECONDS);
+            } catch (InterruptedException ignored) {
+            }
+        }
+
+        @Override
+        public void close() {
+            try {
+                server.close();
+            } catch (IOException ignored) {
+            }
+        }
+
+        int getPort() {
+            return server.getLocalPort();
+        }
+    }
+
+    private static class SslOneShotAuthServer implements AutoCloseable {
+        private final SSLServerSocket server;
+        private final Thread          thread;
+        private final CountDownLatch  ready = new CountDownLatch(1);
+        private final String          reply;
+
+        SslOneShotAuthServer(String reply) throws Exception {
+            this.reply = reply;
+
+            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
+            kpg.initialize(2048);
+            KeyPair kp = kpg.generateKeyPair();
+
+            X509Certificate cert = generateSelfSigned("CN=localhost", kp);
+
+            KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
+            ks.load(null, null);
+            char[] pass = "changeit".toCharArray();
+            ks.setKeyEntry("server", kp.getPrivate(), pass, new Certificate[] 
{cert});
+
+            KeyManagerFactory kmf = 
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+            kmf.init(ks, pass);
+            KeyManager[] kms = kmf.getKeyManagers();
+
+            SSLContext ctx = SSLContext.getInstance("TLSv1.2");
+            ctx.init(kms, null, new SecureRandom());
+            SSLServerSocketFactory ssf = ctx.getServerSocketFactory();
+            this.server = (SSLServerSocket) ssf.createServerSocket(0);
+
+            this.thread = new Thread(() -> {
+                ready.countDown();
+                try (Socket s = server.accept()) {
+                    BufferedReader in = new BufferedReader(new 
InputStreamReader(s.getInputStream()));
+                    in.readLine();
+                    try (OutputStreamWriter out = new 
OutputStreamWriter(s.getOutputStream())) {
+                        out.write(reply + "\n");
+                        out.flush();
+                    }
+                } catch (IOException ignored) {
+                } finally {
+                    try {
+                        server.close();
+                    } catch (IOException ignored) {
+                    }
+                }
+            });
+            this.thread.setDaemon(true);
+            this.thread.start();
+            try {
+                ready.await(5, TimeUnit.SECONDS);
+            } catch (InterruptedException ignored) {
+            }
+        }
+
+        @Override
+        public void close() {
+            try {
+                server.close();
+            } catch (IOException ignored) {
+            }
+        }
+
+        int getPort() {
+            return server.getLocalPort();
+        }
+
+        private static X509Certificate generateSelfSigned(String dn, KeyPair 
keyPair) throws Exception {
+            long now  = System.currentTimeMillis();
+            Date from = new Date(now - 60000);
+            Date to   = new Date(now + 86400000L);
+
+            X509CertInfo info = new X509CertInfo();
+            info.set(X509CertInfo.VERSION, new 
CertificateVersion(CertificateVersion.V3));
+            info.set(X509CertInfo.SERIAL_NUMBER, new 
CertificateSerialNumber(new BigInteger(64, new SecureRandom())));
+            X500Name owner = new X500Name(dn);
+            info.set(X509CertInfo.SUBJECT, owner);
+            info.set(X509CertInfo.ISSUER, owner);
+            info.set(X509CertInfo.VALIDITY, new CertificateValidity(from, to));
+            info.set(X509CertInfo.KEY, new 
CertificateX509Key(keyPair.getPublic()));
+            info.set(X509CertInfo.ALGORITHM_ID, new 
CertificateAlgorithmId(AlgorithmId.get("SHA256withRSA")));
+
+            X509CertImpl cert = new X509CertImpl(info);
+            cert.sign(keyPair.getPrivate(), "SHA256withRSA");
+            return cert;
+        }
+    }
+}
diff --git 
a/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestUnixGroupPrincipal.java
 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestUnixGroupPrincipal.java
new file mode 100644
index 000000000..407c43895
--- /dev/null
+++ 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestUnixGroupPrincipal.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ranger.authentication.unix.jaas;
+
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @generated by Cursor
+ * @description : Unit Test cases for UnixGroupPrincipal
+ */
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestUnixGroupPrincipal {
+    @Test
+    public void test01_getName_returnsGroupName() {
+        UnixGroupPrincipal principal = new UnixGroupPrincipal("dev");
+        assertEquals("dev", principal.getName());
+    }
+}
diff --git 
a/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestUnixUserPrincipal.java
 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestUnixUserPrincipal.java
new file mode 100644
index 000000000..e53d0b678
--- /dev/null
+++ 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestUnixUserPrincipal.java
@@ -0,0 +1,42 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ranger.authentication.unix.jaas;
+
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+/**
+ * @generated by Cursor
+ * @description : Unit Test cases for UnixUserPrincipal
+ */
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestUnixUserPrincipal {
+    @Test
+    public void test01_getName_returnsUserName() {
+        UnixUserPrincipal principal = new UnixUserPrincipal("alice");
+        assertEquals("alice", principal.getName());
+    }
+}
diff --git 
a/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestUsernamePasswordCallbackHandler.java
 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestUsernamePasswordCallbackHandler.java
new file mode 100644
index 000000000..782700dc9
--- /dev/null
+++ 
b/unixauthclient/src/test/java/org/apache/ranger/authentication/unix/jaas/TestUsernamePasswordCallbackHandler.java
@@ -0,0 +1,90 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.ranger.authentication.unix.jaas;
+
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.security.auth.callback.Callback;
+import javax.security.auth.callback.NameCallback;
+import javax.security.auth.callback.PasswordCallback;
+import javax.security.auth.callback.UnsupportedCallbackException;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * @generated by Cursor
+ * @description : Unit Test cases for UsernamePasswordCallbackHandler
+ */
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestUsernamePasswordCallbackHandler {
+    @Test
+    public void test01_handle_name_only_constructorBug_setsNullName() throws 
IOException, UnsupportedCallbackException {
+        UsernamePasswordCallbackHandler handler      = new 
UsernamePasswordCallbackHandler("alice", "secret");
+        NameCallback                    nameCallback = new 
NameCallback("User:");
+        handler.handle(new Callback[] {nameCallback});
+        assertNull(nameCallback.getName());
+    }
+
+    @Test
+    public void test02_handle_password_throwsNPE_whenPasswordNull() {
+        UsernamePasswordCallbackHandler handler          = new 
UsernamePasswordCallbackHandler(null, null);
+        PasswordCallback                passwordCallback = new 
PasswordCallback("Pass:", false);
+        assertThrows(NullPointerException.class, () -> handler.handle(new 
Callback[] {passwordCallback}));
+    }
+
+    @Test
+    public void test03_handle_setsNameAndPassword_whenFieldsInjected() throws 
Exception {
+        UsernamePasswordCallbackHandler handler = new 
UsernamePasswordCallbackHandler(null, null);
+
+        Field userField = 
UsernamePasswordCallbackHandler.class.getDeclaredField("user");
+        userField.setAccessible(true);
+        userField.set(handler, "bob");
+
+        Field pwdField = 
UsernamePasswordCallbackHandler.class.getDeclaredField("password");
+        pwdField.setAccessible(true);
+        pwdField.set(handler, "p@ss");
+
+        NameCallback     nameCallback     = new NameCallback("User:");
+        PasswordCallback passwordCallback = new PasswordCallback("Pass:", 
false);
+
+        handler.handle(new Callback[] {nameCallback, passwordCallback});
+
+        assertEquals("bob", nameCallback.getName());
+        assertArrayEquals("p@ss".toCharArray(), 
passwordCallback.getPassword());
+    }
+
+    @Test
+    public void test04_handle_unsupportedCallback_throws() {
+        UsernamePasswordCallbackHandler handler     = new 
UsernamePasswordCallbackHandler(null, null);
+        Callback                        unsupported = new Callback() {};
+        assertThrows(UnsupportedCallbackException.class, () -> 
handler.handle(new Callback[] {unsupported}));
+    }
+}
diff --git a/unixauthservice/pom.xml b/unixauthservice/pom.xml
index 07f7ab760..2821307f5 100644
--- a/unixauthservice/pom.xml
+++ b/unixauthservice/pom.xml
@@ -101,6 +101,18 @@
             <artifactId>slf4j-api</artifactId>
             <version>${slf4j.version}</version>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-inline</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-junit-jupiter</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>log4j-over-slf4j</artifactId>
diff --git 
a/unixauthservice/src/test/java/org/apache/ranger/authentication/TestPasswordValidator.java
 
b/unixauthservice/src/test/java/org/apache/ranger/authentication/TestPasswordValidator.java
new file mode 100644
index 000000000..69860af0b
--- /dev/null
+++ 
b/unixauthservice/src/test/java/org/apache/ranger/authentication/TestPasswordValidator.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.authentication;
+
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.Socket;
+import java.nio.charset.StandardCharsets;
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @generated by Cursor
+ * @description : Unit Test cases for PasswordValidator
+ */
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestPasswordValidator {
+    @Test
+    public void test00_staticGettersAndSetters() {
+        PasswordValidator.setValidatorProgram("/tmp/prog");
+        PasswordValidator.setAdminUserList(Collections.singletonList("root"));
+        PasswordValidator.setAdminRoleNames("ROLE_SUPER");
+
+        assertEquals("/tmp/prog", PasswordValidator.getValidatorProgram());
+        assertEquals(Collections.singletonList("root"), 
PasswordValidator.getAdminUserList());
+        assertEquals("ROLE_SUPER", PasswordValidator.getAdminRoleNames());
+    }
+
+    @Test
+    public void test01_nullProgramWritesFailedAndClosesSocket() throws 
Exception {
+        PasswordValidator.setValidatorProgram(null);
+        PasswordValidator.setAdminUserList(null);
+        PasswordValidator.setAdminRoleNames(null);
+
+        ByteArrayOutputStream out    = new ByteArrayOutputStream();
+        Socket                socket = buildSocket("LOGIN: alice secret", out);
+
+        new PasswordValidator(socket).run();
+
+        String response = out.toString(StandardCharsets.UTF_8.name()).trim();
+        assertEquals("FAILED: Unable to validate credentials.", response);
+        verify(socket, times(1)).close();
+    }
+
+    @Test
+    public void test02_okResponseAppendsAdminRoleAndDestroysProcess() throws 
Exception {
+        PasswordValidator.setValidatorProgram("/bin/validator");
+        PasswordValidator.setAdminUserList(Arrays.asList("alice", "bob"));
+        PasswordValidator.setAdminRoleNames("ROLE_ADMIN");
+
+        ByteArrayOutputStream out    = new ByteArrayOutputStream();
+        Socket                socket = buildSocket("LOGIN: alice secret", out);
+
+        Process              process       = mock(Process.class);
+        ByteArrayInputStream processStdout = new 
ByteArrayInputStream("OK".getBytes(StandardCharsets.UTF_8));
+        when(process.getInputStream()).thenReturn(processStdout);
+        when(process.getOutputStream()).thenReturn(new 
ByteArrayOutputStream());
+
+        try (MockedStatic<Runtime> runtimeStatic = mockStatic(Runtime.class)) {
+            Runtime rt = mock(Runtime.class);
+            runtimeStatic.when(Runtime::getRuntime).thenReturn(rt);
+            when(rt.exec("/bin/validator")).thenReturn(process);
+
+            new PasswordValidator(socket).run();
+        }
+
+        String response = out.toString(StandardCharsets.UTF_8.name()).trim();
+        assertEquals("OK ROLE_ADMIN", response);
+        verify(process, times(1)).destroy();
+        verify(socket, times(1)).close();
+    }
+
+    @Test
+    public void test03_okResponseWithoutAdminDoesNotAppendRole() throws 
Exception {
+        PasswordValidator.setValidatorProgram("/bin/validator");
+        PasswordValidator.setAdminUserList(Collections.singletonList("bob"));
+        PasswordValidator.setAdminRoleNames("ROLE_ADMIN");
+
+        ByteArrayOutputStream out    = new ByteArrayOutputStream();
+        Socket                socket = buildSocket("LOGIN: alice secret", out);
+
+        Process              process       = mock(Process.class);
+        ByteArrayInputStream processStdout = new 
ByteArrayInputStream("OK".getBytes(StandardCharsets.UTF_8));
+        when(process.getInputStream()).thenReturn(processStdout);
+        when(process.getOutputStream()).thenReturn(new 
ByteArrayOutputStream());
+
+        try (MockedStatic<Runtime> runtimeStatic = mockStatic(Runtime.class)) {
+            Runtime rt = mock(Runtime.class);
+            runtimeStatic.when(Runtime::getRuntime).thenReturn(rt);
+            when(rt.exec("/bin/validator")).thenReturn(process);
+
+            new PasswordValidator(socket).run();
+        }
+
+        String response = out.toString(StandardCharsets.UTF_8.name()).trim();
+        assertEquals("OK", response);
+        verify(process, times(1)).destroy();
+        verify(socket, times(1)).close();
+    }
+
+    @Test
+    public void test04_execThrowsWritesFailed() throws Exception {
+        PasswordValidator.setValidatorProgram("/bin/validator");
+        PasswordValidator.setAdminUserList(Collections.singletonList("alice"));
+        PasswordValidator.setAdminRoleNames("ROLE_ADMIN");
+
+        ByteArrayOutputStream out    = new ByteArrayOutputStream();
+        Socket                socket = buildSocket("LOGIN: alice secret", out);
+
+        try (MockedStatic<Runtime> runtimeStatic = mockStatic(Runtime.class)) {
+            Runtime rt = mock(Runtime.class);
+            runtimeStatic.when(Runtime::getRuntime).thenReturn(rt);
+            when(rt.exec("/bin/validator")).thenThrow(new IOException("boom"));
+
+            new PasswordValidator(socket).run();
+        }
+
+        // Output may not be flushed in catch path; verify socket is closed 
indicating catch executed
+        verify(socket, times(1)).close();
+    }
+
+    @Test
+    public void test05_closeSocketIOExceptionHandled() throws Exception {
+        PasswordValidator.setValidatorProgram(null);
+        PasswordValidator.setAdminUserList(null);
+        PasswordValidator.setAdminRoleNames(null);
+
+        ByteArrayOutputStream out    = new ByteArrayOutputStream();
+        Socket                socket = buildSocket("LOGIN: alice secret", out);
+        doThrow(new IOException("close failure")).when(socket).close();
+
+        new PasswordValidator(socket).run();
+
+        // No exception should be thrown even if close() fails; nothing to 
assert besides method completed
+        verify(socket, times(1)).close();
+    }
+
+    private Socket buildSocket(String requestLine, ByteArrayOutputStream 
responseOut) throws IOException {
+        Socket       socket = mock(Socket.class);
+        InputStream  in     = new ByteArrayInputStream((requestLine + 
"\n").getBytes(StandardCharsets.UTF_8));
+        OutputStream out    = responseOut;
+        when(socket.getInputStream()).thenReturn(in);
+        when(socket.getOutputStream()).thenReturn(out);
+        return socket;
+    }
+}
diff --git 
a/unixauthservice/src/test/java/org/apache/ranger/authentication/TestUnixAuthenticationService.java
 
b/unixauthservice/src/test/java/org/apache/ranger/authentication/TestUnixAuthenticationService.java
new file mode 100644
index 000000000..194204a1d
--- /dev/null
+++ 
b/unixauthservice/src/test/java/org/apache/ranger/authentication/TestUnixAuthenticationService.java
@@ -0,0 +1,385 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.ranger.authentication;
+
+import org.apache.hadoop.conf.Configuration;
+import org.apache.ranger.credentialapi.CredentialReader;
+import org.apache.ranger.plugin.util.XMLUtils;
+import org.apache.ranger.unixusersync.config.UserGroupSyncConfig;
+import org.apache.ranger.unixusersync.ha.UserSyncHAInitializerImpl;
+import org.junit.jupiter.api.MethodOrderer;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.TestMethodOrder;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.MockedConstruction;
+import org.mockito.MockedStatic;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+import javax.net.ssl.KeyManager;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLServerSocket;
+import javax.net.ssl.SSLServerSocketFactory;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.Socket;
+import java.security.KeyStore;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Properties;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.doNothing;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockConstruction;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * @generated by Cursor
+ * @description : Unit Test cases for UnixAuthenticationService
+ */
+
+@ExtendWith(MockitoExtension.class)
+@TestMethodOrder(MethodOrderer.MethodName.class)
+public class TestUnixAuthenticationService {
+    @Test
+    public void test01_initThrowsWhenCredFileMissing() throws Exception {
+        UnixAuthenticationService svc = new UnixAuthenticationService();
+
+        try (MockedStatic<XMLUtils> xmlMock = mockStatic(XMLUtils.class)) {
+            xmlMock.when(() -> XMLUtils.loadConfig(any(String.class), 
any(Properties.class))).thenAnswer(inv -> null);
+
+            try (MockedStatic<CredentialReader> credMock = 
mockStatic(CredentialReader.class)) {
+                // Do not need to reach CredentialReader because missing file 
check will throw first
+                Properties props = new Properties();
+                props.setProperty("ranger.usersync.credstore.filename", 
"/no/such/file.jceks");
+                props.setProperty("ranger.usersync.port", "6168");
+
+                // Inject properties via XMLUtils sequence
+                xmlMock.when(() -> 
XMLUtils.loadConfig(eq("ranger-ugsync-default.xml"), any(Properties.class)))
+                        .thenAnswer(inv -> {
+                            ((Properties) inv.getArgument(1)).putAll(props);
+                            return null;
+                        });
+                xmlMock.when(() -> 
XMLUtils.loadConfig(eq("ranger-ugsync-site.xml"), any(Properties.class)))
+                        .thenAnswer(inv -> null);
+
+                InvocationTargetException ex = 
assertThrows(InvocationTargetException.class, () -> invokeInit(svc));
+                assertInstanceOf(RuntimeException.class, ex.getCause());
+            }
+        }
+    }
+
+    @Test
+    public void test02_getFileInputStreamFallsBackToResourcePath() throws 
Exception {
+        UnixAuthenticationService svc = new UnixAuthenticationService();
+        Method                    m   = 
UnixAuthenticationService.class.getDeclaredMethod("getFileInputStream", 
String.class);
+        m.setAccessible(true);
+
+        // Point to a file guaranteed to not exist; should fallback to 
resource stream, which will be null
+        InputStream is = (InputStream) m.invoke(svc, 
"/this/does/not/exist.txt");
+        // Since no resource with that name, it can still be null; verify 
method returns (null allowed)
+        // Next, try with a real file via temp file which should be returned
+        File tmp = File.createTempFile("ranger-test", ".txt");
+        try {
+            InputStream fis = (InputStream) m.invoke(svc, 
tmp.getAbsolutePath());
+            assertNotNull(fis);
+            fis.close();
+        } finally {
+            //noinspection ResultOfMethodCallIgnored
+            tmp.delete();
+        }
+    }
+
+    @Test
+    public void test03_runInterruptedStopsHA() throws Exception {
+        UnixAuthenticationService svc = new UnixAuthenticationService();
+        UserSyncHAInitializerImpl ha  = mock(UserSyncHAInitializerImpl.class);
+        setField(svc, "userSyncHAInitializerImpl", ha);
+
+        try (MockedConstruction<Thread> threadConstruction = 
mockConstruction(Thread.class, (mock, ctx) -> {
+            doNothing().when(mock).setName(any());
+            doNothing().when(mock).setDaemon(false);
+            doNothing().when(mock).start();
+        })) {
+            // Interrupt current thread so Thread.sleep throws immediately
+            Thread.currentThread().interrupt();
+            // Call run; should catch InterruptedException and then stop HA in 
finally
+            svc.run();
+            // Clear interrupted status for subsequent tests
+            Thread.interrupted();
+        }
+    }
+
+    @Test
+    public void test04_startServiceSslAndAcceptIOExceptionPath() throws 
Throwable {
+        UnixAuthenticationService svc = new UnixAuthenticationService();
+
+        // Reflectively set fields needed by startService
+        setField(svc, "sslEnabled", true);
+        setField(svc, "portNum", 0);
+        setField(svc, "enabledProtocolsList", 
Collections.singletonList("TLSV1.2"));
+        setField(svc, "enabledCipherSuiteList", new ArrayList<>());
+        setField(svc, "keyStorePath", "");
+        setField(svc, "trustStorePath", "");
+        setField(svc, "keyStoreType", KeyStore.getDefaultType());
+        setField(svc, "trustStoreType", KeyStore.getDefaultType());
+
+        try (MockedStatic<SSLContext> sslStatic = 
mockStatic(SSLContext.class)) {
+            SSLContext             sslContext   = mock(SSLContext.class);
+            SSLServerSocketFactory sf           = 
mock(SSLServerSocketFactory.class);
+            SSLServerSocket        secureSocket = mock(SSLServerSocket.class);
+
+            when(SSLContext.getInstance("TLSv1.2")).thenReturn(sslContext);
+            when(sslContext.getServerSocketFactory()).thenReturn(sf);
+            when(sf.createServerSocket(anyInt())).thenReturn(secureSocket);
+            when(secureSocket.getEnabledProtocols()).thenReturn(new String[] 
{"TLSv1.2"});
+            when(secureSocket.getEnabledCipherSuites()).thenReturn(new 
String[] {});
+            when(secureSocket.accept()).thenThrow(new IOException("accept 
fail"));
+
+            assertThrows(IOException.class, svc::startService);
+        }
+    }
+
+    @Test
+    public void test05_initSuccessPathWithBcfksAndAdminLists() throws 
Exception {
+        UnixAuthenticationService svc = new UnixAuthenticationService();
+
+        Properties props    = new Properties();
+        File       credFile = File.createTempFile("ranger-cred", ".bcfks");
+        try {
+            props.setProperty("ranger.usersync.credstore.filename", 
credFile.getAbsolutePath());
+            props.setProperty("ranger.usersync.keystore.file", "");
+            props.setProperty("ranger.usersync.truststore.file", "");
+            props.setProperty("ranger.keystore.file.type", "bcfks");
+            props.setProperty("ranger.truststore.file.type", "bcfks");
+            props.setProperty("ranger.usersync.port", "6168");
+            props.setProperty("ranger.usersync.passwordvalidator.path", 
"/bin/validator");
+            props.setProperty("admin.users", "alice,bob");
+            props.setProperty("admin.roleNames", "ROLE_ADMIN,ROLE_AUDITOR");
+            props.setProperty("ranger.usersync.ssl", "true");
+            props.setProperty("ranger.usersync.https.ssl.enabled.protocols", 
"TLSv1.2");
+            
props.setProperty("ranger.usersync.https.ssl.enabled.cipher.suites", "");
+
+            try (MockedStatic<XMLUtils> xmlMock = mockStatic(XMLUtils.class);
+                    MockedStatic<CredentialReader> credMock = 
mockStatic(CredentialReader.class)) {
+                xmlMock.when(() -> XMLUtils.loadConfig(any(String.class), 
any(Properties.class))).thenAnswer(inv -> {
+                    ((Properties) inv.getArgument(1)).putAll(props);
+                    return null;
+                });
+                credMock.when(() -> CredentialReader.getDecryptedString(any(), 
any(), any())).thenReturn("pass");
+
+                // invoke private init
+                invokeInit(svc);
+            }
+        } finally {
+            //noinspection ResultOfMethodCallIgnored
+            credFile.delete();
+        }
+    }
+
+    @Test
+    public void test06_startServiceWithKeyAndTrustStores() throws Throwable {
+        UnixAuthenticationService svc = new UnixAuthenticationService();
+
+        setField(svc, "sslEnabled", true);
+        setField(svc, "portNum", 0);
+        setField(svc, "enabledProtocolsList", 
Collections.singletonList("TLSV1.2"));
+        setField(svc, "enabledCipherSuiteList", new ArrayList<>());
+        setField(svc, "keyStorePath", "/tmp/keystore.jks");
+        setField(svc, "trustStorePath", "/tmp/truststore.jks");
+        setField(svc, "keyStoreType", KeyStore.getDefaultType());
+        setField(svc, "trustStoreType", KeyStore.getDefaultType());
+        // leave passwords as null to exercise empty defaulting branch
+
+        try (MockedStatic<KeyManagerFactory> kmfStatic = 
mockStatic(KeyManagerFactory.class);
+                MockedStatic<TrustManagerFactory> tmfStatic = 
mockStatic(TrustManagerFactory.class);
+                MockedStatic<SSLContext> sslStatic = 
mockStatic(SSLContext.class)) {
+            KeyManagerFactory kmf = mock(KeyManagerFactory.class);
+            kmfStatic.when(() -> 
KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())).thenReturn(kmf);
+            when(kmf.getKeyManagers()).thenReturn(new KeyManager[0]);
+
+            TrustManagerFactory tmf = mock(TrustManagerFactory.class);
+            tmfStatic.when(() -> 
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())).thenReturn(tmf);
+            when(tmf.getTrustManagers()).thenReturn(new TrustManager[0]);
+
+            SSLContext             sslContext   = mock(SSLContext.class);
+            SSLServerSocketFactory sf           = 
mock(SSLServerSocketFactory.class);
+            SSLServerSocket        secureSocket = mock(SSLServerSocket.class);
+
+            when(SSLContext.getInstance("TLSv1.2")).thenReturn(sslContext);
+            when(sslContext.getServerSocketFactory()).thenReturn(sf);
+            when(sf.createServerSocket(anyInt())).thenReturn(secureSocket);
+            when(secureSocket.getEnabledProtocols()).thenReturn(new String[] 
{"TLSv1.2"});
+            when(secureSocket.getEnabledCipherSuites()).thenReturn(new 
String[] {});
+            when(secureSocket.accept()).thenThrow(new 
java.io.IOException("accept fail"));
+
+            assertThrows(java.io.IOException.class, svc::startService);
+        }
+    }
+
+    @Test
+    public void test07_mainEnableUnixAuthInvokesRun() {
+        try (MockedStatic<UserGroupSyncConfig> ugscStatic = 
mockStatic(UserGroupSyncConfig.class);
+                MockedStatic<UserSyncHAInitializerImpl> haStatic = 
mockStatic(UserSyncHAInitializerImpl.class);
+                MockedConstruction<UnixAuthenticationService> 
serviceConstruction = mockConstruction(UnixAuthenticationService.class, (mock, 
ctx) -> {
+                    // service.run() will be called by main; make it a no-op
+                })) {
+            UserGroupSyncConfig ugsc = mock(UserGroupSyncConfig.class);
+            ugscStatic.when(UserGroupSyncConfig::getInstance).thenReturn(ugsc);
+            when(ugsc.getUserGroupConfig()).thenReturn(new Configuration());
+
+            UserSyncHAInitializerImpl ha = 
mock(UserSyncHAInitializerImpl.class);
+            haStatic.when(() -> 
UserSyncHAInitializerImpl.getInstance(any())).thenReturn(ha);
+
+            UnixAuthenticationService.main(new String[] {"-enableUnixAuth"});
+        }
+    }
+
+    @Test
+    public void test08_startServiceEnablesCipherSuites() throws Throwable {
+        UnixAuthenticationService svc = new UnixAuthenticationService();
+        setField(svc, "sslEnabled", true);
+        setField(svc, "portNum", 0);
+        setField(svc, "enabledProtocolsList", 
Collections.singletonList("TLSV1.2"));
+        ArrayList<String> enabledCiphers = new ArrayList<>();
+        enabledCiphers.add("CIPHER_A");
+        setField(svc, "enabledCipherSuiteList", enabledCiphers);
+        setField(svc, "keyStorePath", "");
+        setField(svc, "trustStorePath", "");
+        setField(svc, "keyStoreType", KeyStore.getDefaultType());
+        setField(svc, "trustStoreType", KeyStore.getDefaultType());
+
+        try (MockedStatic<SSLContext> sslStatic = 
mockStatic(SSLContext.class)) {
+            SSLContext             sslContext   = mock(SSLContext.class);
+            SSLServerSocketFactory sf           = 
mock(SSLServerSocketFactory.class);
+            SSLServerSocket        secureSocket = mock(SSLServerSocket.class);
+
+            when(SSLContext.getInstance("TLSv1.2")).thenReturn(sslContext);
+            when(sslContext.getServerSocketFactory()).thenReturn(sf);
+            when(sf.createServerSocket(anyInt())).thenReturn(secureSocket);
+            when(secureSocket.getEnabledProtocols()).thenReturn(new String[] 
{"TLSv1.2"});
+            when(secureSocket.getEnabledCipherSuites()).thenReturn(new 
String[] {"CIPHER_A", "CIPHER_B"});
+            when(secureSocket.accept()).thenThrow(new IOException("accept 
fail"));
+
+            assertThrows(IOException.class, svc::startService);
+            verify(secureSocket).setEnabledCipherSuites(new String[] 
{"CIPHER_A"});
+        }
+    }
+
+    @Test
+    public void test09_startServiceSpawnsValidatorThread() throws Throwable {
+        UnixAuthenticationService svc = new UnixAuthenticationService();
+        setField(svc, "sslEnabled", true);
+        setField(svc, "portNum", 0);
+        setField(svc, "enabledProtocolsList", 
Collections.singletonList("TLSV1.2"));
+        setField(svc, "enabledCipherSuiteList", new ArrayList<>());
+        setField(svc, "keyStorePath", "");
+        setField(svc, "trustStorePath", "");
+        setField(svc, "keyStoreType", KeyStore.getDefaultType());
+        setField(svc, "trustStoreType", KeyStore.getDefaultType());
+
+        final boolean[] runnableIsPasswordValidator = new boolean[] {false};
+
+        try (MockedStatic<SSLContext> sslStatic = mockStatic(SSLContext.class);
+                MockedConstruction<Thread> threadConstruction = 
mockConstruction(Thread.class, (mockThread, ctx) -> {
+                    Object arg0 = ctx.arguments().get(0);
+                    runnableIsPasswordValidator[0] = (arg0 instanceof 
Runnable) && 
arg0.getClass().getName().equals(PasswordValidator.class.getName());
+                    doNothing().when(mockThread).start();
+                })) {
+            SSLContext             sslContext   = mock(SSLContext.class);
+            SSLServerSocketFactory sf           = 
mock(SSLServerSocketFactory.class);
+            SSLServerSocket        secureSocket = mock(SSLServerSocket.class);
+            Socket                 client       = mock(Socket.class);
+
+            when(SSLContext.getInstance("TLSv1.2")).thenReturn(sslContext);
+            when(sslContext.getServerSocketFactory()).thenReturn(sf);
+            when(sf.createServerSocket(anyInt())).thenReturn(secureSocket);
+            when(secureSocket.getEnabledProtocols()).thenReturn(new String[] 
{"TLSV1.2"});
+            when(secureSocket.getEnabledCipherSuites()).thenReturn(new 
String[] {});
+            when(secureSocket.accept()).thenReturn(client).thenThrow(new 
IOException("done"));
+
+            assertThrows(IOException.class, svc::startService);
+            assertTrue(runnableIsPasswordValidator[0]);
+        }
+    }
+
+    @Test
+    public void 
test10_startUnixUserGroupSyncProcess_MetricsEnabledAndDisabled() throws 
Exception {
+        UnixAuthenticationService svc = new UnixAuthenticationService();
+        Method                    m   = 
UnixAuthenticationService.class.getDeclaredMethod("startUnixUserGroupSyncProcess");
+        m.setAccessible(true);
+
+        try (MockedStatic<UserGroupSyncConfig> ugscStatic = 
mockStatic(UserGroupSyncConfig.class)) {
+            UserGroupSyncConfig ugsc = mock(UserGroupSyncConfig.class);
+            ugscStatic.when(UserGroupSyncConfig::getInstance).thenReturn(ugsc);
+
+            // Metrics enabled -> two threads created
+            when(ugsc.isUserSyncMetricsEnabled()).thenReturn(true);
+            try (MockedConstruction<Thread> threadConstruction = 
mockConstruction(Thread.class, (mockThread, ctx) -> {
+                doNothing().when(mockThread).setName(any());
+                doNothing().when(mockThread).setDaemon(false);
+                doNothing().when(mockThread).start();
+            })) {
+                m.invoke(svc);
+                assertEquals(2, threadConstruction.constructed().size());
+            }
+
+            // Metrics disabled -> one thread created
+            when(ugsc.isUserSyncMetricsEnabled()).thenReturn(false);
+            try (MockedConstruction<Thread> threadConstruction = 
mockConstruction(Thread.class, (mockThread, ctx) -> {
+                doNothing().when(mockThread).setName(any());
+                doNothing().when(mockThread).setDaemon(false);
+                doNothing().when(mockThread).start();
+            })) {
+                m.invoke(svc);
+                assertEquals(1, threadConstruction.constructed().size());
+            }
+        }
+    }
+
+    private void invokeInit(UnixAuthenticationService svc) throws Exception {
+        Method m = UnixAuthenticationService.class.getDeclaredMethod("init");
+        m.setAccessible(true);
+        m.invoke(svc);
+    }
+
+    private void setField(Object target, String fieldName, Object value) 
throws Exception {
+        Field f = UnixAuthenticationService.class.getDeclaredField(fieldName);
+        f.setAccessible(true);
+        f.set(target, value);
+    }
+}

Reply via email to