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

enorman pushed a commit to branch master
in repository 
https://gitbox.apache.org/repos/asf/sling-org-apache-sling-serviceuser-webconsole.git


The following commit(s) were added to refs/heads/master by this push:
     new 11bd87c  SLING-12994 Add junit tests for code coverage (#7)
11bd87c is described below

commit 11bd87c50c984440fffe2966c5f0c4da81ba56af
Author: Eric Norman <[email protected]>
AuthorDate: Mon Nov 10 13:18:20 2025 -0800

    SLING-12994 Add junit tests for code coverage (#7)
---
 pom.xml                                            |  48 +-
 .../impl/ServiceUserWebConsolePlugin.java          | 115 ++-
 .../webconsole/impl/ReflectionTools.java           |  91 ++
 .../impl/ServiceUserWebConsolePluginTest.java      | 959 +++++++++++++++++++++
 4 files changed, 1134 insertions(+), 79 deletions(-)

diff --git a/pom.xml b/pom.xml
index 9e2a047..6dd8eb2 100644
--- a/pom.xml
+++ b/pom.xml
@@ -1,14 +1,14 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <!-- 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. -->
+    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. -->
 <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/maven-v4_0_0.xsd";>
 
     <modelVersion>4.0.0</modelVersion>
@@ -35,6 +35,8 @@
     <properties>
         <sling.java.version>8</sling.java.version>
         
<project.build.outputTimestamp>1734086735</project.build.outputTimestamp>
+        <jackrabbit.version>2.20.6</jackrabbit.version>
+        <oak.version>1.44.0</oak.version>
     </properties>
     <dependencies>
         <dependency>
@@ -53,8 +55,8 @@
         <!-- JCR Specific items -->
         <dependency>
             <groupId>org.apache.jackrabbit</groupId>
-            <artifactId>jackrabbit-api</artifactId>
-            <version>2.10.6</version>
+            <artifactId>oak-jackrabbit-api</artifactId>
+            <version>${oak.version}</version>
             <scope>provided</scope>
         </dependency>
         <dependency>
@@ -71,7 +73,7 @@
         <dependency>
             <groupId>org.apache.jackrabbit</groupId>
             <artifactId>jackrabbit-jcr-commons</artifactId>
-            <version>2.0.0</version>
+            <version>${jackrabbit.version}</version>
             <scope>provided</scope>
         </dependency>
 
@@ -124,8 +126,18 @@
 
         <!-- Testing -->
         <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
             <scope>test</scope>
         </dependency>
         <dependency>
@@ -136,7 +148,13 @@
         <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
-            <version>4.5.1</version>
+            <version>5.6.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.sling-mock.junit5</artifactId>
+            <version>3.5.4</version>
             <scope>test</scope>
         </dependency>
     </dependencies>
diff --git 
a/src/main/java/org/apache/sling/serviceuser/webconsole/impl/ServiceUserWebConsolePlugin.java
 
b/src/main/java/org/apache/sling/serviceuser/webconsole/impl/ServiceUserWebConsolePlugin.java
index e8bd922..f69b4e7 100644
--- 
a/src/main/java/org/apache/sling/serviceuser/webconsole/impl/ServiceUserWebConsolePlugin.java
+++ 
b/src/main/java/org/apache/sling/serviceuser/webconsole/impl/ServiceUserWebConsolePlugin.java
@@ -43,7 +43,6 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
 import java.util.Iterator;
 import java.util.List;
@@ -82,7 +81,6 @@ import org.osgi.service.component.ComponentContext;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
-import org.osgi.service.component.annotations.ReferencePolicyOption;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -121,24 +119,35 @@ public class ServiceUserWebConsolePlugin extends 
AbstractWebConsolePlugin {
 
     private static final Logger log = 
LoggerFactory.getLogger(ServiceUserWebConsolePlugin.class);
 
-    private BundleContext bundleContext;
+    private final BundleContext bundleContext;
 
-    @Reference(policyOption = ReferencePolicyOption.GREEDY)
-    private XSSAPI xss;
+    private final XSSAPI xss;
 
-    @Reference(policyOption = ReferencePolicyOption.GREEDY)
-    private ResourceResolverFactory resolverFactory;
+    private final ResourceResolverFactory resolverFactory;
 
-    @Reference
-    private ServiceUserMapper mapper;
+    private final ServiceUserMapper mapper;
+
+    @Activate
+    public ServiceUserWebConsolePlugin(
+            ComponentContext context,
+            @Reference XSSAPI xss,
+            @Reference ResourceResolverFactory resolverFactory,
+            @Reference ServiceUserMapper mapper) {
+        super();
+        this.bundleContext = context.getBundleContext();
+        this.xss = xss;
+        this.resolverFactory = resolverFactory;
+        this.mapper = mapper;
+    }
 
     private boolean createOrUpdateMapping(HttpServletRequest request, 
ResourceResolver resolver) {
 
         String appPath = getParameter(request, PN_APP_PATH, "");
 
+        String instanceIdentifier = appPath.substring(appPath.lastIndexOf('/') 
+ 1);
+        String pid = String.format("%s~%s", COMPONENT_NAME, 
instanceIdentifier);
         Iterator<Resource> configs = resolver.findResources(
-                "SELECT * FROM [sling:OsgiConfig] WHERE ISDESCENDANTNODE([" + 
appPath + "]) AND NAME() LIKE '"
-                        + COMPONENT_NAME + "%'",
+                "SELECT * FROM [sling:OsgiConfig] WHERE ISDESCENDANTNODE([" + 
appPath + "]) AND NAME() = '" + pid + "'",
                 Query.JCR_SQL2);
 
         try {
@@ -216,7 +225,7 @@ public class ServiceUserWebConsolePlugin extends 
AbstractWebConsolePlugin {
             try {
                 sendErrorRedirect(request, response, "Unexpected exception: " 
+ e);
             } catch (IOException e2) {
-                throw new IOException("Failed to send error response", e2);
+                log.warn("Failed to send error redirect", e2);
             }
         } finally {
             if (needsAdministrativeResolver(request) && resolver != null) {
@@ -236,12 +245,12 @@ public class ServiceUserWebConsolePlugin extends 
AbstractWebConsolePlugin {
                 if (updatePrivileges(request, resolver)) {
                     List<String> params = new ArrayList<>();
                     params.add(PN_ACTION + "=" + "details");
+                    String name = 
userResource.getValueMap().get("rep:principalName", String.class);
                     params.add(PN_ALERT + "="
                             + URLEncoder.encode(
-                                    "Service user " + userResource.getName() + 
" created / updated successfully!",
+                                    "Service user " + name + " created / 
updated successfully!",
                                     StandardCharsets.UTF_8.toString()));
-                    params.add(PN_USER + "="
-                            + URLEncoder.encode(userResource.getName(), 
StandardCharsets.UTF_8.toString()));
+                    params.add(PN_USER + "=" + URLEncoder.encode(name, 
StandardCharsets.UTF_8.toString()));
 
                     WebConsoleUtil.sendRedirect(
                             request, response, "/system/console/" + LABEL + 
"?" + StringUtils.join(params, "&"));
@@ -358,18 +367,16 @@ public class ServiceUserWebConsolePlugin extends 
AbstractWebConsolePlugin {
                 return resolver.getResource(user.getPath());
             } else {
 
-                final String userPath = getParameter(request, PN_USER_PATH, 
"system");
-
-                log.debug("Creating new user with name {} and intermediate 
path {}", name, userPath);
+                // NOTE: use null as the default instead of "system" to allow 
the UserManager
+                //  default location to be applied when the user has not 
specified a value
+                final String intermediatePath = getParameter(request, 
PN_USER_PATH, null);
 
-                User user = userManager.createSystemUser(name, userPath);
-                session.save();
+                log.debug("Creating new user with name {} and intermediate 
path {}", name, intermediatePath);
 
-                String path = "/home/users/" + userPath + "/" + name;
-                log.debug("Moving {} to {}", user.getPath(), path);
-                session.getWorkspace().move(user.getPath(), path);
+                User user = userManager.createSystemUser(name, 
intermediatePath);
                 session.save();
 
+                String path = user.getPath();
                 return resolver.getResource(path);
             }
         } catch (RepositoryException e) {
@@ -400,7 +407,7 @@ public class ServiceUserWebConsolePlugin extends 
AbstractWebConsolePlugin {
                 String path = request.getParameter(param);
                 String privilege = 
request.getParameter(param.replace("-path-", "-privilege-"));
                 if (StringUtils.isNotBlank(path) && 
StringUtils.isNotBlank(privilege)) {
-                    privileges.add(new ImmutablePair<String, String>(path, 
privilege));
+                    privileges.add(new ImmutablePair<>(path, privilege));
                 } else {
                     log.warn("Unable to load ACL due to missing value {}={}", 
path, privilege);
                 }
@@ -511,55 +518,42 @@ public class ServiceUserWebConsolePlugin extends 
AbstractWebConsolePlugin {
         pw.println("</div>");
     }
 
-    @Activate
-    protected void init(ComponentContext context) {
-        this.bundleContext = context.getBundleContext();
-    }
-
     private void printPrincipals(List<Mapping> activeMappings, PrintWriter pw) 
{
         List<Pair<String, Mapping>> mappings = new ArrayList<>();
         for (Mapping mapping : activeMappings) {
             for (String principal : extractPrincipals(mapping)) {
-                mappings.add(new ImmutablePair<String, Mapping>(principal, 
mapping));
+                mappings.add(new ImmutablePair<>(principal, mapping));
             }
         }
-        Collections.sort(mappings, new Comparator<Pair<String, Mapping>>() {
-            @Override
-            public int compare(Pair<String, Mapping> o1, Pair<String, Mapping> 
o2) {
-                if (o1.getKey().equals(o2.getKey())) {
-                    return o1.getValue()
-                            .getServiceName()
-                            .compareTo(o2.getValue().getServiceName());
-                } else {
-                    return o1.getKey().compareTo(o2.getKey());
-                }
+        Collections.sort(mappings, (o1, o2) -> {
+            if (o1.getKey().equals(o2.getKey())) {
+                return 
o1.getValue().getServiceName().compareTo(o2.getValue().getServiceName());
+            } else {
+                return o1.getKey().compareTo(o2.getKey());
             }
         });
 
+        Map<String, Bundle> bundles = new HashMap<>();
         for (Pair<String, Mapping> mapping : mappings) {
             tableRows(pw);
             pw.println("<td><a 
href=\"/system/console/serviceusers?action=details&amp;user="
                     + xss.encodeForHTML(mapping.getKey()) + "\">" + 
xss.encodeForHTML(mapping.getKey()) + "</a></td>");
 
-            Map<String, Bundle> bundles = new HashMap<>();
             Bundle bundle = findBundle(mapping.getValue().getServiceName(), 
bundles);
             if (bundle != null) {
-                bundleContext.getBundle();
                 pw.println("<td><a href=\"/system/console/bundles/" + 
bundle.getBundleId() + "\">"
                         + xss.encodeForHTML(
                                 bundle.getHeaders().get(Constants.BUNDLE_NAME) 
+ " (" + bundle.getSymbolicName())
                         + ")</a></td>");
-                pw.println("<td>" + 
xss.encodeForHTML(mapping.getValue().getSubServiceName()) + TD);
             } else {
-                bundleContext.getBundle();
                 pw.println("<td>" + 
xss.encodeForHTML(mapping.getValue().getServiceName()) + TD);
-                pw.println("<td>"
-                        + xss.encodeForHTML(
-                                mapping.getValue().getSubServiceName() != null
-                                        ? 
mapping.getValue().getSubServiceName()
-                                        : "")
-                        + TD);
             }
+            pw.println("<td>"
+                    + xss.encodeForHTML(
+                            mapping.getValue().getSubServiceName() != null
+                                    ? mapping.getValue().getSubServiceName()
+                                    : "")
+                    + TD);
         }
     }
 
@@ -855,9 +849,7 @@ public class ServiceUserWebConsolePlugin extends 
AbstractWebConsolePlugin {
             idx++;
         }
 
-        if (StringUtils.isNotBlank(alert)) {
-            params.add(PN_ALERT + "=" + URLEncoder.encode(alert, "UTF-8"));
-        }
+        params.add(PN_ALERT + "=" + URLEncoder.encode(alert, 
StandardCharsets.UTF_8.toString()));
 
         WebConsoleUtil.sendRedirect(
                 request, response, "/system/console/" + LABEL + "?" + 
StringUtils.join(params, "&"));
@@ -947,10 +939,8 @@ public class ServiceUserWebConsolePlugin extends 
AbstractWebConsolePlugin {
 
         Map<String, List<String>> toSet = new HashMap<>();
         for (Pair<String, String> privilege : privileges) {
-            if (!toSet.containsKey(privilege.getKey())) {
-                toSet.put(privilege.getKey(), new ArrayList<String>());
-            }
-            toSet.get(privilege.getKey()).add(privilege.getValue());
+            List<String> list = toSet.computeIfAbsent(privilege.getKey(), k -> 
new ArrayList<>());
+            list.add(privilege.getValue());
         }
         log.debug("Loaded updated policy paths: {}", currentPolicies);
 
@@ -1005,13 +995,10 @@ public class ServiceUserWebConsolePlugin extends 
AbstractWebConsolePlugin {
                 for (AccessControlPolicy p : policies) {
                     if (p instanceof AccessControlList) {
                         AccessControlList policy = (AccessControlList) p;
-                        for (AccessControlEntry entry : 
policy.getAccessControlEntries()) {
-                            Principal prin = entry.getPrincipal();
-                            if (prin.getName().equals(name)) {
-                                toRemove = entry;
-                                break;
-                            }
-                        }
+                        toRemove = 
Arrays.stream(policy.getAccessControlEntries())
+                                .filter(entry -> 
entry.getPrincipal().getName().equals(name))
+                                .findFirst()
+                                .orElse(null);
                         if (toRemove != null) {
                             removed = true;
                             policy.removeAccessControlEntry(toRemove);
diff --git 
a/src/test/java/org/apache/sling/serviceuser/webconsole/impl/ReflectionTools.java
 
b/src/test/java/org/apache/sling/serviceuser/webconsole/impl/ReflectionTools.java
new file mode 100644
index 0000000..c40744f
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/serviceuser/webconsole/impl/ReflectionTools.java
@@ -0,0 +1,91 @@
+/*
+ * 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.sling.serviceuser.webconsole.impl;
+
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+
+import static org.junit.jupiter.api.Assertions.fail;
+
+/**
+ * Reflection utilities to facilitate testing
+ */
+@SuppressWarnings("java:S3011")
+public class ReflectionTools {
+
+    private ReflectionTools() {
+        // hide the public constructor
+    }
+
+    public static <T> T getFieldWithReflection(Object obj, String fieldName, 
Class<T> expectedType) {
+        Object result = null;
+        try {
+            Class<?> clazz = obj.getClass();
+            Field field = null;
+            do {
+                try { // NOSONAR
+                    field = clazz.getDeclaredField(fieldName);
+                } catch (NoSuchFieldException nsfe) {
+                    clazz = clazz.getSuperclass();
+                }
+            } while (field == null && clazz != null);
+            if (field != null) {
+                if (Modifier.isStatic(field.getModifiers())) {
+                    field.setAccessible(true);
+                    result = field.get(null);
+                } else {
+                    field.setAccessible(true);
+                    result = field.get(obj);
+                }
+            } else {
+                fail("Failed to find field via reflection: " + fieldName);
+            }
+        } catch (IllegalArgumentException | IllegalAccessException | 
SecurityException e) {
+            fail("Failed to get field via reflection", e);
+        }
+        return expectedType.cast(result);
+    }
+
+    public static void setFieldWithReflection(Object obj, String fieldName, 
Object value) {
+        try {
+            Class<?> clazz = obj.getClass();
+            Field field = null;
+            do {
+                try { // NOSONAR
+                    field = clazz.getDeclaredField(fieldName);
+                } catch (NoSuchFieldException nsfe) {
+                    clazz = clazz.getSuperclass();
+                }
+            } while (field == null && clazz != null);
+            if (field != null) {
+                if (Modifier.isStatic(field.getModifiers())) {
+                    field.setAccessible(true);
+                    field.set(null, value);
+                } else {
+                    field.setAccessible(true);
+                    field.set(obj, value);
+                }
+            } else {
+                fail("Failed to find field via reflection: " + fieldName);
+            }
+        } catch (IllegalArgumentException | IllegalAccessException | 
SecurityException e) {
+            fail("Failed to set field via reflection", e);
+        }
+    }
+}
diff --git 
a/src/test/java/org/apache/sling/serviceuser/webconsole/impl/ServiceUserWebConsolePluginTest.java
 
b/src/test/java/org/apache/sling/serviceuser/webconsole/impl/ServiceUserWebConsolePluginTest.java
new file mode 100644
index 0000000..ed48bf1
--- /dev/null
+++ 
b/src/test/java/org/apache/sling/serviceuser/webconsole/impl/ServiceUserWebConsolePluginTest.java
@@ -0,0 +1,959 @@
+/*
+ * 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.sling.serviceuser.webconsole.impl;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.query.Query;
+import javax.jcr.security.AccessControlEntry;
+import javax.jcr.security.AccessControlList;
+import javax.jcr.security.AccessControlManager;
+import javax.jcr.security.AccessControlPolicy;
+import javax.jcr.security.Privilege;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletResponse;
+
+import java.io.IOException;
+import java.net.URLEncoder;
+import java.nio.charset.StandardCharsets;
+import java.security.Principal;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Stream;
+
+import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.security.JackrabbitAccessControlManager;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.ModifiableValueMap;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.api.resource.ResourceUtil;
+import org.apache.sling.jcr.base.util.AccessControlUtil;
+import org.apache.sling.serviceusermapping.impl.MappingConfigAmendment;
+import org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl;
+import org.apache.sling.testing.mock.jcr.MockJcr;
+import org.apache.sling.testing.mock.osgi.MockBundle;
+import org.apache.sling.testing.mock.sling.ResourceResolverType;
+import org.apache.sling.testing.mock.sling.junit5.SlingContext;
+import org.apache.sling.testing.mock.sling.junit5.SlingContextExtension;
+import org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletRequest;
+import 
org.apache.sling.testing.mock.sling.servlet.MockSlingHttpServletResponse;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.junit.jupiter.params.provider.NullAndEmptySource;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import org.mockito.invocation.InvocationOnMock;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.Constants;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.isNull;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.withSettings;
+
+/**
+ * SLING-12994 junit tests for code coverage
+ */
+@ExtendWith(SlingContextExtension.class)
+class ServiceUserWebConsolePluginTest {
+    private final SlingContext context = new 
SlingContext(ResourceResolverType.JCR_MOCK);
+
+    private ServiceUserWebConsolePlugin plugin;
+
+    /**
+     * Some options to use for test parameters
+     */
+    private enum TestConfigOptions {
+        PRECREATE_SERVICEUSER,
+        PRECREATE_MAPPING_CONFIG,
+        PRECREATE_MAPPING_CONFIG_USERMAPPING,
+        PRECREATE_ACLS,
+        USE_ADMINISTRATIVE_RESOURCE_RESOLVER
+    }
+
+    @BeforeEach
+    void beforeEach() {
+        context.registerInjectActivateService(ServiceUserMapperImpl.class);
+        plugin = 
context.registerInjectActivateService(ServiceUserWebConsolePlugin.class);
+    }
+
+    /**
+     * Test method for {@link 
org.apache.sling.serviceuser.webconsole.impl.ServiceUserWebConsolePlugin#doPost(javax.servlet.http.HttpServletRequest,
 javax.servlet.http.HttpServletResponse)}.
+     */
+    protected static Stream<Arguments> testDoPostArgs() {
+        Map<String, Object> reqParams1 = new HashMap<>();
+        reqParams1.put(ServiceUserWebConsolePlugin.PN_NAME, "myserviceuser1");
+        reqParams1.put(ServiceUserWebConsolePlugin.PN_BUNDLE, "my.bundle1");
+        reqParams1.put(ServiceUserWebConsolePlugin.PN_APP_PATH, 
"/apps/myapp1");
+
+        Map<String, Object> reqParams2 = new HashMap<>(reqParams1);
+        reqParams2.put(ServiceUserWebConsolePlugin.PN_SUB_SERVICE, 
"subservice1");
+
+        Map<String, Object> reqParams3 = new HashMap<>(reqParams1);
+        // privilege params
+        reqParams3.put("acl-path-0", "/content/node1");
+        reqParams3.put("acl-privilege-0", "jcr:read");
+
+        return Stream.of(
+                Arguments.of(reqParams1, new HashSet<>()),
+                Arguments.of(
+                        reqParams1,
+                        new 
HashSet<>(Arrays.asList(TestConfigOptions.USE_ADMINISTRATIVE_RESOURCE_RESOLVER))),
+                Arguments.of(reqParams1, new 
HashSet<>(Arrays.asList(TestConfigOptions.PRECREATE_SERVICEUSER))),
+                Arguments.of(reqParams1, new 
HashSet<>(Arrays.asList(TestConfigOptions.PRECREATE_MAPPING_CONFIG))),
+                Arguments.of(
+                        reqParams1,
+                        new HashSet<>(Arrays.asList(
+                                TestConfigOptions.PRECREATE_SERVICEUSER, 
TestConfigOptions.PRECREATE_MAPPING_CONFIG))),
+                Arguments.of(
+                        reqParams2,
+                        new HashSet<>(Arrays.asList(
+                                TestConfigOptions.PRECREATE_SERVICEUSER,
+                                TestConfigOptions.PRECREATE_MAPPING_CONFIG,
+                                
TestConfigOptions.PRECREATE_MAPPING_CONFIG_USERMAPPING))),
+                Arguments.of(reqParams3, new 
HashSet<>(Arrays.asList(TestConfigOptions.PRECREATE_ACLS))));
+    }
+
+    @ParameterizedTest
+    @MethodSource("testDoPostArgs")
+    void testDoPost(Map<String, Object> requestParams, Set<TestConfigOptions> 
options)
+            throws ServletException, IOException, RepositoryException, 
LoginException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+        request.setParameterMap(requestParams);
+
+        final ResourceResolver rr = request.getResourceResolver();
+        final @Nullable Session jcrSession = rr.adaptTo(Session.class);
+
+        if (options.contains(TestConfigOptions.PRECREATE_SERVICEUSER)) {
+            String name = 
request.getParameter(ServiceUserWebConsolePlugin.PN_NAME);
+            ((JackrabbitSession) 
jcrSession).getUserManager().createSystemUser(name, null);
+        }
+
+        if (options.contains(TestConfigOptions.PRECREATE_MAPPING_CONFIG)) {
+            // create the mapping and mock the jcr query
+            String appPath = 
request.getParameter(ServiceUserWebConsolePlugin.PN_APP_PATH);
+            String identifier = appPath.substring(appPath.lastIndexOf('/') + 
1);
+            String pid = String.format("%s~%s", 
ServiceUserWebConsolePlugin.COMPONENT_NAME, identifier);
+            String path = String.format("%s/config/%s", appPath, pid);
+
+            String mapping = null;
+            if 
(options.contains(TestConfigOptions.PRECREATE_MAPPING_CONFIG_USERMAPPING)) {
+                String bundle = 
request.getParameter(ServiceUserWebConsolePlugin.PN_BUNDLE);
+                String subService = 
request.getParameter(ServiceUserWebConsolePlugin.PN_SUB_SERVICE);
+                String name = 
request.getParameter(ServiceUserWebConsolePlugin.PN_NAME);
+                mapping = toUserMappingValue(bundle, subService, name);
+            }
+            Resource config = createOsgiConfig(rr, path, mapping);
+
+            String statement = String.format(
+                    "SELECT * FROM [sling:OsgiConfig] WHERE 
ISDESCENDANTNODE([%s]) AND NAME() = '%s'", appPath, pid);
+            List<Node> resultList = new ArrayList<>();
+            resultList.add(config.adaptTo(Node.class));
+            MockJcr.setQueryResult(jcrSession, statement, Query.JCR_SQL2, 
resultList);
+        }
+
+        // provide a mocked AccessControlManager object
+        JackrabbitAccessControlManager acm = 
Mockito.mock(JackrabbitAccessControlManager.class);
+        if 
(options.contains(TestConfigOptions.USE_ADMINISTRATIVE_RESOURCE_RESOLVER)) {
+            mockAdministrativeResourceResolver(acm);
+        } else {
+            MockJcr.setAccessControlManager(jcrSession, acm);
+
+            // simulate the resolver request attribute existing
+            
request.setAttribute("org.apache.sling.auth.core.ResourceResolver", rr);
+        }
+
+        if (options.contains(TestConfigOptions.PRECREATE_ACLS)) {
+            Resource node1 = createNodeWithAce(rr, "/content/node1", 
"myserviceuser1");
+            // also an ACE for the root resource for code coverage
+            final @Nullable Resource rootResource = createAce(rr, 
"myserviceuser1", rr.getResource("/"));
+            // mock the findACLs query to return the expected results
+            MockJcr.setQueryResult(
+                    jcrSession,
+                    "SELECT * FROM [rep:GrantACE] AS s WHERE  
[rep:principalName] = 'myserviceuser1'",
+                    Query.JCR_SQL2,
+                    Arrays.asList(node1.adaptTo(Node.class), 
rootResource.adaptTo(Node.class)));
+
+            // mock the access API calls
+            AccessControlPolicy mockPolicy1 = 
Mockito.mock(AccessControlPolicy.class);
+            AccessControlList mockPolicy2 = 
Mockito.mock(AccessControlList.class);
+            AccessControlEntry ace1 = mockAccessControlEntry(acm, 
"myserviceuser2", "jcr:read");
+            AccessControlEntry ace2 = mockAccessControlEntry(acm, 
"myserviceuser1", "jcr:read", "jcr:write");
+            Mockito.doReturn(new AccessControlEntry[] {ace1, ace2})
+                    .when(mockPolicy2)
+                    .getAccessControlEntries();
+            Mockito.doReturn(new AccessControlPolicy[] {mockPolicy1, 
mockPolicy2})
+                    .when(acm)
+                    .getPolicies(anyString());
+        }
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+
+        // NOTE: replace the replaceAccessControlEntry method with one that 
does nothing since we are not
+        //  testing that functionality here and it doesn't work with the 
partially mocked acm.
+        try (MockedStatic<AccessControlUtil> subjectMock = Mockito.mockStatic(
+                AccessControlUtil.class, 
withSettings().defaultAnswer(InvocationOnMock::callRealMethod)); ) {
+            subjectMock
+                    .when(() -> AccessControlUtil.replaceAccessControlEntry(
+                            any(Session.class),
+                            anyString(),
+                            any(Principal.class),
+                            any(String[].class),
+                            any(String[].class),
+                            any(String[].class),
+                            isNull()))
+                    .thenAnswer(invocation -> {
+                        // replaced the method to do nothing
+                        return null;
+                    });
+
+            plugin.doPost(request, response);
+        }
+
+        assertEquals(HttpServletResponse.SC_MOVED_TEMPORARILY, 
response.getStatus());
+        final String location = response.getHeader("Location");
+        assertNotNull(location);
+        
assertTrue(location.contains("Service+user+myserviceuser1+created+%2F+updated+successfully%21"));
+    }
+
+    @Test
+    void testDoPostWithFailureInCreateOrUpdateMapping() throws 
ServletException, IOException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+
+        // simulate the resolver request attribute existing
+        final ResourceResolver rr = Mockito.spy(request.getResourceResolver());
+        // simulate an exception thrown during commit
+        Mockito.doThrow(PersistenceException.class).when(rr).commit();
+        request.setAttribute("org.apache.sling.auth.core.ResourceResolver", 
rr);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put(ServiceUserWebConsolePlugin.PN_NAME, "myserviceuser1");
+        params.put(ServiceUserWebConsolePlugin.PN_BUNDLE, "my.bundle1");
+        params.put(ServiceUserWebConsolePlugin.PN_APP_PATH, "/apps/myapp1");
+        request.setParameterMap(params);
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+
+        // provide a mocked AccessControlManager object
+        JackrabbitAccessControlManager acm = 
Mockito.mock(JackrabbitAccessControlManager.class);
+        MockJcr.setAccessControlManager(rr.adaptTo(Session.class), acm);
+
+        plugin.doPost(request, response);
+
+        assertEquals(HttpServletResponse.SC_MOVED_TEMPORARILY, 
response.getStatus());
+        final String location = response.getHeader("Location");
+        assertNotNull(location);
+        
assertTrue(location.contains("Unable+to+create+service+user+mapping%21"));
+    }
+
+    @Test
+    void testDoPostWithFailureInUpdatePrivileges() throws ServletException, 
IOException, RepositoryException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+
+        // simulate the resolver request attribute existing
+        final ResourceResolver rr = Mockito.spy(request.getResourceResolver());
+        request.setAttribute("org.apache.sling.auth.core.ResourceResolver", 
rr);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put(ServiceUserWebConsolePlugin.PN_NAME, "myserviceuser1");
+        params.put(ServiceUserWebConsolePlugin.PN_BUNDLE, "my.bundle1");
+        params.put(ServiceUserWebConsolePlugin.PN_APP_PATH, "/apps/myapp1");
+        request.setParameterMap(params);
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+
+        // simulate an exception thrown during the updatePrivileges logic
+        Session jcrSession = Mockito.spy(rr.adaptTo(Session.class));
+        
Mockito.doThrow(RepositoryException.class).when(jcrSession).getAccessControlManager();
+        Mockito.doReturn(jcrSession).when(rr).adaptTo(Session.class);
+
+        plugin.doPost(request, response);
+
+        assertEquals(HttpServletResponse.SC_MOVED_TEMPORARILY, 
response.getStatus());
+        final String location = response.getHeader("Location");
+        assertNotNull(location);
+        
assertTrue(location.contains("Unable+to+update+service+user+permissions%21"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(booleans = {true, false})
+    void testDoPostWithFailureToCreateServiceUser(boolean 
throwExceptionDuringRefresh)
+            throws ServletException, IOException, RepositoryException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+
+        // simulate a RepositoryException thrown while creating the system user
+        final ResourceResolver rr = Mockito.spy(request.getResourceResolver());
+        JackrabbitSession jcrSession = Mockito.spy((JackrabbitSession) 
rr.adaptTo(Session.class));
+        Mockito.doReturn(jcrSession).when(rr).adaptTo(Session.class);
+        if (throwExceptionDuringRefresh) {
+            // throw exception during refresh for code coverage
+            
Mockito.doThrow(RepositoryException.class).when(jcrSession).refresh(false);
+        } else {
+            // NOTE: the refresh(false) is not yet implemented by the 
MockSession
+            //   so we have to replace the method to get through this test 
code path
+            Mockito.doNothing().when(jcrSession).refresh(false);
+        }
+        final UserManager um = Mockito.mock(UserManager.class);
+        Mockito.doReturn(um).when(jcrSession).getUserManager();
+        
Mockito.doThrow(RepositoryException.class).when(um).createSystemUser(anyString(),
 nullable(String.class));
+
+        // simulate the resolver request attribute existing
+        request.setAttribute("org.apache.sling.auth.core.ResourceResolver", 
rr);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put(ServiceUserWebConsolePlugin.PN_NAME, "myserviceuser1");
+        params.put(ServiceUserWebConsolePlugin.PN_BUNDLE, "my.bundle1");
+        params.put(ServiceUserWebConsolePlugin.PN_APP_PATH, "/apps/myapp1");
+        request.setParameterMap(params);
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+
+        plugin.doPost(request, response);
+
+        assertEquals(HttpServletResponse.SC_MOVED_TEMPORARILY, 
response.getStatus());
+        final String location = response.getHeader("Location");
+        assertNotNull(location);
+        assertTrue(location.contains("Unable+to+create+service+user%21"));
+    }
+
+    @Test
+    void testDoPostWithCaughtLoginException() throws LoginException, 
ServletException, IOException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+
+        mockResolverFactoryThrowsLoginException();
+
+        Map<String, Object> params = new HashMap<>();
+        params.put(ServiceUserWebConsolePlugin.PN_NAME, "myserviceuser1");
+        params.put(ServiceUserWebConsolePlugin.PN_BUNDLE, "my.bundle1");
+        params.put(ServiceUserWebConsolePlugin.PN_APP_PATH, "/apps/myapp1");
+        request.setParameterMap(params);
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+
+        plugin.doPost(request, response);
+
+        assertEquals(HttpServletResponse.SC_MOVED_TEMPORARILY, 
response.getStatus());
+        final String location = response.getHeader("Location");
+        assertNotNull(location);
+        assertTrue(location.contains("Unexpected+exception"));
+    }
+
+    @SuppressWarnings("deprecation")
+    @Test
+    void testDoPostWithNullResourceResolver() throws LoginException, 
ServletException, IOException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+
+        // simulate the resolverFactory returning a null administrative 
resource resolver
+        ResourceResolverFactory rrf =
+                ReflectionTools.getFieldWithReflection(plugin, 
"resolverFactory", ResourceResolverFactory.class);
+        rrf = Mockito.spy(rrf);
+        
Mockito.doReturn(null).when(rrf).getAdministrativeResourceResolver(null);
+        ReflectionTools.setFieldWithReflection(plugin, "resolverFactory", rrf);
+
+        Map<String, Object> params = new HashMap<>();
+        params.put(ServiceUserWebConsolePlugin.PN_NAME, "myserviceuser1");
+        params.put(ServiceUserWebConsolePlugin.PN_BUNDLE, "my.bundle1");
+        params.put(ServiceUserWebConsolePlugin.PN_APP_PATH, "/apps/myapp1");
+        request.setParameterMap(params);
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+
+        plugin.doPost(request, response);
+
+        assertEquals(HttpServletResponse.SC_MOVED_TEMPORARILY, 
response.getStatus());
+        final String location = response.getHeader("Location");
+        assertNotNull(location);
+        
assertTrue(location.contains("Unable+to+get+serviceresolver+from+request"));
+    }
+
+    @Test
+    void testDoPostWithIOExceptionDuringErrorRedirect() throws LoginException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+
+        mockResolverFactoryThrowsLoginException();
+
+        Map<String, Object> params = new HashMap<>();
+        params.put(ServiceUserWebConsolePlugin.PN_NAME, "myserviceuser1");
+        params.put(ServiceUserWebConsolePlugin.PN_BUNDLE, "my.bundle1");
+        params.put(ServiceUserWebConsolePlugin.PN_APP_PATH, "/apps/myapp1");
+        request.setParameterMap(params);
+
+        final @NotNull MockSlingHttpServletResponse response = 
Mockito.spy(context.response());
+        
Mockito.doThrow(IOException.class).when(response).sendRedirect(anyString());
+
+        assertDoesNotThrow(() -> plugin.doPost(request, response));
+    }
+
+    protected static Stream<Arguments> testDoPostWithMissingParametersArgs() {
+        Map<String, Object> params1 = new HashMap<>();
+        Map<String, Object> params2 = 
Collections.singletonMap(ServiceUserWebConsolePlugin.PN_NAME, "myserviceuser1");
+        Map<String, Object> params3 = new HashMap<>(params2);
+        params3.put(ServiceUserWebConsolePlugin.PN_BUNDLE, "my.bundle1");
+        Map<String, Object> params4 = new HashMap<>();
+        params4.put("acl-path-0", "/content");
+        params4.put("acl-privilege-0", "jcr:read");
+        Map<String, Object> params4b = new HashMap<>(params4);
+        params4.put("acl-path-1", "/content/node1");
+        params4.put("acl-privilege-1", "");
+        params4.put("acl-path-2", "");
+        params4.put("acl-privilege-2", "jcr:read");
+
+        return Stream.of(
+                Arguments.of(params1, params1),
+                Arguments.of(params2, params2),
+                Arguments.of(params3, params3),
+                Arguments.of(params4, params4b));
+    }
+
+    @ParameterizedTest
+    @MethodSource("testDoPostWithMissingParametersArgs")
+    void testDoPostWithMissingParameters(Map<String, Object> params, 
Map<String, Object> expectedRedirectParams)
+            throws ServletException, IOException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+        request.setParameterMap(params);
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+
+        plugin.doPost(request, response);
+
+        assertEquals(HttpServletResponse.SC_MOVED_TEMPORARILY, 
response.getStatus());
+        final String location = response.getHeader("Location");
+        assertNotNull(location);
+        assertTrue(location.contains("Missing+required+parameters%21"));
+
+        // verify that the request parameters are echoed back in the redirect 
location
+        for (Map.Entry<String, Object> param : 
expectedRedirectParams.entrySet()) {
+            final String key = param.getKey();
+            final String charset = StandardCharsets.UTF_8.name();
+            String expected = String.format(
+                    "%s=%s", URLEncoder.encode(key, charset), 
URLEncoder.encode((String) param.getValue(), charset));
+            assertTrue(location.contains(expected), "Expected value in 
redirect location for: " + key);
+        }
+    }
+
+    /**
+     * Test method for {@link 
org.apache.sling.serviceuser.webconsole.impl.ServiceUserWebConsolePlugin#getLabel()}.
+     */
+    @Test
+    void testGetLabel() {
+        assertEquals("serviceusers", plugin.getLabel());
+    }
+
+    /**
+     * Test method for {@link 
org.apache.sling.serviceuser.webconsole.impl.ServiceUserWebConsolePlugin#getResource(java.lang.String)}.
+     */
+    @Test
+    void testGetResource() {
+        
assertNotNull(plugin.getResource("/serviceusers/res/ui/serviceusermanager.js"));
+    }
+
+    @ParameterizedTest
+    @NullAndEmptySource
+    @ValueSource(strings = {"/serviceusers/invalid.js"})
+    void testGetResourceForInvalidPath(String path) {
+        assertNull(plugin.getResource(path));
+    }
+
+    /**
+     * Test method for {@link 
org.apache.sling.serviceuser.webconsole.impl.ServiceUserWebConsolePlugin#getTitle()}.
+     */
+    @Test
+    void testGetTitle() {
+        assertEquals("Service Users", plugin.getTitle());
+    }
+
+    /**
+     * Test method for {@link 
org.apache.sling.serviceuser.webconsole.impl.ServiceUserWebConsolePlugin#renderContent(javax.servlet.http.HttpServletRequest,
 javax.servlet.http.HttpServletResponse)}.
+     */
+    @ParameterizedTest
+    @NullAndEmptySource
+    void testRenderContentForServiceUsers(String action) throws 
ServletException, IOException, RepositoryException {
+
+        final @NotNull BundleContext bundleContext = context.bundleContext();
+
+        // provide some data to print in the output
+        
mockMappingConfigAmendment(bundleContext.getBundle().getSymbolicName(), null, 
"serviceuser2", 3);
+        
mockMappingConfigAmendment(bundleContext.getBundle().getSymbolicName(), 
"subservice1", "serviceuser2", 1);
+        mockMappingConfigAmendment("another.bundle1", "subservice1", 
"serviceuser2", 2);
+        mockMappingConfigAmendment("another.bundle1", null, "serviceuser2", 4);
+        // NOTE: the MockBundle doesn't have a Bundle-Name header, so supply 
one
+        ((MockBundle) bundleContext.getBundle())
+                .setHeaders(Collections.singletonMap(Constants.BUNDLE_NAME, 
"Mock Bundle"));
+
+        // NOTE: The MockBundleContext#getBundles api is always returning an 
empty array
+        // so we need to replace it to get some data to print out during 
ServiceUserWebConsolePlugin#findBundle
+        BundleContext bc =
+                Mockito.spy(ReflectionTools.getFieldWithReflection(plugin, 
"bundleContext", BundleContext.class));
+        Mockito.doReturn(new Bundle[] {bc.getBundle()}).when(bc).getBundles();
+        ReflectionTools.setFieldWithReflection(plugin, "bundleContext", bc);
+
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+        Map<String, Object> params = new HashMap<>();
+        params.put(ServiceUserWebConsolePlugin.PN_ACTION, action);
+        params.put(ServiceUserWebConsolePlugin.PN_ALERT, "some alert here");
+        // privilege params
+        params.put("acl-path-0", "/content");
+        params.put("acl-privilege-0", "jcr:read");
+        request.setParameterMap(params);
+
+        // simulate the resolver request attribute existing
+        final ResourceResolver rr = request.getResourceResolver();
+        request.setAttribute("org.apache.sling.auth.core.ResourceResolver", 
rr);
+
+        // provide a mocked AccessControlManager object
+        JackrabbitAccessControlManager acm = 
Mockito.mock(JackrabbitAccessControlManager.class);
+        final Privilege[] supportedPrivileges = new Privilege[] {
+            createMockPrivilege(acm, Privilege.JCR_READ), 
createMockPrivilege(acm, Privilege.JCR_WRITE)
+        };
+        
Mockito.doReturn(supportedPrivileges).when(acm).getSupportedPrivileges("/");
+        MockJcr.setAccessControlManager(rr.adaptTo(Session.class), acm);
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+        plugin.renderContent(request, response);
+        final String outputAsString = response.getOutputAsString();
+        assertNotNull(outputAsString);
+    }
+
+    @Test
+    void testRenderContentForServiceUsersWithCaughtLoginException()
+            throws ServletException, IOException, LoginException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+        Map<String, Object> params = new HashMap<>();
+        request.setParameterMap(params);
+
+        mockResolverFactoryThrowsLoginException();
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+        plugin.renderContent(request, response);
+        final String outputAsString = response.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertTrue(outputAsString.contains("Exception rendering service 
users"));
+    }
+
+    protected static Stream<Arguments> 
testRenderContentForServiceUserDetailsArgs() {
+        return Stream.of(
+                Arguments.of(new HashSet<>()),
+                Arguments.of(new 
HashSet<>(Arrays.asList(TestConfigOptions.USE_ADMINISTRATIVE_RESOURCE_RESOLVER))),
+                Arguments.of(new 
HashSet<>(Arrays.asList(TestConfigOptions.PRECREATE_SERVICEUSER))));
+    }
+
+    @ParameterizedTest
+    @MethodSource("testRenderContentForServiceUserDetailsArgs")
+    void testRenderContentForServiceUserDetails(Set<TestConfigOptions> options)
+            throws ServletException, IOException, RepositoryException, 
LoginException {
+        final @NotNull BundleContext bundleContext = context.bundleContext();
+
+        // provide some mapping data to print in the output
+        mockMappingConfigAmendment(
+                bundleContext.getBundle().getSymbolicName(), null, null, 
Arrays.asList("myserviceuser1"), 3);
+        
mockMappingConfigAmendment(bundleContext.getBundle().getSymbolicName(), 
"subservice1", "myserviceuser1", 1);
+        mockMappingConfigAmendment(
+                "another.bundle1", "subservice2", null, 
Arrays.asList("otheruser1", "otheruser2"), 2);
+        mockMappingConfigAmendment("another.bundle2", "subservice3", 
"otheruser3", 5);
+
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+
+        final ResourceResolver rr = request.getResourceResolver();
+        final @Nullable Session jcrSession = rr.adaptTo(Session.class);
+
+        // provide some "OSGi Configurations" to print in the output and mock 
the queries
+        String mapping1 =
+                
toUserMappingValue(bundleContext.getBundle().getSymbolicName(), "subservice1", 
"myserviceuser1");
+        Resource config1 = createOsgiConfig(
+                rr,
+                
"/apps/sling/install/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~test1",
+                mapping1);
+        MockJcr.setQueryResult(
+                jcrSession,
+                "SELECT * FROM [sling:OsgiConfig] AS s WHERE 
(ISDESCENDANTNODE([/apps]) OR ISDESCENDANTNODE([/libs])) AND NAME(s) LIKE 
'org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended%' AND 
[user.mapping] LIKE '%=myserviceuser1'",
+                Query.JCR_SQL2,
+                Arrays.asList(config1.adaptTo(Node.class)));
+        // also the alternate code path where the config is stored as a 
nt:file resource
+        String mapping2 =
+                
toUserMappingValue(bundleContext.getBundle().getSymbolicName(), "subservice2", 
"myserviceuser1");
+        Resource config2 = createFileConfig(
+                rr,
+                
"/apps/sling/install/org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended~test2",
+                mapping2);
+        MockJcr.setQueryResult(
+                jcrSession,
+                "SELECT * FROM [nt:file] AS s WHERE (ISDESCENDANTNODE([/apps]) 
OR ISDESCENDANTNODE([/libs])) AND NAME(s) LIKE 
'org.apache.sling.serviceusermapping.impl.ServiceUserMapperImpl.amended%' AND 
[jcr:content/jcr:data] LIKE '%=myserviceuser1%'",
+                Query.JCR_SQL2,
+                Arrays.asList(config2.adaptTo(Node.class)));
+
+        // provide some "ACLs" to print in the output and mock the query
+        Resource node1 = createNodeWithAce(rr, "/content/node1", 
"myserviceuser1");
+        MockJcr.setQueryResult(
+                jcrSession,
+                "SELECT * FROM [rep:GrantACE] AS s WHERE  [rep:principalName] 
= 'myserviceuser1'",
+                Query.JCR_SQL2,
+                Arrays.asList(node1.adaptTo(Node.class)));
+
+        if (options.contains(TestConfigOptions.PRECREATE_SERVICEUSER)) {
+            ((JackrabbitSession) 
jcrSession).getUserManager().createSystemUser("myserviceuser1", null);
+        }
+
+        Map<String, Object> params = new HashMap<>();
+        params.put(ServiceUserWebConsolePlugin.PN_ACTION, "details");
+        params.put(ServiceUserWebConsolePlugin.PN_USER, "myserviceuser1");
+        request.setParameterMap(params);
+
+        // provide a mocked AccessControlManager object
+        JackrabbitAccessControlManager acm = 
Mockito.mock(JackrabbitAccessControlManager.class);
+        final Privilege[] supportedPrivileges = new Privilege[] {
+            createMockPrivilege(acm, Privilege.JCR_READ), 
createMockPrivilege(acm, Privilege.JCR_WRITE)
+        };
+        
Mockito.doReturn(supportedPrivileges).when(acm).getSupportedPrivileges("/");
+        if 
(options.contains(TestConfigOptions.USE_ADMINISTRATIVE_RESOURCE_RESOLVER)) {
+            mockAdministrativeResourceResolver(acm);
+        } else {
+            MockJcr.setAccessControlManager(jcrSession, acm);
+
+            // simulate the resolver request attribute existing
+            
request.setAttribute("org.apache.sling.auth.core.ResourceResolver", rr);
+        }
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+        plugin.renderContent(request, response);
+        final String outputAsString = response.getOutputAsString();
+        assertNotNull(outputAsString);
+    }
+
+    @Test
+    void testRenderContentForServiceUserDetailsWithCaughtLoginException()
+            throws ServletException, IOException, LoginException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+        Map<String, Object> params = new HashMap<>();
+        params.put(ServiceUserWebConsolePlugin.PN_ACTION, "details");
+        params.put(ServiceUserWebConsolePlugin.PN_USER, "myserviceuser1");
+        request.setParameterMap(params);
+
+        // simulate the resolverFactory throwing a LoginException
+        mockResolverFactoryThrowsLoginException();
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+        plugin.renderContent(request, response);
+        final String outputAsString = response.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertTrue(outputAsString.contains("Exception rendering details for 
user"));
+    }
+
+    @Test
+    void testRenderContentForUnknownAction() throws ServletException, 
IOException {
+        final @NotNull MockSlingHttpServletRequest request = context.request();
+
+        Map<String, Object> params = new HashMap<>();
+        params.put(ServiceUserWebConsolePlugin.PN_ACTION, "invalid");
+        request.setParameterMap(params);
+
+        final @NotNull MockSlingHttpServletResponse response = 
context.response();
+        plugin.renderContent(request, response);
+        final String outputAsString = response.getOutputAsString();
+        assertNotNull(outputAsString);
+        assertTrue(outputAsString.contains("Unknown action: invalid"));
+    }
+
+    // -------------------------- begin helper methods 
---------------------------
+
+    /**
+     * Register a mock a service user mapping config amendment service
+     *
+     * @param bundleContext the bundle context
+     * @param bundle the symbolic name of the bundle
+     * @param subService the subservice name (or null)
+     * @param name the service user name
+     * @param serviceRanking the ranking value of the registered service
+     */
+    private void mockMappingConfigAmendment(
+            @NotNull String bundle, @Nullable String subService, @NotNull 
String name, long serviceRanking) {
+        mockMappingConfigAmendment(bundle, subService, name, 
Collections.emptyList(), serviceRanking);
+    }
+
+    /**
+     * Register a mock a service user mapping config amendment service
+     *
+     * @param bundleContext the bundle context
+     * @param bundle the symbolic name of the bundle
+     * @param subService the subservice name (or null)
+     * @param name (optional) the service user name
+     * @param principalNames the service user names (used only when "name" is 
null)
+     * @param serviceRanking the ranking value of the registered service
+     */
+    private void mockMappingConfigAmendment(
+            @NotNull String bundle,
+            @Nullable String subService,
+            @Nullable String name,
+            @NotNull Collection<String> principalNames,
+            long serviceRanking) {
+        String mapping = toUserMappingValue(bundle, subService, name, 
principalNames);
+
+        Map<String, Object> config1 = new HashMap<>();
+        config1.put("user.mapping", mapping);
+        config1.put(Constants.SERVICE_RANKING, serviceRanking);
+        context.registerInjectActivateService(MappingConfigAmendment.class, 
config1);
+    }
+
+    /**
+     * Creates a mocked privilege
+     *
+     * @param acm the access control manager
+     * @param name the privilege name
+     * @return the mocked privilege
+     */
+    private static @NotNull Privilege createMockPrivilege(@Nullable 
AccessControlManager acm, @NotNull String name)
+            throws RepositoryException {
+        Privilege p;
+        if (acm == null) {
+            p = Mockito.mock(Privilege.class);
+            Mockito.when(p.getDeclaredAggregatePrivileges()).thenReturn(new 
Privilege[0]);
+            Mockito.when(p.getName()).thenReturn(name);
+        } else {
+            p = acm.privilegeFromName(name);
+            if (p == null) {
+                // does not exist yet?
+                p = Mockito.mock(Privilege.class);
+                Mockito.when(p.getName()).thenReturn(name);
+                
Mockito.when(p.getDeclaredAggregatePrivileges()).thenReturn(new Privilege[0]);
+                Mockito.when(acm.privilegeFromName(name)).thenReturn(p);
+            }
+        }
+        return p;
+    }
+
+    /**
+     * Creates a mocked access control entry
+     *
+     * @param acm the access control manager
+     * @param user the user name
+     * @param privilegeNames the names of the privileges
+     * @return the mocked access control entry
+     */
+    private @NotNull AccessControlEntry mockAccessControlEntry(
+            @NotNull AccessControlManager acm, @NotNull String user, @NotNull 
String... privilegeNames)
+            throws RepositoryException {
+        AccessControlEntry ace1 = Mockito.mock(AccessControlEntry.class);
+        Principal principal1 = () -> user;
+        Mockito.doReturn(principal1).when(ace1).getPrincipal();
+        List<Privilege> privileges = new ArrayList<>();
+        for (String pname : privilegeNames) {
+            privileges.add(createMockPrivilege(acm, pname));
+        }
+        Privilege[] privilegesArray = privileges.toArray(new 
Privilege[privileges.size()]);
+        Mockito.doReturn(privilegesArray).when(ace1).getPrivileges();
+        return ace1;
+    }
+
+    /**
+     * Creates a sling:OsgiConfig configuration resource
+     *
+     * @param rr the resource resolver
+     * @param path the path for the cresource
+     * @param mapping (optional) the mapping to apply
+     * @return
+     * @throws PersistenceException
+     */
+    private @NotNull Resource createOsgiConfig(
+            final @NotNull ResourceResolver rr, @NotNull String path, 
@Nullable String mapping)
+            throws PersistenceException {
+        Resource config = ResourceUtil.getOrCreateResource(
+                rr,
+                path,
+                Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, 
"sling:OsgiConfig"),
+                NodeType.NT_FOLDER,
+                false);
+
+        if (mapping != null) {
+            ModifiableValueMap properties = 
config.adaptTo(ModifiableValueMap.class);
+            List<String> m = new ArrayList<>();
+            m.add(mapping);
+            properties.put("user.mapping", m.toArray(new String[m.size()]));
+        }
+        rr.commit();
+
+        return config;
+    }
+
+    /**
+     * Creates a nt:file configuration resource
+     *
+     * @param rr the resource resolver
+     * @param path the path for the cresource
+     * @param mapping (optional) the mapping to apply
+     * @return
+     * @throws PersistenceException
+     */
+    private @NotNull Resource createFileConfig(
+            final @NotNull ResourceResolver rr, @NotNull String path, 
@Nullable String mapping)
+            throws PersistenceException {
+        Resource config = ResourceUtil.getOrCreateResource(
+                rr, path, 
Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, "nt:file"), 
NodeType.NT_FOLDER, false);
+
+        Map<String, Object> properties = new HashMap<>();
+        if (mapping != null) {
+            List<String> m = new ArrayList<>();
+            m.add(mapping);
+            properties.put("jcr:data", m.toArray(new String[m.size()]));
+        }
+        rr.create(config, "jcr:content", properties);
+        rr.commit();
+
+        return config;
+    }
+
+    /**
+     * Creates a folder resource with an ACL pre-created
+     *
+     * @param rr the resource resolver
+     * @param path the path of the resource
+     * @param userid the user name to apply the access control entry for
+     * @return the created resource
+     */
+    private @NotNull Resource createNodeWithAce(
+            @NotNull ResourceResolver rr, @NotNull String path, @NotNull 
String userid) throws PersistenceException {
+        Resource config = ResourceUtil.getOrCreateResource(
+                rr,
+                path,
+                Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, 
(Object) "nt:folder"),
+                NodeType.NT_FOLDER,
+                false);
+        Resource ace = createAce(rr, userid, config);
+        rr.commit();
+        return ace;
+    }
+
+    /**
+     * Creates an ACL with an ACE for a resource
+     *
+     * @param rr the resource resolver
+     * @param userid the user name to apply the access control entry for
+     * @param parent the resource to add the ACL to
+     * @return the created ACE resource
+     */
+    private @NotNull Resource createAce(@NotNull ResourceResolver rr, @NotNull 
String userid, @NotNull Resource parent)
+            throws PersistenceException {
+        Resource acl =
+                rr.create(parent, "rep:policy", 
Collections.singletonMap(JcrConstants.JCR_PRIMARYTYPE, "rep:ACL"));
+        Map<String, Object> props = new HashMap<>();
+        props.put(JcrConstants.JCR_PRIMARYTYPE, "rep:GrantACE");
+        props.put("rep:privileges", new String[] {"jcr:read"});
+        props.put("rep:principalName", userid);
+        Resource ace = rr.create(acl, userid, props);
+        rr.commit();
+        return ace;
+    }
+
+    /**
+     * Mock the factory calls for an administrative resource resolver
+     *
+     * @param acm the access control manager
+     */
+    @SuppressWarnings("deprecation")
+    private void mockAdministrativeResourceResolver(@NotNull 
JackrabbitAccessControlManager acm) throws LoginException {
+        // hack to workaround the mock administrative session not having the 
access control manager set
+        ResourceResolverFactory factory = Mockito.spy(
+                ReflectionTools.getFieldWithReflection(plugin, 
"resolverFactory", ResourceResolverFactory.class));
+        ReflectionTools.setFieldWithReflection(plugin, "resolverFactory", 
factory);
+        // overwrite the getAdministrativeResourceResolver method to produce a 
ResourceResolver that has the
+        // accessControlManager set
+        Mockito.doAnswer(invocation -> {
+                    ResourceResolver arr = (ResourceResolver) 
invocation.callRealMethod();
+                    Session session = arr.adaptTo(Session.class);
+                    ReflectionTools.setFieldWithReflection(session, 
"accessControlManager", acm);
+                    return arr;
+                })
+                .when(factory)
+                .getAdministrativeResourceResolver(null);
+    }
+
+    /**
+     * Mocks the scenario where the ResourceResolverFactory throws a 
LoginException
+     * during the getAdministrativeResourceResolver call
+     */
+    @SuppressWarnings("deprecation")
+    private void mockResolverFactoryThrowsLoginException() throws 
LoginException {
+        ResourceResolverFactory rrf =
+                ReflectionTools.getFieldWithReflection(plugin, 
"resolverFactory", ResourceResolverFactory.class);
+        rrf = Mockito.spy(rrf);
+        
Mockito.doThrow(LoginException.class).when(rrf).getAdministrativeResourceResolver(null);
+        ReflectionTools.setFieldWithReflection(plugin, "resolverFactory", rrf);
+    }
+
+    /**
+     * Formats the data it a user.mapping string
+     *
+     * @param bundle the bundle name
+     * @param subService (optional) the subservice name
+     * @param name the service username
+     * @return the formatted mapping value
+     */
+    private String toUserMappingValue(@NotNull String bundle, @Nullable String 
subService, @NotNull String name) {
+        return toUserMappingValue(bundle, subService, name, 
Collections.emptyList());
+    }
+
+    /**
+     * Formats the data it a user.mapping string
+     *
+     * @param bundle the bundle name
+     * @param subService (optional) the subservice name
+     * @param name (optional) the service username
+     * @param principalNames the names of the principals (used only if "name" 
is null)
+     * @return the formatted mapping value
+     */
+    private String toUserMappingValue(
+            @NotNull String bundle,
+            @Nullable String subService,
+            @Nullable String name,
+            @NotNull Collection<String> principalNames) {
+        String value;
+        if (name != null) {
+            value = name;
+        } else {
+            value = "[" + String.join(",", principalNames) + "]";
+        }
+        return bundle + (StringUtils.isNotBlank(subService) ? ":" + subService 
: "") + "=" + value;
+    }
+}

Reply via email to