You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by en...@apache.org on 2023/04/20 23:14:13 UTC

[sling-org-apache-sling-testing-jcr-mock] branch master updated: SLING-11830 Provide a basic MockPrincipalManager and MockUserManager (#18)

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-testing-jcr-mock.git


The following commit(s) were added to refs/heads/master by this push:
     new fd71883  SLING-11830 Provide a basic MockPrincipalManager and MockUserManager (#18)
fd71883 is described below

commit fd71883a6a9e1db741f1b7196c6b9639aea8fc50
Author: Eric Norman <en...@apache.org>
AuthorDate: Thu Apr 20 16:14:08 2023 -0700

    SLING-11830 Provide a basic MockPrincipalManager and MockUserManager (#18)
---
 pom.xml                                            |  11 +
 .../sling/testing/mock/jcr/MockAuthorizable.java   | 182 +++++++++++
 .../apache/sling/testing/mock/jcr/MockGroup.java   | 156 +++++++++
 .../testing/mock/jcr/MockPrincipalManager.java     | 160 +++++++++
 .../apache/sling/testing/mock/jcr/MockSession.java |  57 +++-
 .../apache/sling/testing/mock/jcr/MockUser.java    | 125 +++++++
 .../sling/testing/mock/jcr/MockUserManager.java    | 235 +++++++++++++
 .../apache/sling/testing/mock/jcr/LogCapture.java  |  91 ++++++
 .../testing/mock/jcr/MockAuthorizableTest.java     | 250 ++++++++++++++
 .../sling/testing/mock/jcr/MockGroupTest.java      | 245 ++++++++++++++
 .../testing/mock/jcr/MockPrincipalManagerTest.java | 362 +++++++++++++++++++++
 .../sling/testing/mock/jcr/MockSessionTest.java    |  67 ++++
 .../testing/mock/jcr/MockUserManagerTest.java      | 344 ++++++++++++++++++++
 .../sling/testing/mock/jcr/MockUserTest.java       | 191 +++++++++++
 src/test/resources/logback-test.xml                |  30 ++
 15 files changed, 2505 insertions(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 98bf0f6..ab6c021 100644
--- a/pom.xml
+++ b/pom.xml
@@ -58,6 +58,11 @@
             <artifactId>jcr</artifactId>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <scope>compile</scope>
+        </dependency>
 
         <dependency>
             <groupId>org.apache.jackrabbit</groupId>
@@ -115,6 +120,12 @@
             <artifactId>junit</artifactId>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>ch.qos.logback</groupId>
+            <artifactId>logback-classic</artifactId>
+            <version>1.2.3</version>
+            <scope>test</scope>
+        </dependency>
 
     </dependencies>
 
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockAuthorizable.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockAuthorizable.java
new file mode 100644
index 0000000..e8be563
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockAuthorizable.java
@@ -0,0 +1,182 @@
+/*
+ * 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.testing.mock.jcr;
+
+import java.nio.file.Paths;
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.value.ValueFactoryImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Mock {@link Authorizable} implementation.
+ */
+abstract class MockAuthorizable implements Authorizable {
+    static final String REP_PRINCIPAL_NAME = "rep:principalName";
+    protected String id;
+    protected Principal principal;
+    protected String path;
+    protected MockUserManager mockUserMgr;
+    protected Map<String, Value[]> propsMap = new HashMap<>();
+    protected Set<Group> declaredMemberOf = new HashSet<>();
+
+    MockAuthorizable(@Nullable String id, @Nullable Principal principal,
+            @Nullable String intermediatePath,
+            @NotNull MockUserManager mockUserMgr) {
+        this.principal = principal;
+        if (id == null && principal != null) {
+            this.id = principal.getName();
+        } else {
+            this.id = id;
+        }
+        if (principal == null) {
+            this.principal = () -> this.id;
+        }
+
+        if (intermediatePath == null) {
+            // use a default intermediate path when none supplied
+            if (isGroup()) {
+                intermediatePath = "/home/groups"; // NOSONAR
+            } else {
+                intermediatePath = "/home/users"; // NOSONAR
+            }
+        }
+        this.path = Paths.get(intermediatePath, this.id).toString();
+        this.mockUserMgr = mockUserMgr;
+
+        // pre-populate the principalName property that is needed by MockPrincipalManager#findPrincipals
+        propsMap.put(REP_PRINCIPAL_NAME, new Value[] {ValueFactoryImpl.getInstance().createValue(this.id)});
+    }
+
+    @Override
+    public @NotNull String getID() throws RepositoryException {
+        return id;
+    }
+
+    @Override
+    public boolean isGroup() {
+        return this instanceof Group;
+    }
+
+    @Override
+    public @NotNull Principal getPrincipal() throws RepositoryException {
+        return principal;
+    }
+
+    @Override
+    public @NotNull Iterator<Group> declaredMemberOf() throws RepositoryException {
+        return declaredMemberOf.iterator();
+    }
+
+    @Override
+    public @NotNull Iterator<Group> memberOf() throws RepositoryException {
+        Set<Group> memberOf = new HashSet<>();
+        calcMemberOf(memberOf, this, new HashSet<>());
+        return memberOf.iterator();
+    }
+    /**
+     * Drills down into nested groups to find all the members
+     * 
+     * @param members the set to add the found people to
+     * @param group the group to process
+     * @param processedGroups the set of groups that have already been processed
+     * @throws RepositoryException
+     */
+    private void calcMemberOf(Set<Group> memberOf, Authorizable authorizable, Set<Authorizable> processedAuthorizables) throws RepositoryException {
+        if (!processedAuthorizables.contains(authorizable)) {
+            // mark as processed
+            processedAuthorizables.add(authorizable);
+
+            @NotNull Iterator<Group> declaredIt = authorizable.declaredMemberOf();
+            while(declaredIt.hasNext()) {
+                Group group = declaredIt.next();
+                memberOf.add(group);
+
+                calcMemberOf(memberOf, group, processedAuthorizables);
+            }
+        }
+    }
+
+
+    @Override
+    public void remove() throws RepositoryException {
+        mockUserMgr.removeAuthorizable(this);
+    }
+
+    @Override
+    public @NotNull Iterator<String> getPropertyNames() throws RepositoryException {
+        Set<String> propNames = propsMap.keySet().stream()
+                .filter(key -> key.indexOf('/') == -1)
+                .collect(Collectors.toSet());
+            return propNames.iterator();
+    }
+
+    @Override
+    public @NotNull Iterator<String> getPropertyNames(@NotNull String relPath) throws RepositoryException {
+        Set<String> propNames = propsMap.keySet().stream()
+            .filter(key -> key.startsWith(relPath))
+            .collect(Collectors.toSet());
+        return propNames.iterator();
+    }
+
+    @Override
+    public boolean hasProperty(@NotNull String relPath) throws RepositoryException {
+        return propsMap.containsKey(relPath);
+    }
+
+    @Override
+    public void setProperty(@NotNull String relPath, @Nullable Value value) throws RepositoryException {
+        propsMap.put(relPath, new Value[] {value});
+    }
+
+    @Override
+    public void setProperty(@NotNull String relPath, @Nullable Value[] value) throws RepositoryException {
+        propsMap.put(relPath, value);
+    }
+
+    @Override
+    public @Nullable Value[] getProperty(@NotNull String relPath) throws RepositoryException {
+        return propsMap.get(relPath);
+    }
+
+    @Override
+    public boolean removeProperty(@NotNull String relPath) throws RepositoryException {
+        return propsMap.remove(relPath) != null;
+    }
+
+    @Override
+    public @NotNull String getPath() throws RepositoryException {
+        return path;
+    }
+
+    public void addDeclaredMemberOf(Group group) {
+        declaredMemberOf.add(group);
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockGroup.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockGroup.java
new file mode 100644
index 0000000..27ff95b
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockGroup.java
@@ -0,0 +1,156 @@
+/*
+ * 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.testing.mock.jcr;
+
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+
+/**
+ * Mock {@link Group} implementation.
+ */
+class MockGroup extends MockAuthorizable implements Group {
+    private Map<String, Authorizable> declaredMembers = new HashMap<>();
+
+    public MockGroup(@Nullable String id, @Nullable Principal principal,
+            @Nullable String intermediatePath,
+            @NotNull MockUserManager mockUserMgr) {
+        super(id, principal, intermediatePath, mockUserMgr);
+    }
+
+    @Override
+    public @NotNull Iterator<Authorizable> getDeclaredMembers() throws RepositoryException {
+        return declaredMembers.values().iterator();
+    }
+
+    @Override
+    public @NotNull Iterator<Authorizable> getMembers() throws RepositoryException {
+        Set<Authorizable> members = new HashSet<>();
+        calcMembers(members, this, new HashSet<>());
+        return members.iterator();
+    }
+
+    /**
+     * Drills down into nested groups to find all the members
+     * 
+     * @param members the set to add the found people to
+     * @param group the group to process
+     * @param processedGroups the set of groups that have already been processed
+     * @throws RepositoryException
+     */
+    private void calcMembers(Set<Authorizable> members, Group group, Set<Group> processedGroups) throws RepositoryException {
+        if (!processedGroups.contains(group)) {
+            // mark as processed
+            processedGroups.add(group);
+
+            @NotNull Iterator<Authorizable> declaredIt = group.getDeclaredMembers();
+            while(declaredIt.hasNext()) {
+                Authorizable authorizable = declaredIt.next();
+                members.add(authorizable);
+
+                if (authorizable instanceof Group) {
+                    Group subgroup = (Group)authorizable;
+                    calcMembers(members, subgroup, processedGroups);
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean isDeclaredMember(@NotNull Authorizable authorizable) throws RepositoryException {
+        return declaredMembers.containsValue(authorizable);
+    }
+
+    @Override
+    public boolean isMember(@NotNull Authorizable authorizable) throws RepositoryException {
+        boolean value = declaredMembers.containsValue(authorizable);
+        if (!value) {
+            // groups
+            for (Authorizable m : declaredMembers.values()) {
+                if (m.isGroup()) {
+                    value = ((Group)m).isDeclaredMember(authorizable);
+                }
+                if (value) {
+                    break;
+                }
+            }
+        }
+        return value;
+    }
+
+    @Override
+    public boolean addMember(@NotNull Authorizable authorizable) throws RepositoryException {
+        boolean added = false;
+        if (!isMember(authorizable)) {
+            declaredMembers.put(authorizable.getID(), authorizable);
+            ((MockAuthorizable)authorizable).addDeclaredMemberOf(this);
+            added = true;
+        }
+        return added;
+    }
+
+    @Override
+    public @NotNull Set<String> addMembers(@NotNull String... memberIds) throws RepositoryException {
+        Set<String> added = new HashSet<>();
+        for (String id : memberIds) {
+            if (!declaredMembers.containsKey(id)) {
+                @Nullable Authorizable authorizable = mockUserMgr.getAuthorizable(id);
+                if (authorizable != null) {
+                    addMember(authorizable);
+                    added.add(id);
+                }
+            }
+        }
+        return added;
+    }
+
+    @Override
+    public boolean removeMember(@NotNull Authorizable authorizable) throws RepositoryException {
+        return declaredMembers.remove(authorizable.getID(), authorizable);
+    }
+
+    @Override
+    public @NotNull Set<String> removeMembers(@NotNull String... memberIds) throws RepositoryException {
+        Set<String> removed = new HashSet<>();
+        for (String id : memberIds) {
+            if (declaredMembers.remove(id) != null) {
+                removed.add(id);
+            }
+        }
+        return removed;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("MockGroup [id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockPrincipalManager.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockPrincipalManager.java
new file mode 100644
index 0000000..ba32cd5
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockPrincipalManager.java
@@ -0,0 +1,160 @@
+/*
+ * 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.testing.mock.jcr;
+
+import java.security.Principal;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.principal.PrincipalIterator;
+import org.apache.jackrabbit.api.security.principal.PrincipalManager;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ *
+ */
+public class MockPrincipalManager implements PrincipalManager {
+    private Logger logger = LoggerFactory.getLogger(getClass());
+    protected MockUserManager mockUserManager;
+    private Principal everyone = () -> "everyone";
+
+    MockPrincipalManager(MockUserManager mockUserManager) {
+        this.mockUserManager = mockUserManager;
+    }
+
+    @Override
+    public @NotNull PrincipalIterator findPrincipals(@Nullable String simpleFilter) {
+        return findPrincipals(simpleFilter, UserManager.SEARCH_TYPE_AUTHORIZABLE);
+    }
+
+    @Override
+    public @NotNull PrincipalIterator findPrincipals(@Nullable String simpleFilter, int searchType) {
+        Set<Principal> principals = new HashSet<>();
+        try {
+            @NotNull Iterator<Authorizable> authorizables = mockUserManager.findAuthorizables(
+                    MockAuthorizable.REP_PRINCIPAL_NAME, simpleFilter, searchType);
+            while (authorizables.hasNext()) {
+                Authorizable next = authorizables.next();
+                principals.add(next.getPrincipal());
+            }
+        } catch (RepositoryException e) {
+            // ignore and log
+            if (logger.isDebugEnabled()) {
+                logger.debug("Failed to find principals", e);
+            }
+        }
+        return new PrincipalIteratorAdapter(principals);
+    }
+
+    @Override
+    public @NotNull Principal getEveryone() {
+        return everyone;
+    }
+
+    @Override
+    public @NotNull PrincipalIterator getGroupMembership(@NotNull Principal principal) {
+        Set<Principal> groups = new HashSet<>();
+        try {
+            @Nullable Authorizable authorizable = mockUserManager.getAuthorizable(principal);
+            if (authorizable  != null) {
+                @NotNull Iterator<Group> memberOf = authorizable.memberOf();
+                while (memberOf.hasNext()) {
+                    groups.add(memberOf.next().getPrincipal());
+                }
+            }
+        } catch (RepositoryException e) {
+            // ignore and log
+            if (logger.isDebugEnabled()) {
+                logger.debug("Failed to get group membership", e);
+            }
+        }
+        return new PrincipalIteratorAdapter(groups);
+    }
+
+    @Override
+    public @Nullable Principal getPrincipal(@NotNull String principalName) {
+        Principal value = null;
+        try {
+            @Nullable Authorizable authorizable = mockUserManager.getAuthorizable(principalName);
+            if (authorizable != null) {
+                value = authorizable.getPrincipal();
+            }
+        } catch (RepositoryException e) {
+            // ignore and log
+            if (logger.isDebugEnabled()) {
+                logger.debug("Failed to get principal", e);
+            }
+        }
+        return value;
+    }
+
+    @Override
+    public @NotNull PrincipalIterator getPrincipals(int searchType) {
+        Set<Principal> principals = new HashSet<>();
+        try {
+            @NotNull Set<Authorizable> authorizables = mockUserManager.all(searchType);
+            for (Authorizable authorizable : authorizables) {
+                principals.add(authorizable.getPrincipal());
+            }
+        } catch (RepositoryException e) {
+            // ignore and log
+            if (logger.isDebugEnabled()) {
+                logger.debug("Failed to get principals", e);
+            }
+        }
+        return new PrincipalIteratorAdapter(principals);
+    }
+
+    static class PrincipalIteratorAdapter extends RangeIteratorAdapter implements PrincipalIterator {
+
+        public PrincipalIteratorAdapter(Collection<Principal> collection) {
+            super(collection);
+        }
+
+        @Override
+        public @NotNull Principal nextPrincipal() {
+            return (Principal)next();
+        }
+
+    }
+
+    @Override
+    public boolean hasPrincipal(@NotNull String principalName) {
+        boolean value = false;
+        try {
+            value = mockUserManager.getAuthorizable(principalName) != null;
+        } catch (RepositoryException e) {
+            // ignore and log
+            if (logger.isDebugEnabled()) {
+                logger.debug("Failed to determine principal exists", e);
+            }
+        }
+        return value;
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockSession.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockSession.java
index d944a3e..05a110c 100644
--- a/src/main/java/org/apache/sling/testing/mock/jcr/MockSession.java
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockSession.java
@@ -47,15 +47,19 @@ import javax.jcr.retention.RetentionManager;
 import javax.jcr.security.AccessControlManager;
 
 import org.apache.commons.lang3.StringUtils;
+import org.apache.jackrabbit.api.JackrabbitSession;
+import org.apache.jackrabbit.api.security.principal.PrincipalManager;
+import org.apache.jackrabbit.api.security.user.UserManager;
 import org.apache.jackrabbit.commons.iterator.RangeIteratorAdapter;
 import org.apache.jackrabbit.value.ValueFactoryImpl;
+import org.jetbrains.annotations.NotNull;
 import org.xml.sax.ContentHandler;
 
 /**
  * Mock {@link Session} implementation. This instance holds the JCR data in a
  * simple ordered map.
  */
-class MockSession implements Session {
+class MockSession implements Session, JackrabbitSession {
 
     private final MockRepository repository;
     private final Workspace workspace;
@@ -63,12 +67,16 @@ class MockSession implements Session {
     private final String userId;
     private boolean isLive;
     private boolean hasKnownChanges;
+    private MockPrincipalManager principalManager;
+    private MockUserManager userManager;
     private AccessControlManager accessControlManager = null;
 
     public MockSession(MockRepository repository, Map<String, ItemData> items,
             String userId, String workspaceName) throws RepositoryException {
         this.repository = repository;
         this.workspace = new MockWorkspace(repository, this, workspaceName);
+        this.userManager = new MockUserManager();
+        this.principalManager = new MockPrincipalManager(this.userManager);
         this.items = items;
         this.userId = userId;
         isLive = true;
@@ -502,6 +510,53 @@ class MockSession implements Session {
         throw new UnsupportedOperationException();
     }
 
+
+    // --- jackrabbit session operations ---
+
+    @Override
+    public Item getItemOrNull(final String absPath) throws RepositoryException {
+        Item item = null;
+        if (itemExists(absPath)) {
+            item = getItem(absPath);
+        }
+        return item;
+    }
+
+    @Override
+    public Node getNodeOrNull(final String absPath) throws RepositoryException {
+        Node node = null;
+        if (nodeExists(absPath)) {
+            node = getNode(absPath);
+        }
+        return node;
+    }
+
+    @Override
+    public Property getPropertyOrNull(final String absPath) throws RepositoryException {
+        Property prop = null;
+        if (propertyExists(absPath)) {
+            prop = getProperty(absPath);
+        }
+        return prop;
+    }
+
+    @Override
+    public PrincipalManager getPrincipalManager()
+            throws RepositoryException {
+        return principalManager;
+    }
+
+    @Override
+    public UserManager getUserManager()
+            throws RepositoryException {
+        return userManager;
+    }
+
+    @Override
+    public boolean hasPermission(@NotNull String absPath, @NotNull String... actions) throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
     /**
      * To allow a test to provide mock implementation of the AccessControlManager
      * @param acm the access control manager to use for the session
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockUser.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockUser.java
new file mode 100644
index 0000000..a4ae6b2
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockUser.java
@@ -0,0 +1,125 @@
+/*
+ * 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.testing.mock.jcr;
+
+import java.security.Principal;
+import java.util.Arrays;
+
+import javax.jcr.Credentials;
+import javax.jcr.RepositoryException;
+import javax.jcr.SimpleCredentials;
+
+import org.apache.jackrabbit.api.security.user.Impersonation;
+import org.apache.jackrabbit.api.security.user.User;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Mock {@link User} implementation.
+ */
+class MockUser extends MockAuthorizable implements User {
+    private Logger logger = LoggerFactory.getLogger(getClass());
+    private char[] pwd = {};
+    private boolean disabled;
+    private String disabledReason;
+
+    public MockUser(@Nullable String id, @Nullable Principal principal,
+            @Nullable String intermediatePath,
+            @NotNull MockUserManager mockUserMgr) {
+        super(id, principal, intermediatePath, mockUserMgr);
+    }
+
+    @Override
+    public boolean isAdmin() {
+        boolean result = false;
+        try {
+            result = "admin".equals(getID());
+        } catch (RepositoryException e) {
+            // ignore and log
+            if (logger.isDebugEnabled()) {
+                logger.debug("Failed to determine if this is admin", e);
+            }
+        }
+        return result;
+    }
+
+    @Override
+    public boolean isSystemUser() {
+        return false;
+    }
+
+    @Override
+    public @NotNull Credentials getCredentials() throws RepositoryException {
+        return new SimpleCredentials(id, pwd);
+    }
+
+    @Override
+    public @NotNull Impersonation getImpersonation() throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void changePassword(@Nullable String password) throws RepositoryException {
+        if (password == null) {
+            throw new RepositoryException("Attempt to set 'null' password for user " + getID());
+        }
+
+        pwd = password.toCharArray();
+    }
+
+    @Override
+    public void changePassword(@Nullable String password, @NotNull String oldPassword) throws RepositoryException {
+        if (Arrays.equals(pwd, oldPassword.toCharArray())) {
+            changePassword(password);
+        } else {
+            throw new RepositoryException("old password did not match");
+        }
+    }
+
+    @Override
+    public void disable(@Nullable String reason) throws RepositoryException {
+        if (reason == null) {
+            this.disabled = false;
+            this.disabledReason = null;
+        } else {
+            this.disabled = true;
+            this.disabledReason = reason;
+        }
+    }
+
+    @Override
+    public boolean isDisabled() throws RepositoryException {
+        return disabled;
+    }
+
+    @Override
+    public @Nullable String getDisabledReason() throws RepositoryException {
+        return disabledReason;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder builder = new StringBuilder();
+        builder.append("MockUser [id=");
+        builder.append(id);
+        builder.append("]");
+        return builder.toString();
+    }
+
+}
diff --git a/src/main/java/org/apache/sling/testing/mock/jcr/MockUserManager.java b/src/main/java/org/apache/sling/testing/mock/jcr/MockUserManager.java
new file mode 100644
index 0000000..ed1b0e5
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/mock/jcr/MockUserManager.java
@@ -0,0 +1,235 @@
+/*
+ * 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.testing.mock.jcr;
+
+import java.security.Principal;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+
+import org.apache.jackrabbit.api.security.principal.PrincipalManager;
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.AuthorizableExistsException;
+import org.apache.jackrabbit.api.security.user.AuthorizableTypeException;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.Query;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Mock {@link UserManager} implementation.
+ */
+public class MockUserManager implements UserManager {
+    private Logger logger = LoggerFactory.getLogger(getClass());
+    protected Map<String, Authorizable> authorizables = new HashMap<>();
+    private boolean autoSave;
+
+    /**
+     * Callback function for {@link MockUser#remove()} and {@link MockGroup#remove()} to
+     * invoke.
+     * 
+     * @param id the user or group id
+     * @param a the user or group object
+     * @return true if removed, false otherwise
+     */
+    boolean removeAuthorizable(Authorizable a) throws RepositoryException {
+        return authorizables.remove(a.getID(), a);
+    }
+
+    Set<Authorizable> all(int searchType) throws RepositoryException { // NOSONAR
+       return authorizables.values().stream()
+               .filter(a -> {
+                   boolean match;
+                   if (PrincipalManager.SEARCH_TYPE_ALL == searchType) {
+                       match = true;
+                   } else if (PrincipalManager.SEARCH_TYPE_GROUP == searchType) {
+                       match = a.isGroup();
+                   } else if (PrincipalManager.SEARCH_TYPE_NOT_GROUP == searchType) {
+                       match = !a.isGroup();
+                   } else {
+                       match = false;
+                   }
+                   return match;
+               })
+               .collect(Collectors.toSet());
+    }
+
+    @Override
+    public boolean isAutoSave() {
+        return autoSave;
+    }
+
+    @Override
+    public void autoSave(boolean autoSave) throws RepositoryException {
+        this.autoSave = autoSave;
+    }
+
+    @Override
+    public @NotNull Group createGroup(@NotNull String groupID) throws RepositoryException {
+        return maybeCreateGroup(groupID, null, null);
+    }
+
+    @Override
+    public @NotNull Group createGroup(@NotNull Principal principal) throws RepositoryException {
+        return maybeCreateGroup(null, principal, null);
+    }
+
+    @Override
+    public @NotNull Group createGroup(@NotNull Principal principal, @Nullable String intermediatePath)
+            throws RepositoryException {
+        return maybeCreateGroup(null, principal, intermediatePath);
+    }
+
+    @Override
+    public @NotNull Group createGroup(@NotNull String groupID, @NotNull Principal principal, @Nullable String intermediatePath)
+            throws RepositoryException {
+        return maybeCreateGroup(groupID, principal, intermediatePath);
+    }
+
+    private @NotNull Group maybeCreateGroup(@Nullable String groupID, @Nullable Principal principal, @Nullable String intermediatePath)
+            throws RepositoryException {
+        if (authorizables.containsKey(groupID)) {
+            throw new AuthorizableExistsException("Group already exists");
+        }
+        return (Group)authorizables.computeIfAbsent(groupID, id -> new MockGroup(id, principal, intermediatePath, this));
+    }
+
+    @Override
+    public @NotNull User createSystemUser(@NotNull String userID, @Nullable String intermediatePath)
+            throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public @NotNull User createUser(@NotNull String userID, @Nullable String password)
+            throws RepositoryException {
+        return maybeCreateUser(userID, password, null, null);
+    }
+
+    @Override
+    public @NotNull User createUser(@NotNull String userID, @Nullable String password, @NotNull Principal principal,
+            @Nullable String intermediatePath) throws RepositoryException {
+        return maybeCreateUser(userID, password, principal, intermediatePath);
+    }
+
+    private @NotNull User maybeCreateUser(@Nullable String userID, @Nullable String password, @Nullable Principal principal,
+            @Nullable String intermediatePath) throws RepositoryException {
+        if (authorizables.containsKey(userID)) {
+            throw new AuthorizableExistsException("User already exists");
+        }
+        return (User)authorizables.computeIfAbsent(userID, id -> new MockUser(id, principal, intermediatePath, this));
+    }
+
+    @Override
+    public @NotNull Iterator<Authorizable> findAuthorizables(@NotNull Query query) throws RepositoryException {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public @NotNull Iterator<Authorizable> findAuthorizables(@NotNull String relPath, @Nullable String value)
+            throws RepositoryException {
+        return findAuthorizables(relPath, value, UserManager.SEARCH_TYPE_AUTHORIZABLE);
+    }
+
+    @Override
+    public @NotNull Iterator<Authorizable> findAuthorizables(@NotNull String relPath, @Nullable String value, int searchType)
+            throws RepositoryException {
+        Set<Authorizable> matches = new HashSet<>();
+        for (Authorizable authorizable : authorizables.values()) {
+            if (UserManager.SEARCH_TYPE_GROUP == searchType) {
+                if (!authorizable.isGroup()) {
+                    continue; //not a group, so skip it
+                }
+            } else if (UserManager.SEARCH_TYPE_USER == searchType) {
+                if (authorizable.isGroup()) {
+                    continue; //not a user, so skip it
+                }
+            } else if (UserManager.SEARCH_TYPE_AUTHORIZABLE != searchType) {
+                continue; // some other invalid value?
+            }
+            Value[] property = authorizable.getProperty(relPath);
+            if (property != null) {
+                if (value == null) {
+                    // found a match?
+                   matches.add(authorizable);
+                } else {
+                    for (Value value2 : property) {
+                        if (value.equals(value2.getString())) {
+                            // found a match?
+                            matches.add(authorizable);
+                            break; // no need to check other values
+                        }
+                    }
+                }
+            }
+        }
+        return matches.iterator();
+    }
+
+    @Override
+    public @Nullable Authorizable getAuthorizable(@NotNull String id) throws RepositoryException {
+        return authorizables.get(id);
+    }
+
+    @Override
+    public @Nullable Authorizable getAuthorizable(@NotNull Principal principal) throws RepositoryException {
+        return authorizables.get(principal.getName());
+    }
+
+    @Override
+    public <T extends Authorizable> @Nullable T getAuthorizable(@NotNull String id, @NotNull Class<T> authorizableClass)
+            throws RepositoryException {
+        T a = null;
+        Authorizable authorizable = authorizables.get(id);
+        if (authorizableClass.isInstance(authorizable)) {
+            a = authorizableClass.cast(authorizable);
+        } else {
+            throw new AuthorizableTypeException("Not the expected authorizable class");
+        }
+        return a;
+    }
+
+    @Override
+    public @Nullable Authorizable getAuthorizableByPath(@NotNull String path)
+            throws RepositoryException {
+        return authorizables.values().stream()
+            .filter(a -> {
+                try {
+                    return path.equals(a.getPath());
+                } catch (RepositoryException e) {
+                    // ignore and log
+                    if (logger.isDebugEnabled()) {
+                        logger.debug("Failed to match authorizable path", e);
+                    }
+                    return false;
+                }
+            })
+            .findFirst()
+            .orElse(null);
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/LogCapture.java b/src/test/java/org/apache/sling/testing/mock/jcr/LogCapture.java
new file mode 100644
index 0000000..be27de1
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/LogCapture.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.testing.mock.jcr;
+
+import static org.junit.Assert.fail;
+
+import java.util.function.Predicate;
+import java.util.stream.Stream;
+
+import org.slf4j.LoggerFactory;
+
+import ch.qos.logback.classic.Level;
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.LoggerContext;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+
+/** 
+ * Capture logs for testing 
+ * 
+ * Initially cloned from: https://github.com/apache/sling-org-apache-sling-graphql-core/blob/0b1c1dd72ed04324ea84d2227c3223ec65b0b21e/src/test/java/org/apache/sling/graphql/core/util/LogCapture.java
+ */
+public class LogCapture extends ListAppender<ILoggingEvent> implements AutoCloseable {
+    private final boolean verboseFailure;
+    private Logger logger;
+
+    public LogCapture(String loggerName, boolean verboseFailure) {
+        this.verboseFailure = verboseFailure;
+        logger = (Logger) LoggerFactory.getLogger(loggerName);
+        logger.setLevel(Level.ALL);
+        setContext((LoggerContext) LoggerFactory.getILoggerFactory());
+        logger.addAppender(this);
+        start();
+    }
+
+    public void setLoggerLevel(Level newLevel) {
+        ((ch.qos.logback.classic.Logger)logger).setLevel(newLevel);
+    }
+
+    @Override
+    public void close() throws Exception {
+        if (logger != null) {
+            logger.detachAppender(this);
+        }
+    }
+
+    public boolean anyMatch(Predicate<ILoggingEvent> p) {
+        return this.list.stream().anyMatch(p);
+    }
+
+    public void assertContains(Level atLevel, String ... substrings) {
+        Stream.of(substrings).forEach(substring -> {
+            if (!anyMatch(event -> event.getLevel() == atLevel && event.getFormattedMessage().contains(substring))) {
+                if (verboseFailure) {
+                    fail(String.format("No log message contains [%s] in log%n%s", substring, this.list.toString()));
+                } else {
+                    fail(String.format("No log message contains [%s]", substring));
+                }
+            }
+        });
+    }
+
+    public void assertNotContains(Level atLevel, String ... substrings) {
+        Stream.of(substrings).forEach(substring -> {
+            if (anyMatch(event -> event.getLevel() == atLevel && event.getFormattedMessage().contains(substring))) {
+                if (verboseFailure) {
+                    fail(String.format("Unexpected log message contains [%s] in log%n%s", substring, this.list.toString()));
+                } else {
+                    fail(String.format("Unexpacted log message contains [%s]", substring));
+                }
+            }
+        });
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockAuthorizableTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockAuthorizableTest.java
new file mode 100644
index 0000000..e0474ae
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockAuthorizableTest.java
@@ -0,0 +1,250 @@
+/*
+ * 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.testing.mock.jcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import java.security.Principal;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.Value;
+import javax.jcr.ValueFactory;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.value.ValueFactoryImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+
+/**
+ *
+ */
+public abstract class MockAuthorizableTest<T extends Authorizable> {
+    protected UserManager userManager;
+    protected T authorizable;
+    protected ValueFactory vf = ValueFactoryImpl.getInstance();
+
+    @Before
+    public void before() throws RepositoryException {
+        userManager = new MockUserManager();
+        authorizable = createAuthorizable();
+    }
+
+    protected abstract T createAuthorizable() throws RepositoryException;
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#getID()}.
+     */
+    public abstract void testGetID() throws RepositoryException;
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#isGroup()}.
+     */
+    public abstract void testIsGroup();
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#getPrincipal()}.
+     */
+    @Test
+    public void testGetPrincipal() throws RepositoryException {
+        @NotNull String id = authorizable.getID();
+        @NotNull Principal principal = authorizable.getPrincipal();
+        assertEquals(id, principal.getName());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#declaredMemberOf()}.
+     */
+    @Test
+    public void testDeclaredMemberOf() throws RepositoryException {
+        // initially not member of any group
+        @NotNull Iterator<Group> declaredMemberOf = authorizable.declaredMemberOf();
+        assertFalse(declaredMemberOf.hasNext());
+
+        // member of a group
+        @NotNull Group members1 = userManager.createGroup("members1");
+        members1.addMember(authorizable);
+        declaredMemberOf = authorizable.declaredMemberOf();
+        assertTrue(declaredMemberOf.hasNext());
+        assertEquals(members1, declaredMemberOf.next());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#memberOf()}.
+     */
+    @Test
+    public void testMemberOf() throws RepositoryException {
+        // initially not member of any group
+        @NotNull Iterator<Group> memberOf = authorizable.memberOf();
+        assertFalse(memberOf.hasNext());
+
+        // member of a group
+        @NotNull Group members1 = userManager.createGroup("members1");
+        @NotNull Group members2 = userManager.createGroup("members2");
+        members1.addMember(members2);
+        members2.addMember(authorizable);
+        memberOf = authorizable.memberOf();
+        assertTrue(memberOf.hasNext());
+        Set<Group> memberOfSet = new HashSet<>();
+        memberOf.forEachRemaining(memberOfSet::add);
+        assertTrue(memberOfSet.contains(members1));
+        assertTrue(memberOfSet.contains(members2));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#remove()}.
+     */
+    @Test
+    public void testRemove() throws RepositoryException {
+        @NotNull String id = authorizable.getID();
+        authorizable.remove();
+        assertNull(userManager.getAuthorizable(id));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#getPropertyNames()}.
+     */
+    @Test
+    public void testGetPropertyNames() throws RepositoryException {
+        // only rep:principalName property
+        @NotNull Iterator<String> propertyNames = authorizable.getPropertyNames();
+        assertTrue(propertyNames.hasNext());
+        Set<String> propertyNamesSet = new HashSet<>();
+        propertyNames.forEachRemaining(propertyNamesSet::add);
+        assertEquals(1, propertyNamesSet.size());
+        assertTrue(propertyNamesSet.contains(MockAuthorizable.REP_PRINCIPAL_NAME));
+
+        // set some props
+        authorizable.setProperty("prop1", vf.createValue("value1"));
+        authorizable.setProperty("relPath/prop2", vf.createValue("value2"));
+
+        propertyNames = authorizable.getPropertyNames();
+        assertTrue(propertyNames.hasNext());
+        propertyNamesSet.clear();
+        propertyNames.forEachRemaining(propertyNamesSet::add);
+        assertEquals(2, propertyNamesSet.size());
+        assertTrue(propertyNamesSet.contains("prop1"));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#getPropertyNames(java.lang.String)}.
+     */
+    @Test
+    public void testGetPropertyNamesString() throws RepositoryException {
+        // no properties
+        @NotNull Iterator<String> propertyNames = authorizable.getPropertyNames("relPath");
+        assertFalse(propertyNames.hasNext());
+
+        // set some props
+        authorizable.setProperty("prop1", vf.createValue("value1"));
+        authorizable.setProperty("relPath/prop2", vf.createValue("value2"));
+
+        propertyNames = authorizable.getPropertyNames("relPath");
+        assertTrue(propertyNames.hasNext());
+        Set<String> propertyNamesSet = new HashSet<>();
+        propertyNames.forEachRemaining(propertyNamesSet::add);
+        assertEquals(1, propertyNamesSet.size());
+        assertTrue(propertyNamesSet.contains("relPath/prop2"));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#hasProperty(java.lang.String)}.
+     */
+    @Test
+    public void testHasProperty() throws RepositoryException {
+        assertFalse(authorizable.hasProperty("prop1"));
+        authorizable.setProperty("prop1", ValueFactoryImpl.getInstance().createValue("value1"));
+        assertTrue(authorizable.hasProperty("prop1"));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#setProperty(java.lang.String, javax.jcr.Value)}.
+     */
+    @Test
+    public void testSetPropertyStringValue() throws RepositoryException {
+        authorizable.setProperty("relPath", vf.createValue("value1"));
+        Value[] property = authorizable.getProperty("relPath");
+        assertNotNull(property);
+        assertEquals(1, property.length);
+        assertEquals("value1", property[0].getString());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#setProperty(java.lang.String, javax.jcr.Value[])}.
+     */
+    @Test
+    public void testSetPropertyStringValueArray() throws RepositoryException {
+        authorizable.setProperty("relPath", new Value [] {
+                vf.createValue("value1"),
+                vf.createValue("value2")});
+        Value[] property = authorizable.getProperty("relPath");
+        assertNotNull(property);
+        assertEquals(2, property.length);
+        assertEquals("value1", property[0].getString());
+        assertEquals("value2", property[1].getString());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#getProperty(java.lang.String)}.
+     */
+    @Test
+    public void testGetProperty() throws RepositoryException {
+        // not set
+        @Nullable Value[] property = authorizable.getProperty("relPath");
+        assertNull(property);
+
+        authorizable.setProperty("relPath", vf.createValue("value1"));
+        property = authorizable.getProperty("relPath");
+        assertNotNull(property);
+        assertEquals(1, property.length);
+        assertEquals("value1", property[0].getString());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#removeProperty(java.lang.String)}.
+     */
+    @Test
+    public void testRemoveProperty() throws RepositoryException {
+        authorizable.setProperty("prop1", ValueFactoryImpl.getInstance().createValue("value1"));
+        assertTrue(authorizable.removeProperty("prop1"));
+        assertFalse(authorizable.hasProperty("prop1"));
+
+        assertFalse(authorizable.removeProperty("prop1"));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockAuthorizable#getPath()}.
+     */
+    public abstract void testGetPath() throws UnsupportedRepositoryOperationException, RepositoryException;
+
+    @Test
+    public void testToString() {
+        assertNotNull(authorizable.toString());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockGroupTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockGroupTest.java
new file mode 100644
index 0000000..28aaa12
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockGroupTest.java
@@ -0,0 +1,245 @@
+/*
+ * 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.testing.mock.jcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.AuthorizableExistsException;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+
+/**
+ *
+ */
+public class MockGroupTest extends MockAuthorizableTest<Group> {
+
+    @Override
+    protected Group createAuthorizable() throws RepositoryException {
+        return userManager.createGroup("group1");
+    }
+
+    @Test
+    @Override
+    public void testGetID() throws RepositoryException {
+        assertEquals("group1", authorizable.getID());
+    }
+
+    @Test
+    @Override
+    public void testIsGroup() {
+        assertTrue(authorizable.isGroup());
+    }
+
+    @Test
+    public void testGetMemberOfWithNestedGroups() throws RepositoryException {
+        @NotNull Group member1 = userManager.createGroup("member1");
+        @NotNull Group submember1 = userManager.createGroup("submember1");
+        member1.addMember(submember1);
+        submember1.addMember(member1); // to verify no circular loop
+        @NotNull User member2 = userManager.createUser("member2", "pwd");
+        submember1.addMember(member2);
+        authorizable.addMember(member1);
+
+        @NotNull Iterator<Group> groups = member2.memberOf();
+        Set<Authorizable> memberOfSet = new HashSet<>();
+        groups.forEachRemaining(memberOfSet::add);
+        assertEquals(3, memberOfSet.size());
+        assertTrue(memberOfSet.contains(member1));
+        assertTrue(memberOfSet.contains(submember1));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockGroup#getDeclaredMembers()}.
+     */
+    @Test
+    public void testGetDeclaredMembers() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group member1 = userManager.createGroup("member1");
+        @NotNull User member2 = userManager.createUser("member2", "pwd");
+        member1.addMember(member2);
+        authorizable.addMember(member1);
+
+        @NotNull Iterator<Authorizable> members = authorizable.getDeclaredMembers();
+        Set<Authorizable> membersSet = new HashSet<>();
+        members.forEachRemaining(membersSet::add);
+        assertEquals(1, membersSet.size());
+        assertTrue(membersSet.contains(member1));
+        assertFalse(membersSet.contains(member2));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockGroup#getMembers()}.
+     */
+    @Test
+    public void testGetMembers() throws RepositoryException {
+        @NotNull Group member1 = userManager.createGroup("member1");
+        @NotNull User member2 = userManager.createUser("member2", "pwd");
+        member1.addMember(member2);
+        authorizable.addMember(member1);
+
+        @NotNull Iterator<Authorizable> members = authorizable.getMembers();
+        Set<Authorizable> membersSet = new HashSet<>();
+        members.forEachRemaining(membersSet::add);
+        assertEquals(2, membersSet.size());
+        assertTrue(membersSet.contains(member1));
+        assertTrue(membersSet.contains(member2));
+    }
+    @Test
+    public void testGetMembersWithNestedGroups() throws RepositoryException {
+        @NotNull Group member1 = userManager.createGroup("member1");
+        @NotNull Group submember1 = userManager.createGroup("submember1");
+        member1.addMember(submember1);
+        submember1.addMember(member1); // to verify no circular loop
+        @NotNull User member2 = userManager.createUser("member2", "pwd");
+        submember1.addMember(member2);
+        authorizable.addMember(member1);
+
+        @NotNull Iterator<Authorizable> members = authorizable.getMembers();
+        Set<Authorizable> membersSet = new HashSet<>();
+        members.forEachRemaining(membersSet::add);
+        assertEquals(3, membersSet.size());
+        assertTrue(membersSet.contains(member1));
+        assertTrue(membersSet.contains(member2));
+        assertTrue(membersSet.contains(submember1));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockGroup#isDeclaredMember(org.apache.jackrabbit.api.security.user.Authorizable)}.
+     */
+    @Test
+    public void testIsDeclaredMember() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group member1 = userManager.createGroup("member1");
+        @NotNull User member2 = userManager.createUser("member2", "pwd");
+        member1.addMember(member2);
+
+        assertFalse(authorizable.isDeclaredMember(member1));
+        assertFalse(authorizable.isDeclaredMember(member2));
+
+        authorizable.addMember(member1);
+        assertTrue(authorizable.isDeclaredMember(member1));
+        assertFalse(authorizable.isDeclaredMember(member2));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockGroup#isMember(org.apache.jackrabbit.api.security.user.Authorizable)}.
+     */
+    @Test
+    public void testIsMember() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group member1 = userManager.createGroup("member1");
+        @NotNull User member2 = userManager.createUser("member2", "pwd");
+        @NotNull User member3 = userManager.createUser("member3", "pwd");
+        member1.addMember(member2);
+        authorizable.addMember(member3);
+
+        assertFalse(authorizable.isMember(member2));
+        assertTrue(authorizable.isMember(member3));
+
+        authorizable.addMember(member1);
+        assertTrue(authorizable.isMember(member2));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockGroup#addMember(org.apache.jackrabbit.api.security.user.Authorizable)}.
+     */
+    @Test
+    public void testAddMember() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group member1 = userManager.createGroup("member1");
+        @NotNull User member2 = userManager.createUser("member2", "pwd");
+        assertTrue(authorizable.addMember(member1));
+        assertTrue(authorizable.addMember(member2));
+        // one more time
+        assertFalse(authorizable.addMember(member2));
+
+        @NotNull Iterator<Authorizable> declaredMembers = authorizable.getDeclaredMembers();
+        Set<Authorizable> membersSet = new HashSet<>();
+        declaredMembers.forEachRemaining(membersSet::add);
+        assertEquals(2, membersSet.size());
+        assertTrue(membersSet.contains(member1));
+        assertTrue(membersSet.contains(member2));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockGroup#addMembers(java.lang.String[])}.
+     */
+    @Test
+    public void testAddMembers() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group member1 = userManager.createGroup("member1");
+        @NotNull User member2 = userManager.createUser("member2", "pwd");
+        @NotNull Set<String> added = authorizable.addMembers(member1.getID(), member2.getID(), member1.getID(), "invalid");
+        assertEquals(2, added.size());
+
+        @NotNull Iterator<Authorizable> declaredMembers = authorizable.getDeclaredMembers();
+        Set<Authorizable> membersSet = new HashSet<>();
+        declaredMembers.forEachRemaining(membersSet::add);
+        assertEquals(2, membersSet.size());
+        assertTrue(membersSet.contains(member1));
+        assertTrue(membersSet.contains(member2));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockGroup#removeMember(org.apache.jackrabbit.api.security.user.Authorizable)}.
+     */
+    @Test
+    public void testRemoveMember() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group member1 = userManager.createGroup("member1");
+        @NotNull User member2 = userManager.createUser("member2", "pwd");
+        authorizable.addMember(member1);
+        authorizable.addMember(member2);
+        assertTrue(authorizable.removeMember(member1));
+
+        @NotNull Iterator<Authorizable> declaredMembers = authorizable.getDeclaredMembers();
+        Set<Authorizable> membersSet = new HashSet<>();
+        declaredMembers.forEachRemaining(membersSet::add);
+        assertEquals(1, membersSet.size());
+        assertFalse(membersSet.contains(member1));
+        assertTrue(membersSet.contains(member2));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockGroup#removeMembers(java.lang.String[])}.
+     */
+    @Test
+    public void testRemoveMembers() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group member1 = userManager.createGroup("member1");
+        @NotNull User member2 = userManager.createUser("member2", "pwd");
+        authorizable.addMember(member1);
+        authorizable.addMember(member2);
+        @NotNull Set<String> removed = authorizable.removeMembers(member1.getID(), member2.getID(), "invalid");
+        assertEquals(2, removed.size());
+
+        @NotNull Iterator<Authorizable> declaredMembers = authorizable.getDeclaredMembers();
+        assertFalse(declaredMembers.hasNext());
+    }
+
+    @Test
+    @Override
+    public void testGetPath() throws UnsupportedRepositoryOperationException, RepositoryException {
+        assertEquals("/home/groups/group1", authorizable.getPath());
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockPrincipalManagerTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockPrincipalManagerTest.java
new file mode 100644
index 0000000..eaa1a39
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockPrincipalManagerTest.java
@@ -0,0 +1,362 @@
+/*
+ * 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.testing.mock.jcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+
+import java.security.Principal;
+import java.util.HashSet;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.jackrabbit.api.security.principal.PrincipalIterator;
+import org.apache.jackrabbit.api.security.principal.PrincipalManager;
+import org.apache.jackrabbit.api.security.user.AuthorizableExistsException;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import ch.qos.logback.classic.Level;
+
+/**
+ *
+ */
+public class MockPrincipalManagerTest {
+    protected UserManager userManager;
+    protected MockPrincipalManager principalManager;
+
+    @Before
+    public void before() throws RepositoryException {
+        userManager = new MockUserManager();
+        principalManager = new MockPrincipalManager((MockUserManager)userManager);
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockPrincipalManager#findPrincipals(java.lang.String)}.
+     */
+    @Test
+    public void testFindPrincipalsString() throws AuthorizableExistsException, RepositoryException {
+        @NotNull User user1 = userManager.createUser("user1", "pwd", () -> "user1", "/home/users/path1");
+        @NotNull Group group1 = userManager.createGroup("group1", () -> "group1", "/home/groups/path1");
+
+        @NotNull PrincipalIterator principals = principalManager.findPrincipals("other");
+        assertFalse(principals.hasNext());
+
+        principals = principalManager.findPrincipals("user1");
+        assertTrue(principals.hasNext());
+        assertEquals(user1.getPrincipal(), principals.nextPrincipal());
+
+        principals = principalManager.findPrincipals("group1");
+        assertTrue(principals.hasNext());
+        assertEquals(group1.getPrincipal(), principals.nextPrincipal());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockPrincipalManager#findPrincipals(java.lang.String, int)}.
+     */
+    @Test
+    public void testFindPrincipalsStringInt() throws AuthorizableExistsException, RepositoryException {
+        @NotNull User user1 = userManager.createUser("user1", "pwd", () -> "user1", "/home/users/path1");
+        @NotNull Group group1 = userManager.createGroup("group1", () -> "group1", "/home/groups/path1");
+
+        @NotNull PrincipalIterator principals = principalManager.findPrincipals("other", PrincipalManager.SEARCH_TYPE_ALL);
+        assertFalse(principals.hasNext());
+
+        principals = principalManager.findPrincipals("user1", PrincipalManager.SEARCH_TYPE_NOT_GROUP);
+        assertTrue(principals.hasNext());
+        assertEquals(user1.getPrincipal(), principals.nextPrincipal());
+
+        principals = principalManager.findPrincipals("group1", PrincipalManager.SEARCH_TYPE_GROUP);
+        assertTrue(principals.hasNext());
+        assertEquals(group1.getPrincipal(), principals.nextPrincipal());
+    }
+    @Test
+    public void testFindPrincipalsStringIntCatchRepositoryException() throws Exception {
+        MockUserManager mockUserManager = Mockito.spy((MockUserManager)userManager);
+        Mockito.doThrow(RepositoryException.class).when(mockUserManager).findAuthorizables(anyString(), anyString(), anyInt());
+        // replace the field with our mocked variant
+        principalManager.mockUserManager = mockUserManager;
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(principalManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.WARN);
+
+            // this should log an warning message
+            @NotNull PrincipalIterator principals = principalManager.findPrincipals("other", PrincipalManager.SEARCH_TYPE_ALL);
+            assertFalse(principals.hasNext());
+
+            // verify the msg was not logged
+            capture.assertNotContains(Level.DEBUG, "Failed to find principals");
+        }
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(principalManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.DEBUG);
+
+            // this should log an warning message
+            @NotNull PrincipalIterator principals = principalManager.findPrincipals("other", PrincipalManager.SEARCH_TYPE_ALL);
+            assertFalse(principals.hasNext());
+
+            // verify the msg was logged
+            capture.assertContains(Level.DEBUG, "Failed to find principals");
+        }
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockPrincipalManager#getEveryone()}.
+     */
+    @Test
+    public void testGetEveryone() {
+        @NotNull Principal everyone = principalManager.getEveryone();
+        assertNotNull(everyone);
+        assertEquals("everyone", everyone.getName());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockPrincipalManager#getGroupMembership(java.security.Principal)}.
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testGetGroupMembership() throws AuthorizableExistsException, RepositoryException {
+        @NotNull User user1 = userManager.createUser("user1", "pwd");
+        Principal principal = principalManager.getPrincipal("user1");
+        PrincipalIterator groupMembership = principalManager.getGroupMembership(principal);
+        assertFalse(groupMembership.hasNext());
+
+        @NotNull Group members1 = userManager.createGroup("members1");
+        @NotNull Group members2 = userManager.createGroup("members2");
+        members1.addMember(members2);
+        members2.addMember(user1);
+
+        groupMembership = principalManager.getGroupMembership(principal);
+        assertTrue(groupMembership.hasNext());
+        final Set<Principal> groupsSet = new HashSet<>();
+        groupMembership.forEachRemaining(p -> groupsSet.add((Principal) p));
+        assertEquals(2, groupsSet.size());
+        assertTrue(groupsSet.stream().anyMatch(p -> "members1".equals(p.getName())));
+        assertTrue(groupsSet.stream().anyMatch(p -> "members2".equals(p.getName())));
+
+        // non-existing principal
+        groupMembership = principalManager.getGroupMembership(() -> "other");
+        assertFalse(groupMembership.hasNext());
+    }
+    @Test
+    public void testGetGroupMembershipCatchRepositoryException() throws Exception {
+        MockUserManager mockUserManager = Mockito.spy((MockUserManager)userManager);
+        Mockito.doThrow(RepositoryException.class).when(mockUserManager).getAuthorizable(any(Principal.class));
+        // replace the field with our mocked variant
+        principalManager.mockUserManager = mockUserManager;
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(principalManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.WARN);
+
+            // this should log an warning message
+            @NotNull PrincipalIterator principals = principalManager.getGroupMembership(() -> "other");
+            assertFalse(principals.hasNext());
+
+            // verify the msg was not logged
+            capture.assertNotContains(Level.DEBUG, "Failed to get group membership");
+        }
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(principalManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.DEBUG);
+
+            // this should log an warning message
+            @NotNull PrincipalIterator principals = principalManager.getGroupMembership(() -> "other");
+            assertFalse(principals.hasNext());
+
+            // verify the msg was logged
+            capture.assertContains(Level.DEBUG, "Failed to get group membership");
+        }
+    }
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockPrincipalManager#getPrincipal(java.lang.String)}.
+     */
+    @Test
+    public void testGetPrincipal() throws AuthorizableExistsException, RepositoryException {
+        // no
+        Principal principal = principalManager.getPrincipal("user1");
+        assertNull(principal);
+
+        // yes!
+        userManager.createUser("user1", "pwd");
+        principal = principalManager.getPrincipal("user1");
+        assertNotNull(principal);
+        assertEquals("user1", principal.getName());
+    }
+    @Test
+    public void testGetPrincipalCatchRepositoryException() throws Exception {
+        MockUserManager mockUserManager = Mockito.spy((MockUserManager)userManager);
+        Mockito.doThrow(RepositoryException.class).when(mockUserManager).getAuthorizable(anyString());
+        // replace the field with our mocked variant
+        principalManager.mockUserManager = mockUserManager;
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(principalManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.WARN);
+
+            // this should log an warning message
+            assertNull(principalManager.getPrincipal("other"));
+
+            // verify the msg was not logged
+            capture.assertNotContains(Level.DEBUG, "Failed to get principal");
+        }
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(principalManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.DEBUG);
+
+            // this should log an warning message
+            assertNull(principalManager.getPrincipal("other"));
+
+            // verify the msg was logged
+            capture.assertContains(Level.DEBUG, "Failed to get principal");
+        }
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockPrincipalManager#getPrincipals(int)}.
+     */
+    @SuppressWarnings("unchecked")
+    @Test
+    public void testGetPrincipals() throws AuthorizableExistsException, RepositoryException {
+        // none
+        PrincipalIterator principals = principalManager.getPrincipals(PrincipalManager.SEARCH_TYPE_ALL);
+        assertFalse(principals.hasNext());
+
+        userManager.createUser("user1", "pwd");
+        userManager.createGroup("group1");
+
+        principals = principalManager.getPrincipals(PrincipalManager.SEARCH_TYPE_ALL);
+        assertTrue(principals.hasNext());
+        final Set<Principal> principalsSet = new HashSet<>();
+        principals.forEachRemaining(p -> principalsSet.add((Principal) p));
+        assertEquals(2, principalsSet.size());
+
+        principals = principalManager.getPrincipals(PrincipalManager.SEARCH_TYPE_GROUP);
+        assertTrue(principals.hasNext());
+        principalsSet.clear();
+        principals.forEachRemaining(p -> principalsSet.add((Principal) p));
+        assertEquals(1, principalsSet.size());
+        assertEquals("group1", principalsSet.iterator().next().getName());
+
+        principals = principalManager.getPrincipals(PrincipalManager.SEARCH_TYPE_NOT_GROUP);
+        assertTrue(principals.hasNext());
+        principalsSet.clear();
+        principals.forEachRemaining(p -> principalsSet.add((Principal) p));
+        assertEquals(1, principalsSet.size());
+        assertEquals("user1", principalsSet.iterator().next().getName());
+    }
+    @Test
+    public void testGetPrincipalsCatchRepositoryException() throws Exception {
+        MockUserManager mockUserManager = Mockito.spy((MockUserManager)userManager);
+        Mockito.doThrow(RepositoryException.class).when(mockUserManager).all(anyInt());
+        // replace the field with our mocked variant
+        principalManager.mockUserManager = mockUserManager;
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(principalManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.WARN);
+
+            // this should log an warning message
+            PrincipalIterator principals = principalManager.getPrincipals(PrincipalManager.SEARCH_TYPE_ALL);
+            assertFalse(principals.hasNext());
+
+            // verify the msg was not logged
+            capture.assertNotContains(Level.DEBUG, "Failed to get principals");
+        }
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(principalManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.DEBUG);
+
+            // this should log an warning message
+            PrincipalIterator principals = principalManager.getPrincipals(PrincipalManager.SEARCH_TYPE_ALL);
+            assertFalse(principals.hasNext());
+
+            // verify the msg was logged
+            capture.assertContains(Level.DEBUG, "Failed to get principals");
+        }
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockPrincipalManager#hasPrincipal(java.lang.String)}.
+     */
+    @Test
+    public void testHasPrincipal() throws AuthorizableExistsException, RepositoryException {
+        // no
+        assertFalse(principalManager.hasPrincipal("user1"));
+
+        // yes!
+        userManager.createUser("user1", "pwd");
+        assertTrue(principalManager.hasPrincipal("user1"));
+    }
+    @Test
+    public void testHasPrincipalCatchRepositoryException() throws Exception {
+        MockUserManager mockUserManager = Mockito.spy((MockUserManager)userManager);
+        Mockito.doThrow(RepositoryException.class).when(mockUserManager).getAuthorizable(anyString());
+        // replace the field with our mocked variant
+        principalManager.mockUserManager = mockUserManager;
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(principalManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.WARN);
+
+            // this should log an warning message
+            assertFalse(principalManager.hasPrincipal("user1"));
+
+            // verify the msg was not logged
+            capture.assertNotContains(Level.DEBUG, "Failed to determine principal exists");
+        }
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(principalManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.DEBUG);
+
+            // this should log an warning message
+            assertFalse(principalManager.hasPrincipal("user1"));
+
+            // verify the msg was logged
+            capture.assertContains(Level.DEBUG, "Failed to determine principal exists");
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockSessionTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockSessionTest.java
index 0cea4a1..ca8eddc 100644
--- a/src/test/java/org/apache/sling/testing/mock/jcr/MockSessionTest.java
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockSessionTest.java
@@ -22,9 +22,11 @@ import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertFalse;
 import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertThrows;
 import static org.junit.Assert.assertTrue;
 
+import javax.jcr.AccessDeniedException;
 import javax.jcr.ItemExistsException;
 import javax.jcr.ItemNotFoundException;
 import javax.jcr.NamespaceRegistry;
@@ -34,9 +36,11 @@ import javax.jcr.PathNotFoundException;
 import javax.jcr.Property;
 import javax.jcr.RepositoryException;
 import javax.jcr.Session;
+import javax.jcr.UnsupportedRepositoryOperationException;
 import javax.jcr.security.AccessControlManager;
 
 import org.apache.jackrabbit.JcrConstants;
+import org.apache.jackrabbit.api.JackrabbitSession;
 import org.junit.Test;
 import org.mockito.Mockito;
 
@@ -411,6 +415,69 @@ public class MockSessionTest {
         assertTrue(session.propertyExists("/node1/child2/grandchild1/prop1"));
     }
 
+
+    // --- jackrabbit session operations ---
+
+    @Test
+    public void testGetItemOrNull() throws RepositoryException {
+        JackrabbitSession s = (JackrabbitSession)MockJcr.newSession();
+        Node node1 = s.getRootNode().addNode("node1");
+        Node child1 = node1.addNode("child1");
+        String childPath = child1.getPath();
+        Property prop1 = child1.setProperty("prop1", "value1");
+        String propPath = prop1.getPath();
+
+        assertNotNull(s.getItemOrNull(childPath));
+        assertNotNull(s.getItemOrNull(propPath));
+        prop1.remove();
+        assertNull(s.getItemOrNull(propPath));
+        child1.remove();
+        assertNull(s.getItemOrNull(childPath));
+    }
+
+    @Test
+    public void testGetNodeOrNull() throws RepositoryException {
+        JackrabbitSession s = (JackrabbitSession)MockJcr.newSession();
+        Node node1 = s.getRootNode().addNode("node1");
+        Node child1 = node1.addNode("child1");
+        String path = child1.getPath();
+        assertNotNull(s.getNodeOrNull(path));
+        child1.remove();
+        assertNull(s.getNodeOrNull(path));
+    }
+
+    @Test
+    public void testGetPropertyOrNull() throws RepositoryException {
+        JackrabbitSession s = (JackrabbitSession)MockJcr.newSession();
+        Node node1 = s.getRootNode().addNode("node1");
+        Node child1 = node1.addNode("child1");
+        Property prop1 = child1.setProperty("prop1", "value1");
+        String path = prop1.getPath();
+        assertNotNull(s.getPropertyOrNull(path));
+        prop1.remove();
+        assertNull(s.getPropertyOrNull(path));
+    }
+
+    @Test
+    public void testGetPrincipalManager()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        JackrabbitSession s = (JackrabbitSession)MockJcr.newSession();
+        assertNotNull(s.getPrincipalManager());
+    }
+
+    @Test
+    public void testGetUserManager()
+            throws AccessDeniedException, UnsupportedRepositoryOperationException, RepositoryException {
+        JackrabbitSession s = (JackrabbitSession)MockJcr.newSession();
+        assertNotNull(s.getUserManager());
+    }
+
+    @Test
+    public void testHasPermission() throws RepositoryException {
+        JackrabbitSession s = (JackrabbitSession)MockJcr.newSession();
+        assertThrows(UnsupportedOperationException.class, () -> s.hasPermission("/path1", JackrabbitSession.ACTION_ADD_PROPERTY, JackrabbitSession.ACTION_MODIFY_PROPERTY));
+    }
+
     @Test
     public void testSetAccessControlManager() throws RepositoryException {
         Session s = MockJcr.newSession();
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockUserManagerTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockUserManagerTest.java
new file mode 100644
index 0000000..057791b
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockUserManagerTest.java
@@ -0,0 +1,344 @@
+/*
+ * 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.testing.mock.jcr;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import java.util.Iterator;
+import java.util.Set;
+
+import javax.jcr.RepositoryException;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.ValueFactory;
+
+import org.apache.jackrabbit.api.security.user.Authorizable;
+import org.apache.jackrabbit.api.security.user.AuthorizableExistsException;
+import org.apache.jackrabbit.api.security.user.AuthorizableTypeException;
+import org.apache.jackrabbit.api.security.user.Group;
+import org.apache.jackrabbit.api.security.user.Query;
+import org.apache.jackrabbit.api.security.user.User;
+import org.apache.jackrabbit.api.security.user.UserManager;
+import org.apache.jackrabbit.value.ValueFactoryImpl;
+import org.jetbrains.annotations.NotNull;
+import org.jetbrains.annotations.Nullable;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import ch.qos.logback.classic.Level;
+
+/**
+ *
+ */
+public class MockUserManagerTest {
+    protected MockUserManager userManager;
+    protected ValueFactory vf = ValueFactoryImpl.getInstance();
+
+    @Before
+    public void before() throws RepositoryException {
+        userManager = new MockUserManager();
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#removeAuthorizable(org.apache.jackrabbit.api.security.user.Authorizable)}.
+     */
+    @Test
+    public void testRemoveAuthorizable() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group group1 = userManager.createGroup("group1");
+        @NotNull User user1 = userManager.createUser("user1", "pwd");
+        assertTrue(userManager.removeAuthorizable(user1));
+        assertTrue(userManager.removeAuthorizable(group1));
+
+        assertFalse(userManager.removeAuthorizable(user1));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#all(int)}.
+     */
+    @Test
+    public void testAll() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group group1 = userManager.createGroup("group1");
+        @NotNull User user1 = userManager.createUser("user1", "pwd");
+        Set<Authorizable> allAuthorizables = userManager.all(UserManager.SEARCH_TYPE_AUTHORIZABLE);
+        assertEquals(2, allAuthorizables.size());
+        assertTrue(allAuthorizables.contains(user1));
+        assertTrue(allAuthorizables.contains(group1));
+
+        Set<Authorizable> allUsers = userManager.all(UserManager.SEARCH_TYPE_USER);
+        assertEquals(1, allUsers.size());
+        assertTrue(allUsers.contains(user1));
+
+        Set<Authorizable> allGroups = userManager.all(UserManager.SEARCH_TYPE_GROUP);
+        assertEquals(1, allGroups.size());
+        assertTrue(allGroups.contains(group1));
+
+        Set<Authorizable> invalidType = userManager.all(-1);
+        assertTrue(invalidType.isEmpty());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#isAutoSave()}.
+     */
+    @Test
+    public void testIsAutoSave() {
+        assertFalse(userManager.isAutoSave());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#autoSave(boolean)}.
+     */
+    @Test
+    public void testAutoSave() throws UnsupportedRepositoryOperationException, RepositoryException {
+        userManager.autoSave(true);
+        assertTrue(userManager.isAutoSave());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#createGroup(java.lang.String)}.
+     */
+    @Test
+    public void testCreateGroupString() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group group1 = userManager.createGroup("group1");
+        assertNotNull(group1);
+
+        assertThrows(AuthorizableExistsException.class, () -> userManager.createGroup("group1"));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#createGroup(java.security.Principal)}.
+     */
+    @Test
+    public void testCreateGroupPrincipal() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group group1 = userManager.createGroup(() -> "group1");
+        assertNotNull(group1);
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#createGroup(java.security.Principal, java.lang.String)}.
+     */
+    @Test
+    public void testCreateGroupPrincipalString() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group group1 = userManager.createGroup(() -> "group1", "/home/groups/path1");
+        assertNotNull(group1);
+        assertEquals("/home/groups/path1/group1", group1.getPath());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#createGroup(java.lang.String, java.security.Principal, java.lang.String)}.
+     */
+    @Test
+    public void testCreateGroupStringPrincipalString() throws AuthorizableExistsException, RepositoryException {
+        @NotNull Group group1 = userManager.createGroup("group1", () -> "group1", "/home/groups/path1");
+        assertNotNull(group1);
+        assertEquals("/home/groups/path1/group1", group1.getPath());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#createSystemUser(java.lang.String, java.lang.String)}.
+     */
+    @Test
+    public void testCreateSystemUser() {
+        assertThrows(UnsupportedOperationException.class, () -> userManager.createSystemUser("systemuser1", "/home/users/system"));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#createUser(java.lang.String, java.lang.String)}.
+     */
+    @Test
+    public void testCreateUserStringString() throws AuthorizableExistsException, RepositoryException {
+        @NotNull User user1 = userManager.createUser("user1", "pwd");
+        assertNotNull(user1);
+
+        assertThrows(AuthorizableExistsException.class, () -> userManager.createUser("user1", "pwd"));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#createUser(java.lang.String, java.lang.String, java.security.Principal, java.lang.String)}.
+     */
+    @Test
+    public void testCreateUserStringStringPrincipalString() throws AuthorizableExistsException, RepositoryException {
+        @NotNull User user1 = userManager.createUser("user1", "pwd", () -> "user1", "/home/users/path1");
+        assertNotNull(user1);
+        assertEquals("/home/users/path1/user1", user1.getPath());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#findAuthorizables(org.apache.jackrabbit.api.security.user.Query)}.
+     */
+    @Test
+    public void testFindAuthorizablesQuery() throws RepositoryException {
+        Query query = Mockito.mock(Query.class);
+        assertThrows(UnsupportedOperationException.class, () -> userManager.findAuthorizables(query));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#findAuthorizables(java.lang.String, java.lang.String)}.
+     */
+    @Test
+    public void testFindAuthorizablesStringString() throws RepositoryException {
+        @NotNull User user1 = userManager.createUser("user1", "pwd", () -> "user1", "/home/users/path1");
+        user1.setProperty("prop1", vf.createValue("prop1Value"));
+        user1.setProperty("sub1/prop2", vf.createValue("prop2Value"));
+
+        // none found
+        @NotNull Iterator<Authorizable> authorizables = userManager.findAuthorizables("prop1", "other");
+        assertFalse(authorizables.hasNext());
+
+        // found by name
+        authorizables = userManager.findAuthorizables("prop1", "prop1Value");
+        assertTrue(authorizables.hasNext());
+        assertEquals(user1, authorizables.next());
+
+        // found by relPath
+        authorizables = userManager.findAuthorizables("sub1/prop2", "prop2Value");
+        assertTrue(authorizables.hasNext());
+        assertEquals(user1, authorizables.next());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#findAuthorizables(java.lang.String, java.lang.String, int)}.
+     */
+    @Test
+    public void testFindAuthorizablesStringStringInt() throws RepositoryException {
+        @NotNull Group group1 = userManager.createGroup("group1", () -> "group1", "/home/groups/path1");
+        group1.setProperty("prop1", vf.createValue("prop1Value"));
+        group1.setProperty("sub1/prop2", vf.createValue("prop2Value"));
+
+        @NotNull User user1 = userManager.createUser("user1", "pwd", () -> "user1", "/home/users/path1");
+        user1.setProperty("prop1", vf.createValue("prop1Value"));
+        user1.setProperty("sub1/prop2", vf.createValue("prop2Value"));
+
+        // none found
+        @NotNull Iterator<Authorizable> authorizables = userManager.findAuthorizables("prop1", "other", UserManager.SEARCH_TYPE_AUTHORIZABLE);
+        assertFalse(authorizables.hasNext());
+
+        // found by name
+        authorizables = userManager.findAuthorizables("prop1", "prop1Value", UserManager.SEARCH_TYPE_USER);
+        assertTrue(authorizables.hasNext());
+        assertEquals(user1, authorizables.next());
+
+        // found by name
+        authorizables = userManager.findAuthorizables("prop1", "prop1Value", UserManager.SEARCH_TYPE_GROUP);
+        assertTrue(authorizables.hasNext());
+        assertEquals(group1, authorizables.next());
+
+        // found by relPath
+        authorizables = userManager.findAuthorizables("sub1/prop2", "prop2Value", UserManager.SEARCH_TYPE_USER);
+        assertTrue(authorizables.hasNext());
+        assertEquals(user1, authorizables.next());
+
+        // found by relPath
+        authorizables = userManager.findAuthorizables("sub1/prop2", "prop2Value", UserManager.SEARCH_TYPE_GROUP);
+        assertTrue(authorizables.hasNext());
+        assertEquals(group1, authorizables.next());
+
+        // found by relPath but specific value is not necessary
+        authorizables = userManager.findAuthorizables("sub1/prop2", null, UserManager.SEARCH_TYPE_GROUP);
+        assertTrue(authorizables.hasNext());
+        assertEquals(group1, authorizables.next());
+
+        // not found by relPath
+        authorizables = userManager.findAuthorizables("sub1/prop3","prop3Value", UserManager.SEARCH_TYPE_GROUP);
+        assertFalse(authorizables.hasNext());
+
+        // some invalid type
+        authorizables = userManager.findAuthorizables("sub1/prop2", "prop2Value", -1);
+        assertFalse(authorizables.hasNext());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#getAuthorizable(java.lang.String)}.
+     */
+    @Test
+    public void testGetAuthorizableString() throws AuthorizableExistsException, RepositoryException {
+        @NotNull User user1 = userManager.createUser("user1", "pwd", () -> "user1", "/home/users/path1");
+        @Nullable Authorizable authorizable = userManager.getAuthorizable(user1.getID());
+        assertEquals(user1, authorizable);
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#getAuthorizable(java.security.Principal)}.
+     */
+    @Test
+    public void testGetAuthorizablePrincipal() throws AuthorizableExistsException, RepositoryException {
+        @NotNull User user1 = userManager.createUser("user1", "pwd", () -> "user1", "/home/users/path1");
+        @Nullable Authorizable authorizable = userManager.getAuthorizable(() -> "user1");
+        assertEquals(user1, authorizable);
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#getAuthorizable(java.lang.String, java.lang.Class)}.
+     */
+    @Test
+    public void testGetAuthorizableStringClassOfT() throws AuthorizableExistsException, RepositoryException {
+        @NotNull User user1 = userManager.createUser("user1", "pwd", () -> "user1", "/home/users/path1");
+        @Nullable Authorizable authorizable = userManager.getAuthorizable("user1", User.class);
+        assertEquals(user1, authorizable);
+
+        assertThrows(AuthorizableTypeException.class, () -> userManager.getAuthorizable("user1", Group.class));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUserManager#getAuthorizableByPath(java.lang.String)}.
+     */
+    @Test
+    public void testGetAuthorizableByPath() throws AuthorizableExistsException, RepositoryException {
+        @NotNull User user1 = userManager.createUser("user1", "pwd", () -> "user1", "/home/users/path1");
+        @Nullable Authorizable authorizable = userManager.getAuthorizableByPath(user1.getPath());
+        assertEquals(user1, authorizable);
+        
+        //throws exception
+    }
+    @Test
+    public void testGetAuthorizableByPathCatchRepositoryException() throws Exception {
+        @NotNull User user1 = userManager.createUser("user1", "pwd", () -> "user1", "/home/users/path1");
+        User mockAuthorizable = Mockito.spy(user1);
+        // replace the original with our mocked copy
+        userManager.authorizables.put(mockAuthorizable.getID(), mockAuthorizable);
+        Mockito.doThrow(RepositoryException.class).when(mockAuthorizable).getPath();
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(userManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.WARN);
+
+            // this should log an warning message
+            assertNull(userManager.getAuthorizableByPath(user1.getPath()));
+
+            // verify the msg was logged
+            capture.assertNotContains(Level.DEBUG, "Failed to match authorizable path");
+        }
+
+        // verify that the debug msg about exception was logged
+        try (LogCapture capture = new LogCapture(userManager.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.DEBUG);
+
+            // this should log an warning message
+            assertNull(userManager.getAuthorizableByPath(user1.getPath()));
+
+            // verify the msg was logged
+            capture.assertContains(Level.DEBUG, "Failed to match authorizable path");
+        }
+    }
+
+}
diff --git a/src/test/java/org/apache/sling/testing/mock/jcr/MockUserTest.java b/src/test/java/org/apache/sling/testing/mock/jcr/MockUserTest.java
new file mode 100644
index 0000000..11e44e8
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/mock/jcr/MockUserTest.java
@@ -0,0 +1,191 @@
+/*
+ * 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.testing.mock.jcr;
+
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
+
+import javax.jcr.Credentials;
+import javax.jcr.RepositoryException;
+import javax.jcr.SimpleCredentials;
+import javax.jcr.UnsupportedRepositoryOperationException;
+
+import org.apache.jackrabbit.api.security.user.User;
+import org.jetbrains.annotations.NotNull;
+import org.junit.Test;
+import org.mockito.Mockito;
+
+import ch.qos.logback.classic.Level;
+
+/**
+ *
+ */
+public class MockUserTest extends MockAuthorizableTest<User> {
+
+    @Override
+    protected User createAuthorizable() throws RepositoryException {
+        return userManager.createUser("user1", "pwd");
+    }
+
+    @Test
+    @Override
+    public void testGetID() throws RepositoryException {
+        assertEquals("user1", authorizable.getID());
+    }
+
+    @Test
+    @Override
+    public void testIsGroup() {
+        assertFalse(authorizable.isGroup());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUser#isAdmin()}.
+     */
+    @Test
+    public void testIsAdmin() throws RepositoryException {
+        assertFalse(authorizable.isAdmin());
+        assertTrue(userManager.createUser("admin", "admin").isAdmin());
+    }
+    @Test
+    public void testIsAdminCatchRepositoryException() throws Exception {
+        User mockAuthorizable = Mockito.spy(authorizable);
+        Mockito.doThrow(RepositoryException.class).when(mockAuthorizable).getID();
+
+        // verify that the debug msg about exception was not logged
+        try (LogCapture capture = new LogCapture(authorizable.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.WARN);
+
+            // this should log an warning message
+            assertFalse(mockAuthorizable.isAdmin());
+
+            // verify the msg was logged
+            capture.assertNotContains(Level.DEBUG, "Failed to determine if this is admin");
+        }
+
+        // verify that the debug msg about exception was logged
+        try (LogCapture capture = new LogCapture(authorizable.getClass().getName(), true)) {
+            // bump up the log level
+            capture.setLoggerLevel(Level.DEBUG);
+
+            // this should log an warning message
+            assertFalse(mockAuthorizable.isAdmin());
+
+            // verify the msg was logged
+            capture.assertContains(Level.DEBUG, "Failed to determine if this is admin");
+        }
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUser#isSystemUser()}.
+     */
+    @Test
+    public void testIsSystemUser() {
+        assertFalse(authorizable.isSystemUser());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUser#getCredentials()}.
+     */
+    @Test
+    public void testGetCredentials() throws RepositoryException {
+        @NotNull Credentials credentials = authorizable.getCredentials();
+        assertTrue(credentials instanceof SimpleCredentials);
+        assertEquals(authorizable.getID(), ((SimpleCredentials)credentials).getUserID());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUser#getImpersonation()}.
+     */
+    @Test
+    public void testGetImpersonation() {
+        assertThrows(UnsupportedOperationException.class, () -> authorizable.getImpersonation());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUser#changePassword(java.lang.String)}.
+     */
+    @Test
+    public void testChangePasswordString() throws RepositoryException {
+        authorizable.changePassword("changed");
+        assertPassword("changed".toCharArray());
+
+        assertThrows(RepositoryException.class, () -> authorizable.changePassword(null));
+    }
+
+    protected void assertPassword(char [] expectedPwd) throws RepositoryException {
+        SimpleCredentials creds = (SimpleCredentials)authorizable.getCredentials();
+        assertArrayEquals(expectedPwd, creds.getPassword());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUser#changePassword(java.lang.String, java.lang.String)}.
+     */
+    @Test
+    public void testChangePasswordStringString() throws RepositoryException {
+        authorizable.changePassword("changed", "");
+        assertPassword("changed".toCharArray());
+
+        assertThrows(RepositoryException.class, () -> authorizable.changePassword("changed2", "wrong"));
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUser#disable(java.lang.String)}.
+     */
+    @Test
+    public void testDisable() throws RepositoryException {
+        authorizable.disable("Expired");
+        assertTrue(authorizable.isDisabled());
+        assertEquals("Expired", authorizable.getDisabledReason());
+
+        authorizable.disable(null);
+        assertFalse(authorizable.isDisabled());
+        assertNull(authorizable.getDisabledReason());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUser#isDisabled()}.
+     */
+    @Test
+    public void testIsDisabled() throws RepositoryException {
+        assertFalse(authorizable.isDisabled());
+        authorizable.disable("Obsolete");
+        assertTrue(authorizable.isDisabled());
+    }
+
+    /**
+     * Test method for {@link org.apache.sling.testing.mock.jcr.MockUser#getDisabledReason()}.
+     */
+    @Test
+    public void testGetDisabledReason() throws RepositoryException {
+        assertNull(authorizable.getDisabledReason());
+        authorizable.disable("Obsolete");
+        assertEquals("Obsolete", authorizable.getDisabledReason());
+    }
+
+    @Test
+    @Override
+    public void testGetPath() throws UnsupportedRepositoryOperationException, RepositoryException {
+        assertEquals("/home/users/user1", authorizable.getPath());
+    }
+
+}
diff --git a/src/test/resources/logback-test.xml b/src/test/resources/logback-test.xml
new file mode 100644
index 0000000..ac70aa8
--- /dev/null
+++ b/src/test/resources/logback-test.xml
@@ -0,0 +1,30 @@
+<?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.
+-->
+<configuration>
+  <appender name="file" class="ch.qos.logback.core.FileAppender">
+    <File>target/testing.log</File>
+    <encoder>
+      <pattern>%d{dd.MM.yyyy HH:mm:ss} *%-5p* [%t] %c{1}: %m \(%F, line %L\)%n</pattern>
+    </encoder>
+  </appender>
+  <root level="INFO">
+    <appender-ref ref="file"/>
+  </root>
+</configuration>