Merge branch 'master' into 0.75-SNAPSHOT

Change-Id: I573d75058d8c23792c13d8a10f014fcbbdc9b33f
diff --git a/src/main/java/de/ids_mannheim/korap/constant/PredefinedRole.java b/src/main/java/de/ids_mannheim/korap/constant/PredefinedRole.java
index f91e511..e12f080 100644
--- a/src/main/java/de/ids_mannheim/korap/constant/PredefinedRole.java
+++ b/src/main/java/de/ids_mannheim/korap/constant/PredefinedRole.java
@@ -7,24 +7,37 @@
  *
  */
 public enum PredefinedRole {
-    USER_GROUP_ADMIN(1), USER_GROUP_MEMBER(2), VC_ACCESS_ADMIN(
-            3), VC_ACCESS_MEMBER(
-                    4), QUERY_ACCESS_ADMIN(5), QUERY_ACCESS_MEMBER(6);
+    GROUP_ADMIN, 
+    GROUP_MEMBER, 
+    QUERY_ACCESS;
 
-    private int id;
-    private String name;
+//    USER_GROUP_ADMIN(1), USER_GROUP_MEMBER(2), VC_ACCESS_ADMIN(
+//            3), VC_ACCESS_MEMBER(
+//                    4), QUERY_ACCESS_ADMIN(5), QUERY_ACCESS_MEMBER(6);
+//
+//    private int id;
+//    private String name;
+//
+//    PredefinedRole (int i) {
+//        this.id = i;
+//        this.name = name().toLowerCase().replace("_", " ");
+//    }
+//
+//    public int getId () {
+//        return id;
+//    }
 
-    PredefinedRole (int i) {
-        this.id = i;
-        this.name = name().toLowerCase().replace("_", " ");
+    
+    private String value;
+
+    PredefinedRole () {
+        this.value = name().toLowerCase().replace("_", " ");
     }
-
-    public int getId () {
-        return id;
-    }
+    
+    
 
     @Override
     public String toString () {
-        return this.name;
+        return this.value;
     }
 }
diff --git a/src/main/java/de/ids_mannheim/korap/constant/PrivilegeType.java b/src/main/java/de/ids_mannheim/korap/constant/PrivilegeType.java
index 0466fa2..5aea201 100644
--- a/src/main/java/de/ids_mannheim/korap/constant/PrivilegeType.java
+++ b/src/main/java/de/ids_mannheim/korap/constant/PrivilegeType.java
@@ -1,16 +1,18 @@
 package de.ids_mannheim.korap.constant;
 
-import de.ids_mannheim.korap.entity.Privilege;
-import de.ids_mannheim.korap.entity.Role;
-
 /**
  * Defines the privilege or permissions of users or admins
  * based on their roles.
  * 
  * @author margaretha
- * @see Privilege
  * @see Role
  */
 public enum PrivilegeType {
-    READ, WRITE, DELETE;
+    READ_MEMBER, 
+    WRITE_MEMBER, 
+    DELETE_MEMBER, 
+    SHARE_QUERY,
+    DELETE_QUERY,
+    READ_QUERY,
+    READ_LARGE_SNIPPET;
 }
diff --git a/src/main/java/de/ids_mannheim/korap/constant/QueryAccessStatus.java b/src/main/java/de/ids_mannheim/korap/constant/QueryAccessStatus.java
deleted file mode 100644
index 3b4f786..0000000
--- a/src/main/java/de/ids_mannheim/korap/constant/QueryAccessStatus.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package de.ids_mannheim.korap.constant;
-
-import de.ids_mannheim.korap.entity.QueryAccess;
-
-/**
- * Defines possible statuses of {@link QueryAccess}
- * 
- * @author margaretha
- * @see QueryAccess
- *
- */
-public enum QueryAccessStatus {
-
-    ACTIVE, DELETED,
-    // has not been used yet
-    PENDING,
-    // access for hidden group
-    // maybe not necessary?
-    HIDDEN;
-}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java b/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java
deleted file mode 100644
index ad1e77b..0000000
--- a/src/main/java/de/ids_mannheim/korap/dao/PrivilegeDao.java
+++ /dev/null
@@ -1,71 +0,0 @@
-package de.ids_mannheim.korap.dao;
-
-import java.util.List;
-
-import org.springframework.stereotype.Repository;
-import org.springframework.transaction.annotation.Transactional;
-
-import de.ids_mannheim.korap.constant.PrivilegeType;
-import de.ids_mannheim.korap.entity.Privilege;
-import de.ids_mannheim.korap.entity.Privilege_;
-import de.ids_mannheim.korap.entity.Role;
-import de.ids_mannheim.korap.entity.Role_;
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.PersistenceContext;
-import jakarta.persistence.Query;
-import jakarta.persistence.criteria.CriteriaBuilder;
-import jakarta.persistence.criteria.CriteriaQuery;
-import jakarta.persistence.criteria.Root;
-
-/**
- * Manages database queries and transactions regarding
- * {@link Privilege} entity or database table.
- * 
- * @see Privilege
- * @see PrivilegeType
- * @see Role
- * 
- * @author margaretha
- *
- */
-@Transactional
-@Repository
-public class PrivilegeDao {
-
-    @PersistenceContext
-    private EntityManager entityManager;
-
-    public void addPrivilegesToRole (Role role,
-            List<PrivilegeType> privilegeTypes) {
-        for (PrivilegeType type : privilegeTypes) {
-            Privilege privilege = new Privilege(type, role);
-            entityManager.persist(privilege);
-        }
-    }
-
-    public void deletePrivilegeFromRole (int roleId,
-            PrivilegeType privilegeType) {
-        List<Privilege> privilegeList = retrievePrivilegeByRoleId(roleId);
-        for (Privilege p : privilegeList) {
-            if (p.getName().equals(privilegeType)) {
-                entityManager.remove(p);
-                break;
-            }
-        }
-    }
-
-    @SuppressWarnings("unchecked")
-    public List<Privilege> retrievePrivilegeByRoleId (int roleId) {
-        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
-        CriteriaQuery<Privilege> query = criteriaBuilder
-                .createQuery(Privilege.class);
-
-        Root<Privilege> root = query.from(Privilege.class);
-        root.fetch(Privilege_.role);
-        query.select(root);
-        query.where(criteriaBuilder
-                .equal(root.get(Privilege_.role).get(Role_.id), roleId));
-        Query q = entityManager.createQuery(query);
-        return q.getResultList();
-    }
-}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/QueryAccessDao.java b/src/main/java/de/ids_mannheim/korap/dao/QueryAccessDao.java
deleted file mode 100644
index 5448b49..0000000
--- a/src/main/java/de/ids_mannheim/korap/dao/QueryAccessDao.java
+++ /dev/null
@@ -1,254 +0,0 @@
-package de.ids_mannheim.korap.dao;
-
-import java.util.List;
-
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.NoResultException;
-import jakarta.persistence.PersistenceContext;
-import jakarta.persistence.Query;
-import jakarta.persistence.TypedQuery;
-import jakarta.persistence.criteria.CriteriaBuilder;
-import jakarta.persistence.criteria.CriteriaQuery;
-import jakarta.persistence.criteria.Join;
-import jakarta.persistence.criteria.Predicate;
-import jakarta.persistence.criteria.Root;
-
-import org.springframework.stereotype.Repository;
-import org.springframework.transaction.annotation.Transactional;
-
-import de.ids_mannheim.korap.constant.QueryAccessStatus;
-import de.ids_mannheim.korap.entity.UserGroup;
-import de.ids_mannheim.korap.entity.UserGroup_;
-import de.ids_mannheim.korap.entity.QueryAccess;
-import de.ids_mannheim.korap.entity.QueryAccess_;
-import de.ids_mannheim.korap.entity.QueryDO;
-import de.ids_mannheim.korap.entity.QueryDO_;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.utils.ParameterChecker;
-
-/**
- * Manages database queries and transactions regarding
- * {@link QueryAccess} entity and its corresponding database
- * table.
- * 
- * @author margaretha
- *
- * @see QueryAccess
- * @see Query
- */
-@Transactional
-@Repository
-public class QueryAccessDao {
-
-    @PersistenceContext
-    private EntityManager entityManager;
-
-    public QueryAccess retrieveAccessById (int accessId)
-            throws KustvaktException {
-        ParameterChecker.checkIntegerValue(accessId, "accessId");
-
-        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
-        CriteriaQuery<QueryAccess> query = builder
-                .createQuery(QueryAccess.class);
-
-        Root<QueryAccess> access = query.from(QueryAccess.class);
-        query.select(access);
-        query.where(builder.equal(access.get(QueryAccess_.id), accessId));
-        Query q = entityManager.createQuery(query);
-        try {
-            return (QueryAccess) q.getSingleResult();
-        }
-        catch (NoResultException e) {
-            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
-                    "Query access is not found", String.valueOf(accessId));
-        }
-    }
-
-    // for query-access admins
-    public List<QueryAccess> retrieveActiveAccessByQuery (int queryId)
-            throws KustvaktException {
-        ParameterChecker.checkIntegerValue(queryId, "queryId");
-
-        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
-        CriteriaQuery<QueryAccess> query = builder
-                .createQuery(QueryAccess.class);
-
-        Root<QueryAccess> access = query.from(QueryAccess.class);
-        Join<QueryAccess, QueryDO> accessQuery = access
-                .join(QueryAccess_.query);
-
-        Predicate p = builder.and(
-                builder.equal(accessQuery.get(QueryDO_.id), queryId),
-                builder.equal(access.get(QueryAccess_.status),
-                        QueryAccessStatus.ACTIVE));
-        query.select(access);
-        query.where(p);
-        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
-        return q.getResultList();
-    }
-
-    public List<QueryAccess> retrieveActiveAccessByQuery (String queryCreator,
-            String queryName) throws KustvaktException {
-        ParameterChecker.checkStringValue(queryCreator, "queryCreator");
-        ParameterChecker.checkStringValue(queryName, "queryName");
-
-        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
-        CriteriaQuery<QueryAccess> query = builder
-                .createQuery(QueryAccess.class);
-
-        Root<QueryAccess> access = query.from(QueryAccess.class);
-        Join<QueryAccess, QueryDO> accessQuery = access
-                .join(QueryAccess_.query);
-
-        Predicate p = builder.and(
-                builder.equal(accessQuery.get(QueryDO_.name), queryName),
-                builder.equal(accessQuery.get(QueryDO_.createdBy),
-                        queryCreator),
-                builder.equal(access.get(QueryAccess_.status),
-                        QueryAccessStatus.ACTIVE));
-        query.select(access);
-        query.where(p);
-        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
-        return q.getResultList();
-    }
-
-    public List<QueryAccess> retrieveAllAccess () throws KustvaktException {
-
-        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
-        CriteriaQuery<QueryAccess> query = builder
-                .createQuery(QueryAccess.class);
-        Root<QueryAccess> access = query.from(QueryAccess.class);
-        query.select(access);
-        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
-        return q.getResultList();
-    }
-
-    public List<QueryAccess> retrieveAllAccessByQuery (String queryCreator,
-            String queryName) throws KustvaktException {
-        ParameterChecker.checkStringValue(queryCreator, "queryCreator");
-        ParameterChecker.checkStringValue(queryName, "queryName");
-
-        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
-        CriteriaQuery<QueryAccess> query = builder
-                .createQuery(QueryAccess.class);
-
-        Root<QueryAccess> access = query.from(QueryAccess.class);
-        Join<QueryAccess, QueryDO> accessQuery = access
-                .join(QueryAccess_.query);
-
-        Predicate conditions = builder.and(
-                builder.equal(accessQuery.get(QueryDO_.createdBy),
-                        queryCreator),
-                builder.equal(accessQuery.get(QueryDO_.name), queryName));
-        query.select(access);
-        query.where(conditions);
-        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
-        return q.getResultList();
-    }
-
-    public List<QueryAccess> retrieveAllAccessByGroup (int groupId)
-            throws KustvaktException {
-        ParameterChecker.checkIntegerValue(groupId, "groupId");
-
-        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
-        CriteriaQuery<QueryAccess> query = builder
-                .createQuery(QueryAccess.class);
-
-        Root<QueryAccess> access = query.from(QueryAccess.class);
-        Join<QueryAccess, UserGroup> accessQuery = access
-                .join(QueryAccess_.userGroup);
-
-        query.select(access);
-        query.where(builder.equal(accessQuery.get(UserGroup_.id), groupId));
-        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
-        return q.getResultList();
-    }
-
-    public List<QueryAccess> retrieveActiveAccessByGroup (int groupId)
-            throws KustvaktException {
-        ParameterChecker.checkIntegerValue(groupId, "groupId");
-
-        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
-        CriteriaQuery<QueryAccess> query = builder
-                .createQuery(QueryAccess.class);
-
-        Root<QueryAccess> access = query.from(QueryAccess.class);
-        Join<QueryAccess, UserGroup> accessQuery = access
-                .join(QueryAccess_.userGroup);
-
-        Predicate p = builder.and(
-                builder.equal(accessQuery.get(UserGroup_.id), groupId),
-                builder.equal(access.get(QueryAccess_.status),
-                        QueryAccessStatus.ACTIVE));
-
-        query.select(access);
-        query.where(p);
-        TypedQuery<QueryAccess> q = entityManager.createQuery(query);
-        return q.getResultList();
-    }
-
-    /**
-     * Hidden accesses are only created for published or system query.
-     * 
-     * Warn: The actual hidden accesses are not checked.
-     * 
-     * @param queryId
-     *            queryId
-     * @return true if there is a hidden access, false otherwise
-     * @throws KustvaktException
-     */
-    public QueryAccess retrieveHiddenAccess (int queryId)
-            throws KustvaktException {
-        ParameterChecker.checkIntegerValue(queryId, "queryId");
-
-        CriteriaBuilder builder = entityManager.getCriteriaBuilder();
-        CriteriaQuery<QueryAccess> query = builder
-                .createQuery(QueryAccess.class);
-
-        Root<QueryAccess> access = query.from(QueryAccess.class);
-        Join<QueryAccess, QueryDO> accessQuery = access
-                .join(QueryAccess_.query);
-
-        Predicate p = builder.and(
-                builder.equal(accessQuery.get(QueryDO_.id), queryId),
-                builder.equal(access.get(QueryAccess_.status),
-                        QueryAccessStatus.HIDDEN)
-        // ,
-        // builder.notEqual(access.get(QueryAccess_.deletedBy),
-        // "NULL")
-        );
-
-        query.select(access);
-        query.where(p);
-
-        try {
-            Query q = entityManager.createQuery(query);
-            return (QueryAccess) q.getSingleResult();
-        }
-        catch (NoResultException e) {
-            return null;
-        }
-    }
-
-    public void createAccessToQuery (QueryDO query, UserGroup userGroup,
-            String createdBy, QueryAccessStatus status) {
-        QueryAccess queryAccess = new QueryAccess();
-        queryAccess.setQuery(query);
-        queryAccess.setUserGroup(userGroup);
-        queryAccess.setCreatedBy(createdBy);
-        queryAccess.setStatus(status);
-        entityManager.persist(queryAccess);
-    }
-
-    public void deleteAccess (QueryAccess access, String deletedBy) {
-        // soft delete
-
-        // hard delete
-        if (!entityManager.contains(access)) {
-            access = entityManager.merge(access);
-        }
-        entityManager.remove(access);
-    }
-
-}
diff --git a/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java b/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java
index e17582a..800be3d 100644
--- a/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java
+++ b/src/main/java/de/ids_mannheim/korap/dao/QueryDao.java
@@ -6,6 +6,23 @@
 import java.util.List;
 import java.util.Set;
 
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.QueryType;
+import de.ids_mannheim.korap.constant.ResourceType;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.entity.QueryDO_;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.Role_;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+import de.ids_mannheim.korap.entity.UserGroupMember_;
+import de.ids_mannheim.korap.entity.UserGroup_;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.user.User.CorpusAccess;
+import de.ids_mannheim.korap.utils.ParameterChecker;
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.NoResultException;
 import jakarta.persistence.NonUniqueResultException;
@@ -18,27 +35,6 @@
 import jakarta.persistence.criteria.Predicate;
 import jakarta.persistence.criteria.Root;
 
-import org.springframework.stereotype.Repository;
-import org.springframework.transaction.annotation.Transactional;
-
-import de.ids_mannheim.korap.constant.GroupMemberStatus;
-import de.ids_mannheim.korap.constant.QueryAccessStatus;
-import de.ids_mannheim.korap.constant.QueryType;
-import de.ids_mannheim.korap.constant.ResourceType;
-import de.ids_mannheim.korap.constant.UserGroupStatus;
-import de.ids_mannheim.korap.entity.QueryAccess;
-import de.ids_mannheim.korap.entity.QueryAccess_;
-import de.ids_mannheim.korap.entity.QueryDO;
-import de.ids_mannheim.korap.entity.QueryDO_;
-import de.ids_mannheim.korap.entity.UserGroup;
-import de.ids_mannheim.korap.entity.UserGroupMember;
-import de.ids_mannheim.korap.entity.UserGroupMember_;
-import de.ids_mannheim.korap.entity.UserGroup_;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.user.User.CorpusAccess;
-import de.ids_mannheim.korap.utils.ParameterChecker;
-
 /**
  * QueryDao manages database queries and transactions
  * regarding virtual corpus and KorAP queries.
@@ -273,35 +269,19 @@
         CriteriaQuery<QueryDO> cq = builder.createQuery(QueryDO.class);
 
         Root<QueryDO> query = cq.from(QueryDO.class);
-        Join<QueryDO, QueryAccess> access = query.join(QueryDO_.queryAccess);
-
-        // Predicate corpusStatus = builder.and(
-        // builder.notEqual(access.get(QueryAccess_.status),
-        // VirtualCorpusAccessStatus.HIDDEN),
-        // builder.notEqual(access.get(QueryAccess_.status),
-        // VirtualCorpusAccessStatus.DELETED));
-
+        Join<QueryDO, Role> roles = query.join(QueryDO_.roles);
+        Join<Role, UserGroupMember> members = roles
+                .join(Role_.userGroupMembers);
+        
         Predicate type = builder.equal(query.get(QueryDO_.queryType),
                 queryType);
-
-        Predicate accessStatus = builder.notEqual(
-                access.get(QueryAccess_.status), QueryAccessStatus.DELETED);
-
-        Predicate userGroupStatus = builder.notEqual(
-                access.get(QueryAccess_.userGroup).get(UserGroup_.status),
-                UserGroupStatus.DELETED);
-        Join<UserGroup, UserGroupMember> members = access
-                .join(QueryAccess_.userGroup).join(UserGroup_.members);
-
         Predicate memberStatus = builder.equal(
                 members.get(UserGroupMember_.status), GroupMemberStatus.ACTIVE);
-
         Predicate user = builder.equal(members.get(UserGroupMember_.userId),
                 userId);
 
         cq.select(query);
-        cq.where(builder.and(type, accessStatus, userGroupStatus, memberStatus,
-                user));
+        cq.where(builder.and(type, memberStatus, user));
 
         Query q = entityManager.createQuery(cq);
         return q.getResultList();
@@ -352,14 +332,12 @@
                 .createQuery(QueryDO.class);
 
         Root<QueryDO> query = criteriaQuery.from(QueryDO.class);
-        Join<QueryDO, QueryAccess> queryAccess = query
-                .join(QueryDO_.queryAccess);
-        Join<QueryAccess, UserGroup> accessGroup = queryAccess
-                .join(QueryAccess_.userGroup);
+        Join<QueryDO, Role> query_role = query
+                .join(QueryDO_.roles);
 
         criteriaQuery.select(query);
-        criteriaQuery
-                .where(builder.equal(accessGroup.get(UserGroup_.id), groupId));
+        criteriaQuery.where(builder.equal(
+                query_role.get(Role_.userGroup).get(UserGroup_.id), groupId));
         Query q = entityManager.createQuery(criteriaQuery);
         return q.getResultList();
     }
diff --git a/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java b/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
index 5481d30..6c68c13 100644
--- a/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
+++ b/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
@@ -4,23 +4,35 @@
 import java.util.List;
 import java.util.Set;
 
-import jakarta.persistence.EntityManager;
-import jakarta.persistence.PersistenceContext;
-import jakarta.persistence.Query;
-import jakarta.persistence.criteria.CriteriaBuilder;
-import jakarta.persistence.criteria.CriteriaQuery;
-import jakarta.persistence.criteria.ListJoin;
-import jakarta.persistence.criteria.Root;
-
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Repository;
 import org.springframework.transaction.annotation.Transactional;
 
+import de.ids_mannheim.korap.constant.PredefinedRole;
 import de.ids_mannheim.korap.constant.PrivilegeType;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.entity.QueryDO_;
 import de.ids_mannheim.korap.entity.Role;
 import de.ids_mannheim.korap.entity.Role_;
+import de.ids_mannheim.korap.entity.UserGroup;
 import de.ids_mannheim.korap.entity.UserGroupMember;
 import de.ids_mannheim.korap.entity.UserGroupMember_;
+import de.ids_mannheim.korap.entity.UserGroup_;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.NoResultException;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+import jakarta.persistence.TypedQuery;
+import jakarta.persistence.criteria.CriteriaBuilder;
+import jakarta.persistence.criteria.CriteriaDelete;
+import jakarta.persistence.criteria.CriteriaQuery;
+import jakarta.persistence.criteria.Expression;
+import jakarta.persistence.criteria.Join;
+import jakarta.persistence.criteria.JoinType;
+import jakarta.persistence.criteria.ListJoin;
+import jakarta.persistence.criteria.Root;
+import jakarta.persistence.criteria.Subquery;
 
 /**
  * Manages database queries and transactions regarding {@link Role}
@@ -28,7 +40,6 @@
  * 
  * @author margaretha
  * @see Role
- * @see PrivilegeDao
  */
 @Transactional
 @Repository
@@ -37,47 +48,38 @@
     @PersistenceContext
     private EntityManager entityManager;
 
-    @Autowired
-    private PrivilegeDao privilegeDao;
-
-    public void createRole (String name, List<PrivilegeType> privilegeTypes) {
-        Role r = new Role();
-        r.setName(name);
-        entityManager.persist(r);
-        privilegeDao.addPrivilegesToRole(r, privilegeTypes);
+    public void addRole (Role newRole) {
+        entityManager.persist(newRole);
+        entityManager.flush();
     }
 
-    public void deleteRole (int roleId) {
-        Role r = retrieveRoleById(roleId);
-        entityManager.remove(r);
-    }
-
-    public void editRoleName (int roleId, String name) {
-        Role r = retrieveRoleById(roleId);
-        r.setName(name);
-        entityManager.persist(r);
-    }
-
-    public Role retrieveRoleById (int roleId) {
+    public Role retrieveRoleById (int roleId) throws KustvaktException {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<Role> query = criteriaBuilder.createQuery(Role.class);
 
         Root<Role> root = query.from(Role.class);
-        root.fetch(Role_.privileges);
+        root.fetch(Role_.userGroup);
         query.select(root);
         query.where(criteriaBuilder.equal(root.get(Role_.id), roleId));
         Query q = entityManager.createQuery(query);
-        return (Role) q.getSingleResult();
+        
+        try {
+            return (Role) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                    "Role is not found", String.valueOf(roleId));
+        }
     }
 
-    public Role retrieveRoleByName (String roleName) {
+    public Role retrieveRoleByName (PredefinedRole role) {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<Role> query = criteriaBuilder.createQuery(Role.class);
 
         Root<Role> root = query.from(Role.class);
-        root.fetch(Role_.privileges);
+        //        root.fetch(Role_.privileges);
         query.select(root);
-        query.where(criteriaBuilder.equal(root.get(Role_.name), roleName));
+        query.where(criteriaBuilder.equal(root.get(Role_.name), role));
         Query q = entityManager.createQuery(query);
         return (Role) q.getSingleResult();
     }
@@ -93,10 +95,132 @@
         query.select(root);
         query.where(criteriaBuilder.equal(memberRole.get(UserGroupMember_.id),
                 userId));
-        Query q = entityManager.createQuery(query);
-        @SuppressWarnings("unchecked")
+        TypedQuery<Role> q = entityManager.createQuery(query);
         List<Role> resultList = q.getResultList();
         return new HashSet<Role>(resultList);
     }
+    
+    public Set<Role> retrieveRoleByGroupId (int groupId, boolean hasQuery) {
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Role> query = cb.createQuery(Role.class);
+
+        Root<Role> role = query.from(Role.class);
+        role.fetch("userGroup", JoinType.INNER);
+        
+        query.select(role);
+        if (hasQuery) {
+            role.fetch("query", JoinType.INNER);
+            query.where(cb.equal(role.get("userGroup").get("id"), groupId),
+                    cb.isNotNull(role.get("query").get("id")));
+        }
+        else {
+            query.where(cb.equal(role.get("userGroup").get("id"), groupId));
+        }
+
+        TypedQuery<Role> q = entityManager.createQuery(query);
+        List<Role> resultList = q.getResultList();
+        return new HashSet<Role>(resultList);
+    }
+    
+    public Set<Role> retrieveRolesByGroupIdWithUniqueQuery (int groupId) {
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Role> query = cb.createQuery(Role.class);
+
+        Root<Role> role = query.from(Role.class);
+        role.fetch("userGroup", JoinType.INNER);
+//        role.fetch("query", JoinType.INNER);
+//        role.fetch("userGroupMembers", JoinType.INNER);
+        
+        Expression<?> queryId = role.get("query").get("id");
+        
+        query.select(role);
+        query.where(
+                cb.equal(role.get("userGroup").get("id"), groupId)
+        );
+        query.groupBy(queryId);
+        query.having(cb.equal(cb.count(queryId), 1));
+
+        TypedQuery<Role> q = entityManager.createQuery(query);
+        List<Role> resultList = q.getResultList();
+        return new HashSet<Role>(resultList);
+    }
+
+    public Set<Role> retrieveRoleByQueryIdAndUsername (int queryId,
+            String username) {
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Role> query = cb.createQuery(Role.class);
+
+        Root<Role> role = query.from(Role.class);
+        role.fetch(Role_.query, JoinType.INNER);
+
+        Join<Role, UserGroupMember> members = role.join("userGroupMembers",
+                JoinType.INNER);
+        
+        query.select(role);
+        query.where(cb.equal(role.get(Role_.query).get(QueryDO_.id), queryId),
+                cb.equal(members.get(UserGroupMember_.userId), username));
+
+        TypedQuery<Role> q = entityManager.createQuery(query);
+        List<Role> resultList = q.getResultList();
+        return new HashSet<Role>(resultList);
+    }
+
+    public Role retrieveRoleByGroupIdQueryIdPrivilege (int groupId, int queryId,
+            PrivilegeType p) throws KustvaktException {
+
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        CriteriaQuery<Role> query = cb.createQuery(Role.class);
+
+        Root<Role> role = query.from(Role.class);
+        role.fetch("userGroup", JoinType.INNER);
+        role.fetch(Role_.query, JoinType.INNER);
+
+        query.select(role);
+        query.where(
+                cb.equal(role.get(Role_.query).get(QueryDO_.id), queryId),
+                cb.equal(role.get(Role_.privilege), p), 
+                cb.equal(role.get(Role_.userGroup).get(UserGroup_.id),
+                        groupId));
+
+        TypedQuery<Role> q = entityManager.createQuery(query);
+        return (Role) q.getSingleResult();
+    }
+
+    @Deprecated
+    public void deleteRole (int roleId) throws KustvaktException {
+
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        CriteriaDelete<Role> delete = cb.createCriteriaDelete(Role.class);
+        Root<Role> role = delete.from(Role.class);
+
+        delete.where(
+                cb.equal(role.get("id"), roleId));
+        
+        entityManager.createQuery(delete).executeUpdate();
+    }
+    
+    public void deleteRoleByGroupAndQuery (String groupName,
+            String queryCreator, String queryName) throws KustvaktException {
+        CriteriaBuilder cb = entityManager.getCriteriaBuilder();
+        
+        CriteriaDelete<Role> delete = cb.createCriteriaDelete(Role.class);
+        Root<Role> deleteRole = delete.from(Role.class);
+        
+        Subquery<Integer> subquery = delete.subquery(Integer.class);
+        Root<Role> role = subquery.from(Role.class);
+        Join<Role, UserGroup> groupRole = role.join(Role_.userGroup);
+        Join<Role, QueryDO> queryRole = role.join(Role_.query);
+
+        subquery.select(role.get(Role_.id))
+                .where(cb.and(
+                        cb.equal(groupRole.get(UserGroup_.name), groupName),
+                        cb.equal(queryRole.get(QueryDO_.createdBy),
+                                queryCreator),
+                        cb.equal(queryRole.get(QueryDO_.name), queryName)));
+
+       
+        delete.where(deleteRole.get(Role_.id).in(subquery));
+        entityManager.createQuery(delete).executeUpdate();
+    }
 
 }
diff --git a/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java b/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
index 8a91e9c..5444a96 100644
--- a/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
+++ b/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
@@ -1,9 +1,28 @@
 package de.ids_mannheim.korap.dao;
 
+import java.time.ZonedDateTime;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.PrivilegeType;
+import de.ids_mannheim.korap.constant.UserGroupStatus;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.entity.QueryDO_;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.Role_;
+import de.ids_mannheim.korap.entity.UserGroup;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+import de.ids_mannheim.korap.entity.UserGroupMember_;
+import de.ids_mannheim.korap.entity.UserGroup_;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.ParameterChecker;
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.NoResultException;
 import jakarta.persistence.PersistenceContext;
@@ -15,27 +34,6 @@
 import jakarta.persistence.criteria.Predicate;
 import jakarta.persistence.criteria.Root;
 
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Repository;
-import org.springframework.transaction.annotation.Transactional;
-
-import de.ids_mannheim.korap.constant.GroupMemberStatus;
-import de.ids_mannheim.korap.constant.PredefinedRole;
-import de.ids_mannheim.korap.constant.UserGroupStatus;
-import de.ids_mannheim.korap.constant.QueryAccessStatus;
-import de.ids_mannheim.korap.entity.Role;
-import de.ids_mannheim.korap.entity.UserGroup;
-import de.ids_mannheim.korap.entity.UserGroupMember;
-import de.ids_mannheim.korap.entity.UserGroupMember_;
-import de.ids_mannheim.korap.entity.UserGroup_;
-import de.ids_mannheim.korap.entity.QueryDO;
-import de.ids_mannheim.korap.entity.QueryAccess;
-import de.ids_mannheim.korap.entity.QueryAccess_;
-import de.ids_mannheim.korap.entity.QueryDO_;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.utils.ParameterChecker;
-
 /**
  * Manages database queries and transactions regarding
  * {@link UserGroup} entity and database table.
@@ -52,9 +50,6 @@
     @PersistenceContext
     private EntityManager entityManager;
 
-    @Autowired
-    private RoleDao roleDao;
-
     public int createGroup (String name, String description, String createdBy,
             UserGroupStatus status) throws KustvaktException {
         ParameterChecker.checkStringValue(name, "name");
@@ -66,24 +61,44 @@
         group.setDescription(description);
         group.setStatus(status);
         group.setCreatedBy(createdBy);
+        group.setCreatedDate(ZonedDateTime.now());
         entityManager.persist(group);
-
-        Set<Role> roles = new HashSet<Role>();
-        roles.add(roleDao
-                .retrieveRoleById(PredefinedRole.USER_GROUP_ADMIN.getId()));
-        roles.add(roleDao
-                .retrieveRoleById(PredefinedRole.VC_ACCESS_ADMIN.getId()));
-
-        UserGroupMember owner = new UserGroupMember();
-        owner.setUserId(createdBy);
-        owner.setCreatedBy(createdBy);
-        owner.setStatus(GroupMemberStatus.ACTIVE);
-        owner.setGroup(group);
-        owner.setRoles(roles);
-        entityManager.persist(owner);
-
+        entityManager.flush();
+        
+        if (createdBy != "system") {
+            Set<Role> roles = createUserGroupAdminRoles(group);
+            for (Role role : roles) {
+                entityManager.persist(role);
+            }
+            entityManager.flush();
+        
+            UserGroupMember owner = new UserGroupMember();
+            owner.setUserId(createdBy);
+            owner.setCreatedBy(createdBy);
+            owner.setStatus(GroupMemberStatus.ACTIVE);
+            owner.setGroup(group);
+            owner.setRoles(roles);
+            entityManager.persist(owner);
+            entityManager.flush();
+        };
+        
         return group.getId();
     }
+    
+    private Set<Role> createUserGroupAdminRoles (UserGroup group) {
+        Set<Role> roles = new HashSet<Role>();
+        roles.add(new Role(PredefinedRole.GROUP_ADMIN,
+                PrivilegeType.DELETE_MEMBER, group));
+        roles.add(new Role(PredefinedRole.GROUP_ADMIN, PrivilegeType.READ_MEMBER,
+                group));
+        roles.add(new Role(PredefinedRole.GROUP_ADMIN, PrivilegeType.WRITE_MEMBER,
+                group));
+        roles.add(new Role(PredefinedRole.GROUP_ADMIN, PrivilegeType.SHARE_QUERY,
+                group));
+        roles.add(new Role(PredefinedRole.GROUP_ADMIN, PrivilegeType.DELETE_QUERY,
+                group));
+        return roles;
+    }
 
     public void deleteGroup (int groupId, String deletedBy,
             boolean isSoftDelete) throws KustvaktException {
@@ -232,23 +247,24 @@
                     "Group " + groupName + " is not found", groupName, e);
         }
     }
-
-    public UserGroup retrieveHiddenGroupByQuery (int queryId)
+    
+    public UserGroup retrieveHiddenGroupByQueryName (String queryName)
             throws KustvaktException {
-        ParameterChecker.checkIntegerValue(queryId, "queryId");
+        ParameterChecker.checkNameValue(queryName, "queryName");
 
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<UserGroup> criteriaQuery = criteriaBuilder
                 .createQuery(UserGroup.class);
 
         Root<UserGroup> root = criteriaQuery.from(UserGroup.class);
-        Join<UserGroup, QueryAccess> access = root.join(UserGroup_.queryAccess);
-        Join<QueryAccess, QueryDO> query = access.join(QueryAccess_.query);
+        Join<UserGroup, Role> group_role = root.join(UserGroup_.roles);
+        Join<Role, QueryDO> query_role = group_role.join(Role_.query);
 
         Predicate p = criteriaBuilder.and(
                 criteriaBuilder.equal(root.get(UserGroup_.status),
                         UserGroupStatus.HIDDEN),
-                criteriaBuilder.equal(query.get(QueryDO_.id), queryId));
+                criteriaBuilder.equal(query_role.get(QueryDO_.name), queryName)
+        );
 
         criteriaQuery.select(root);
         criteriaQuery.where(p);
@@ -258,7 +274,40 @@
             return (UserGroup) q.getSingleResult();
         }
         catch (NoResultException e) {
-            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND,
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
+                    "No hidden group for query " + queryName
+                            + " is found",
+                    String.valueOf(queryName), e);
+        }
+
+    }
+
+    public UserGroup retrieveHiddenGroupByQueryId (int queryId)
+            throws KustvaktException {
+        ParameterChecker.checkIntegerValue(queryId, "queryId");
+
+        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
+        CriteriaQuery<UserGroup> criteriaQuery = criteriaBuilder
+                .createQuery(UserGroup.class);
+
+        Root<UserGroup> root = criteriaQuery.from(UserGroup.class);
+        Join<UserGroup, Role> group_role = root.join(UserGroup_.roles);
+        Join<Role, QueryDO> query_role = group_role.join(Role_.query);
+
+        Predicate p = criteriaBuilder.and(
+                criteriaBuilder.equal(root.get(UserGroup_.status),
+                        UserGroupStatus.HIDDEN),
+                criteriaBuilder.equal(query_role.get(QueryDO_.id), queryId));
+
+        criteriaQuery.select(root);
+        criteriaQuery.where(p);
+        Query q = entityManager.createQuery(criteriaQuery);
+
+        try {
+            return (UserGroup) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
                     "No hidden group for query with id " + queryId
                             + " is found",
                     String.valueOf(queryId), e);
@@ -322,48 +371,4 @@
         }
 
     }
-
-    public void addQueryToGroup (QueryDO query, String createdBy,
-            QueryAccessStatus status, UserGroup group) {
-        QueryAccess accessGroup = new QueryAccess();
-        accessGroup.setCreatedBy(createdBy);
-        accessGroup.setStatus(status);
-        accessGroup.setUserGroup(group);
-        accessGroup.setQuery(query);;
-        entityManager.persist(accessGroup);
-    }
-
-    public void addQueryToGroup (List<QueryDO> queries, String createdBy,
-            UserGroup group, QueryAccessStatus status) {
-
-        for (QueryDO q : queries) {
-            addQueryToGroup(q, createdBy, status, group);
-        }
-    }
-
-    public void deleteQueryFromGroup (int queryId, int groupId)
-            throws KustvaktException {
-        ParameterChecker.checkIntegerValue(queryId, "queryId");
-        ParameterChecker.checkIntegerValue(groupId, "groupId");
-
-        CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
-        CriteriaQuery<QueryAccess> criteriaQuery = criteriaBuilder
-                .createQuery(QueryAccess.class);
-
-        Root<QueryAccess> root = criteriaQuery.from(QueryAccess.class);
-        Join<QueryAccess, QueryDO> queryAccess = root.join(QueryAccess_.query);
-        Join<QueryAccess, UserGroup> group = root.join(QueryAccess_.userGroup);
-
-        Predicate query = criteriaBuilder.equal(queryAccess.get(QueryDO_.id),
-                queryId);
-        Predicate userGroup = criteriaBuilder.equal(group.get(UserGroup_.id),
-                groupId);
-
-        criteriaQuery.select(root);
-        criteriaQuery.where(criteriaBuilder.and(query, userGroup));
-        Query q = entityManager.createQuery(criteriaQuery);
-        QueryAccess access = (QueryAccess) q.getSingleResult();
-        entityManager.remove(access);
-    }
-
 }
diff --git a/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java b/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
index c399fd6..d7b57bd 100644
--- a/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
+++ b/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
@@ -17,6 +17,7 @@
 import org.springframework.transaction.annotation.Transactional;
 
 import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PredefinedRole;
 import de.ids_mannheim.korap.entity.Role;
 import de.ids_mannheim.korap.entity.Role_;
 import de.ids_mannheim.korap.entity.UserGroupMember;
@@ -44,6 +45,7 @@
     public void addMember (UserGroupMember member) throws KustvaktException {
         ParameterChecker.checkObjectValue(member, "userGroupMember");
         entityManager.persist(member);
+        entityManager.flush();
     }
 
     public void updateMember (UserGroupMember member) throws KustvaktException {
@@ -104,9 +106,8 @@
     }
 
     @SuppressWarnings("unchecked")
-    public List<UserGroupMember> retrieveMemberByRole (int groupId, int roleId)
-            throws KustvaktException {
-        ParameterChecker.checkIntegerValue(roleId, "roleId");
+    public List<UserGroupMember> retrieveMemberByRole (int groupId,
+            PredefinedRole role) throws KustvaktException {
         ParameterChecker.checkIntegerValue(groupId, "groupId");
 
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
@@ -121,7 +122,7 @@
                         groupId),
                 criteriaBuilder.equal(root.get(UserGroupMember_.status),
                         GroupMemberStatus.ACTIVE),
-                criteriaBuilder.equal(memberRole.get(Role_.id), roleId));
+                criteriaBuilder.equal(memberRole.get(Role_.NAME), role));
 
         query.select(root);
         query.where(predicate);
@@ -131,9 +132,9 @@
         }
         catch (NoResultException e) {
             throw new KustvaktException(
-                    StatusCodes.NO_RESULT_FOUND, "No member with role " + roleId
+                    StatusCodes.NO_RESULT_FOUND, "No member with role " + role.name()
                             + " is found in group " + groupId,
-                    String.valueOf(roleId));
+                    role.name());
         }
     }
 
diff --git a/src/main/java/de/ids_mannheim/korap/dto/QueryAccessDto.java b/src/main/java/de/ids_mannheim/korap/dto/QueryAccessDto.java
deleted file mode 100644
index 84b8b3f..0000000
--- a/src/main/java/de/ids_mannheim/korap/dto/QueryAccessDto.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package de.ids_mannheim.korap.dto;
-
-import lombok.Getter;
-import lombok.Setter;
-
-/**
- * Defines the structure of query accesses, e.g. as JSON
- * objects in HTTP Responses.
- * 
- * @author margaretha
- *
- */
-@Getter
-@Setter
-public class QueryAccessDto {
-    private int accessId;
-    private String createdBy;
-    private int queryId;
-    private String queryName;
-    private int userGroupId;
-    private String userGroupName;
-
-    @Override
-    public String toString () {
-        return "accessId=" + accessId + ", createdBy=" + createdBy
-                + " , queryId=" + queryId + ", queryName=" + queryName
-                + ", userGroupId=" + userGroupId + ", userGroupName="
-                + userGroupName;
-    }
-}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/RoleDto.java b/src/main/java/de/ids_mannheim/korap/dto/RoleDto.java
new file mode 100644
index 0000000..0e16496
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/RoleDto.java
@@ -0,0 +1,37 @@
+package de.ids_mannheim.korap.dto;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonInclude;
+
+import lombok.Getter;
+import lombok.Setter;
+
+/**
+ * Defines the structure of query roles, e.g. as JSON
+ * objects in HTTP Responses.
+ * 
+ * @author margaretha
+ *
+ */
+@Getter
+@Setter
+public class RoleDto {
+    private int roleId;
+    private String privilege;
+    @JsonInclude(JsonInclude.Include.NON_DEFAULT)
+    private int queryId;
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private String queryName;
+    private int userGroupId;
+    private String userGroupName;
+    private List<String> members;
+
+    @Override
+    public String toString () {
+        return "roleId=" + roleId + " , queryId=" + queryId + ", queryName="
+                + queryName + ", userGroupId=" + userGroupId
+                + ", userGroupName=" + userGroupName 
+                +", members=" + members;
+    }
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/UserGroupDto.java b/src/main/java/de/ids_mannheim/korap/dto/UserGroupDto.java
index 273d52e..0ed52ef 100644
--- a/src/main/java/de/ids_mannheim/korap/dto/UserGroupDto.java
+++ b/src/main/java/de/ids_mannheim/korap/dto/UserGroupDto.java
@@ -5,6 +5,7 @@
 import com.fasterxml.jackson.annotation.JsonInclude;
 
 import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PrivilegeType;
 import de.ids_mannheim.korap.constant.UserGroupStatus;
 import lombok.Getter;
 import lombok.Setter;
@@ -29,6 +30,8 @@
     @JsonInclude(JsonInclude.Include.NON_EMPTY)
     private List<UserGroupMemberDto> members;
 
+    @JsonInclude(JsonInclude.Include.NON_NULL)
     private GroupMemberStatus userMemberStatus;
-    private List<String> userRoles;
+    @JsonInclude(JsonInclude.Include.NON_NULL)
+    private List<PrivilegeType> userPrivileges;
 }
diff --git a/src/main/java/de/ids_mannheim/korap/dto/UserGroupMemberDto.java b/src/main/java/de/ids_mannheim/korap/dto/UserGroupMemberDto.java
index 23cfa3d..ef50116 100644
--- a/src/main/java/de/ids_mannheim/korap/dto/UserGroupMemberDto.java
+++ b/src/main/java/de/ids_mannheim/korap/dto/UserGroupMemberDto.java
@@ -3,6 +3,7 @@
 import java.util.List;
 
 import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PrivilegeType;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -18,5 +19,5 @@
 public class UserGroupMemberDto {
     private String userId;
     private GroupMemberStatus status;
-    private List<String> roles;
+    private List<PrivilegeType> privileges;
 }
diff --git a/src/main/java/de/ids_mannheim/korap/dto/converter/QueryAccessConverter.java b/src/main/java/de/ids_mannheim/korap/dto/converter/QueryAccessConverter.java
deleted file mode 100644
index 5dfad63..0000000
--- a/src/main/java/de/ids_mannheim/korap/dto/converter/QueryAccessConverter.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package de.ids_mannheim.korap.dto.converter;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.springframework.stereotype.Component;
-
-import de.ids_mannheim.korap.dto.QueryAccessDto;
-import de.ids_mannheim.korap.entity.QueryAccess;
-
-/**
- * QueryAccessConverter prepares data transfer objects (DTOs)
- * from {@link QueryAccess} entities. DTO structure defines
- * controllers output, namely the structure of JSON objects in HTTP
- * responses.
- * 
- * @author margaretha
- *
- */
-@Component
-public class QueryAccessConverter {
-
-    public List<QueryAccessDto> createQueryAccessDto (
-            List<QueryAccess> accessList) {
-        List<QueryAccessDto> dtos = new ArrayList<>(accessList.size());
-        for (QueryAccess access : accessList) {
-            QueryAccessDto dto = new QueryAccessDto();
-            dto.setAccessId(access.getId());
-            dto.setCreatedBy(access.getCreatedBy());
-
-            dto.setQueryId(access.getQuery().getId());
-            dto.setQueryName(access.getQuery().getName());
-
-            dto.setUserGroupId(access.getUserGroup().getId());
-            dto.setUserGroupName(access.getUserGroup().getName());
-
-            dtos.add(dto);
-        }
-        return dtos;
-    }
-
-}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/converter/RoleConverter.java b/src/main/java/de/ids_mannheim/korap/dto/converter/RoleConverter.java
new file mode 100644
index 0000000..08ddb8e
--- /dev/null
+++ b/src/main/java/de/ids_mannheim/korap/dto/converter/RoleConverter.java
@@ -0,0 +1,49 @@
+package de.ids_mannheim.korap.dto.converter;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+import org.springframework.stereotype.Component;
+
+import de.ids_mannheim.korap.dto.RoleDto;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+
+/**
+ * QueryAccessConverter prepares data transfer objects (DTOs)
+ * from {@link QueryAccess} entities. DTO structure defines
+ * controllers output, namely the structure of JSON objects in HTTP
+ * responses.
+ * 
+ * @author margaretha
+ *
+ */
+@Component
+public class RoleConverter {
+
+    public List<RoleDto> createRoleDto (Set<Role> roles) {
+        List<RoleDto> dtos = new ArrayList<>(roles.size());
+        for (Role role : roles) {
+            RoleDto dto = new RoleDto();
+            dto.setRoleId(role.getId());
+            dto.setPrivilege(role.getPrivilege().name());
+            
+            if (role.getQuery() != null) {
+                dto.setQueryId(role.getQuery().getId());
+                dto.setQueryName(role.getQuery().getName());
+            }
+            dto.setUserGroupId(role.getUserGroup().getId());
+            dto.setUserGroupName(role.getUserGroup().getName());
+            List<String> members = new ArrayList<>(
+                    role.getUserGroupMembers().size());
+            for (UserGroupMember m : role.getUserGroupMembers()) {
+                members.add(m.getUserId());
+            }
+            dto.setMembers(members);
+            dtos.add(dto);
+        }
+        return dtos;
+    }
+
+}
diff --git a/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java b/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java
index 706cc21..275ee70 100644
--- a/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java
+++ b/src/main/java/de/ids_mannheim/korap/dto/converter/UserGroupConverter.java
@@ -8,6 +8,7 @@
 import org.springframework.stereotype.Component;
 
 import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PrivilegeType;
 import de.ids_mannheim.korap.dto.UserGroupDto;
 import de.ids_mannheim.korap.dto.UserGroupMemberDto;
 import de.ids_mannheim.korap.entity.Role;
@@ -39,7 +40,7 @@
         dto.setUserMemberStatus(userMemberStatus);
 
         if (roleSet != null) {
-            dto.setUserRoles(convertRoleSetToStringList(roleSet));
+            dto.setUserPrivileges(createPrivilegeList(roleSet));
         }
 
         if (members != null) {
@@ -50,8 +51,7 @@
                 UserGroupMemberDto memberDto = new UserGroupMemberDto();
                 memberDto.setUserId(member.getUserId());
                 memberDto.setStatus(member.getStatus());
-                memberDto.setRoles(
-                        convertRoleSetToStringList(member.getRoles()));
+                memberDto.setPrivileges(createPrivilegeList(member.getRoles()));
                 memberDtos.add(memberDto);
             }
             dto.setMembers(memberDtos);
@@ -63,12 +63,12 @@
         return dto;
     }
 
-    private List<String> convertRoleSetToStringList (Set<Role> roleSet) {
-        List<String> roles = new ArrayList<>(roleSet.size());
-        for (Role r : roleSet) {
-            roles.add(r.getName());
+    private List<PrivilegeType> createPrivilegeList (Set<Role> roles) {
+        List<PrivilegeType> privileges = new ArrayList<>(roles.size());
+        for (Role r : roles) {
+            privileges.add(r.getPrivilege());
         }
-        Collections.sort(roles);
-        return roles;
+        Collections.sort(privileges);
+        return privileges;
     }
 }
diff --git a/src/main/java/de/ids_mannheim/korap/entity/Privilege.java b/src/main/java/de/ids_mannheim/korap/entity/Privilege.java
deleted file mode 100644
index 0828595..0000000
--- a/src/main/java/de/ids_mannheim/korap/entity/Privilege.java
+++ /dev/null
@@ -1,49 +0,0 @@
-package de.ids_mannheim.korap.entity;
-
-import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.JoinColumn;
-import jakarta.persistence.ManyToOne;
-import jakarta.persistence.Table;
-
-import de.ids_mannheim.korap.constant.PrivilegeType;
-import lombok.Getter;
-import lombok.Setter;
-
-/**
- * Describes privilege table listing users and their roles.
- * 
- * @author margaretha
- *
- */
-@Getter
-@Setter
-@Entity
-@Table(name = "privilege")
-public class Privilege {
-
-    @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private int id;
-    @Enumerated(EnumType.STRING)
-    private PrivilegeType name;
-    @ManyToOne(fetch = FetchType.LAZY)
-    @JoinColumn(name = "role_id", referencedColumnName = "id")
-    private Role role;
-
-    public Privilege () {}
-
-    public Privilege (PrivilegeType name, Role role) {
-        this.name = name;
-        this.role = role;
-    }
-
-    public String toString () {
-        return "id=" + id + ", name=" + name + ", role=" + role;
-    }
-}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/QueryAccess.java b/src/main/java/de/ids_mannheim/korap/entity/QueryAccess.java
deleted file mode 100644
index 42c7968..0000000
--- a/src/main/java/de/ids_mannheim/korap/entity/QueryAccess.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package de.ids_mannheim.korap.entity;
-
-import jakarta.persistence.Column;
-import jakarta.persistence.Entity;
-import jakarta.persistence.EnumType;
-import jakarta.persistence.Enumerated;
-import jakarta.persistence.FetchType;
-import jakarta.persistence.GeneratedValue;
-import jakarta.persistence.GenerationType;
-import jakarta.persistence.Id;
-import jakarta.persistence.JoinColumn;
-import jakarta.persistence.ManyToOne;
-import jakarta.persistence.Table;
-
-import de.ids_mannheim.korap.constant.QueryAccessStatus;
-import lombok.Getter;
-import lombok.Setter;
-
-/**
- * Describes the relationship between virtual corpora and user groups,
- * i.e. which groups may access which virtual corpora, and the history
- * of group-access management.
- * 
- * @author margaretha
- * @see QueryDO
- * @see UserGroup
- */
-@Setter
-@Getter
-@Entity
-@Table(name = "query_access")
-public class QueryAccess {
-
-    @Id
-    @GeneratedValue(strategy = GenerationType.IDENTITY)
-    private int id;
-    @Column(name = "created_by")
-    private String createdBy;
-    @Column(name = "approved_by")
-    private String approvedBy;
-    @Column(name = "deleted_by")
-    private String deletedBy;
-
-    @Enumerated(EnumType.STRING)
-    private QueryAccessStatus status;
-
-    @ManyToOne(fetch = FetchType.EAGER)
-    @JoinColumn(name = "query_id", referencedColumnName = "id")
-    private QueryDO query;
-
-    @ManyToOne(fetch = FetchType.EAGER)
-    @JoinColumn(name = "user_group_id", referencedColumnName = "id")
-    private UserGroup userGroup;
-
-    @Override
-    public String toString () {
-        return "id=" + id + ", query= " + query + ", userGroup= " + userGroup;
-    }
-}
diff --git a/src/main/java/de/ids_mannheim/korap/entity/QueryDO.java b/src/main/java/de/ids_mannheim/korap/entity/QueryDO.java
index 77d8102..6b39bd4 100644
--- a/src/main/java/de/ids_mannheim/korap/entity/QueryDO.java
+++ b/src/main/java/de/ids_mannheim/korap/entity/QueryDO.java
@@ -64,8 +64,9 @@
     @Column(name = "query_language")
     private String queryLanguage;
 
-    @OneToMany(mappedBy = "query", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
-    private List<QueryAccess> queryAccess;
+    @OneToMany(mappedBy = "query", fetch = FetchType.LAZY, 
+            cascade = CascadeType.REMOVE)
+    private List<Role> roles;
 
     @Override
     public String toString () {
diff --git a/src/main/java/de/ids_mannheim/korap/entity/Role.java b/src/main/java/de/ids_mannheim/korap/entity/Role.java
index f096c80..dba9d3c 100644
--- a/src/main/java/de/ids_mannheim/korap/entity/Role.java
+++ b/src/main/java/de/ids_mannheim/korap/entity/Role.java
@@ -2,17 +2,20 @@
 
 import java.util.List;
 
-import jakarta.persistence.CascadeType;
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.PrivilegeType;
 import jakarta.persistence.Column;
 import jakarta.persistence.Entity;
+import jakarta.persistence.EnumType;
+import jakarta.persistence.Enumerated;
 import jakarta.persistence.FetchType;
 import jakarta.persistence.GeneratedValue;
 import jakarta.persistence.GenerationType;
 import jakarta.persistence.Id;
+import jakarta.persistence.JoinColumn;
 import jakarta.persistence.ManyToMany;
-import jakarta.persistence.OneToMany;
+import jakarta.persistence.ManyToOne;
 import jakarta.persistence.Table;
-
 import lombok.Getter;
 import lombok.Setter;
 
@@ -32,16 +35,55 @@
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     private int id;
     @Column(unique = true)
-    private String name;
+    @Enumerated(EnumType.STRING)
+    private PredefinedRole name;
+    @Enumerated(EnumType.STRING)
+    private PrivilegeType privilege;
+    
+    @ManyToOne(fetch = FetchType.EAGER)
+    @JoinColumn(name = "query_id", referencedColumnName = "id")
+    private QueryDO query;
 
-    @ManyToMany(mappedBy = "roles", fetch = FetchType.LAZY)
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "group_id", referencedColumnName = "id")
+    private UserGroup userGroup;
+    
+//    @ManyToMany(fetch = FetchType.LAZY)
+//    @JoinTable(
+//        name = "role_user_roles",
+//        joinColumns = @JoinColumn(name = "role_id"),
+//        inverseJoinColumns = @JoinColumn(name = "user_role_id")
+//    )
+//    private Set<UserRole> user_roles;
+    
+    @ManyToMany(mappedBy = "roles", fetch = FetchType.EAGER)
     private List<UserGroupMember> userGroupMembers;
-
-    @OneToMany(mappedBy = "role", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
-    private List<Privilege> privileges;
+//
+//    @OneToMany(mappedBy = "role", fetch = FetchType.EAGER, cascade = CascadeType.REMOVE)
+//    private List<Privilege> privileges;
+    
+    public Role () {}
+    
+    public Role (PredefinedRole name, PrivilegeType privilege, UserGroup group) {
+        setName(name);
+        setPrivilege(privilege);
+        setUserGroup(group);
+    }
+    
+    public Role (PredefinedRole name, PrivilegeType privilege, UserGroup group,
+                 QueryDO query) {
+        setName(name);
+        setPrivilege(privilege);
+        setUserGroup(group);
+        setQuery(query);
+    }
 
     public String toString () {
-        return "id=" + id + ", name=" + name;
+        return "id=" + id + ", name=" + name + ", privilege=" + privilege
+                + ", usergroup=" + userGroup.getId() 
+//                + ", members=" + userGroupMembers 
+                + ", query=" + ((query!=null) ? query.getId() : query)
+                ;
     }
 
     @Override
@@ -58,7 +100,20 @@
     @Override
     public boolean equals (Object obj) {
         Role r = (Role) obj;
-        if (this.id == r.getId() && this.name.equals(r.getName())) {
+        if (this.name.equals(r.getName())
+                && this.privilege.equals(r.getPrivilege())
+                && this.userGroup.equals(r.getUserGroup())) {
+            if (this.query != null && r.getQuery() == null) {
+                return false;
+            }
+            if (this.query == null && r.getQuery() != null) {
+                return false;
+            }
+            if(this.query != null && r.getQuery() != null
+                    && !this.query.equals(r.getQuery())) {
+                return false;
+            }
+
             return true;
         }
         return false;
diff --git a/src/main/java/de/ids_mannheim/korap/entity/UserGroup.java b/src/main/java/de/ids_mannheim/korap/entity/UserGroup.java
index 9a7db7d..4bb17fe 100644
--- a/src/main/java/de/ids_mannheim/korap/entity/UserGroup.java
+++ b/src/main/java/de/ids_mannheim/korap/entity/UserGroup.java
@@ -1,5 +1,6 @@
 package de.ids_mannheim.korap.entity;
 
+import java.time.ZonedDateTime;
 import java.util.List;
 
 import jakarta.persistence.CascadeType;
@@ -46,15 +47,19 @@
     private String createdBy;
     @Column(name = "deleted_by")
     private String deletedBy;
+    @Column(name = "created_date")
+    private ZonedDateTime createdDate;
 
     @Enumerated(EnumType.STRING)
     private UserGroupStatus status;
 
-    @OneToMany(mappedBy = "group", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
+    @OneToMany(mappedBy = "group", fetch = FetchType.LAZY, 
+            cascade = CascadeType.REMOVE)
     private List<UserGroupMember> members;
 
-    @OneToMany(mappedBy = "userGroup", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
-    private List<QueryAccess> queryAccess;
+    @OneToMany(mappedBy = "userGroup", fetch = FetchType.LAZY, 
+            cascade = CascadeType.REMOVE)
+    private List<Role> roles;
 
     @Override
     public String toString () {
diff --git a/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java b/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
index 94778c6..5c1e3fd 100644
--- a/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
+++ b/src/main/java/de/ids_mannheim/korap/exceptions/StatusCodes.java
@@ -81,16 +81,18 @@
     // policy errors
 
     // database codes
-    public static final int DB_GET_FAILED = 500;
+//    public static final int DB_GET_FAILED = 500;
     public static final int DB_INSERT_FAILED = 501;
-    public static final int DB_DELETE_FAILED = 502;
-    public static final int DB_UPDATE_FAILED = 503;
+//    public static final int DB_DELETE_FAILED = 502;
+//    public static final int DB_UPDATE_FAILED = 503;
 
-    public static final int DB_GET_SUCCESSFUL = 504;
-    public static final int DB_INSERT_SUCCESSFUL = 505;
-    public static final int DB_DELETE_SUCCESSFUL = 506;
-    public static final int DB_UPDATE_SUCCESSFUL = 507;
-    public static final int DB_ENTRY_EXISTS = 508;
+//    public static final int DB_GET_SUCCESSFUL = 504;
+//    public static final int DB_INSERT_SUCCESSFUL = 505;
+//    public static final int DB_DELETE_SUCCESSFUL = 506;
+//    public static final int DB_UPDATE_SUCCESSFUL = 507;
+//    public static final int DB_ENTRY_EXISTS = 508;
+    
+    public static final int DB_UNIQUE_CONSTRAINT_FAILED = 509;
 
     //    public static final int ARGUMENT_VALIDATION_FAILURE = 700;
     // public static final int ARGUMENT_VALIDATION_FAILURE = 701;
@@ -119,6 +121,7 @@
     public static final int GROUP_MEMBER_NOT_FOUND = 1604;
     public static final int INVITATION_EXPIRED = 1605;
     public static final int GROUP_DELETED = 1606;
+    public static final int GROUP_ADMIN_EXISTS = 1607;
 
     /**
      * 1800 Oauth2
diff --git a/src/main/java/de/ids_mannheim/korap/service/QueryService.java b/src/main/java/de/ids_mannheim/korap/service/QueryService.java
index 7e45d30..c04441f 100644
--- a/src/main/java/de/ids_mannheim/korap/service/QueryService.java
+++ b/src/main/java/de/ids_mannheim/korap/service/QueryService.java
@@ -3,8 +3,10 @@
 import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
+import java.util.Set;
 import java.util.regex.Pattern;
 
 import org.apache.logging.log4j.LogManager;
@@ -18,18 +20,21 @@
 import de.ids_mannheim.korap.cache.VirtualCorpusCache;
 import de.ids_mannheim.korap.config.FullConfiguration;
 import de.ids_mannheim.korap.constant.GroupMemberStatus;
-import de.ids_mannheim.korap.constant.QueryAccessStatus;
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.PrivilegeType;
 import de.ids_mannheim.korap.constant.QueryType;
 import de.ids_mannheim.korap.constant.ResourceType;
 import de.ids_mannheim.korap.dao.AdminDao;
-import de.ids_mannheim.korap.dao.QueryAccessDao;
 import de.ids_mannheim.korap.dao.QueryDao;
-import de.ids_mannheim.korap.dto.QueryAccessDto;
+import de.ids_mannheim.korap.dao.RoleDao;
+import de.ids_mannheim.korap.dao.UserGroupDao;
+import de.ids_mannheim.korap.dao.UserGroupMemberDao;
+import de.ids_mannheim.korap.dto.RoleDto;
 import de.ids_mannheim.korap.dto.QueryDto;
-import de.ids_mannheim.korap.dto.converter.QueryAccessConverter;
+import de.ids_mannheim.korap.dto.converter.RoleConverter;
 import de.ids_mannheim.korap.dto.converter.QueryConverter;
-import de.ids_mannheim.korap.entity.QueryAccess;
 import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.entity.Role;
 import de.ids_mannheim.korap.entity.UserGroup;
 import de.ids_mannheim.korap.entity.UserGroupMember;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
@@ -43,6 +48,7 @@
 import de.ids_mannheim.korap.web.controller.QueryReferenceController;
 import de.ids_mannheim.korap.web.controller.VirtualCorpusController;
 import de.ids_mannheim.korap.web.input.QueryJson;
+import jakarta.persistence.NoResultException;
 import jakarta.ws.rs.core.Response.Status;
 
 /**
@@ -74,7 +80,12 @@
     @Autowired
     private QueryDao queryDao;
     @Autowired
-    private QueryAccessDao accessDao;
+    private RoleDao roleDao;
+    @Autowired
+    private UserGroupDao userGroupDao;
+    @Autowired
+    private UserGroupMemberDao memberDao;
+
     @Autowired
     private AdminDao adminDao;
     @Autowired
@@ -86,7 +97,7 @@
     @Autowired
     private QueryConverter converter;
     @Autowired
-    private QueryAccessConverter accessConverter;
+    private RoleConverter roleConverter;
 
     private void verifyUsername (String contextUsername, String pathUsername)
             throws KustvaktException {
@@ -145,7 +156,7 @@
         return dtos;
     }
 
-    public void deleteQueryByName (String username, String queryName,
+    public void deleteQueryByName (String deletedBy, String queryName,
             String createdBy, QueryType type) throws KustvaktException {
 
         QueryDO query = queryDao.retrieveQueryByName(queryName, createdBy);
@@ -155,15 +166,13 @@
             throw new KustvaktException(StatusCodes.NO_RESOURCE_FOUND,
                     "Query " + code + " is not found.", String.valueOf(code));
         }
-        else if (query.getCreatedBy().equals(username)
-                || adminDao.isAdmin(username)) {
+        else if (query.getCreatedBy().equals(deletedBy)
+                || adminDao.isAdmin(deletedBy)) {
 
             if (query.getType().equals(ResourceType.PUBLISHED)) {
-                QueryAccess access = accessDao
-                        .retrieveHiddenAccess(query.getId());
-                accessDao.deleteAccess(access, "system");
-                userGroupService.deleteAutoHiddenGroup(
-                        access.getUserGroup().getId(), "system");
+                UserGroup group = userGroupDao
+                        .retrieveHiddenGroupByQueryName(queryName);
+                userGroupDao.deleteGroup(group.getId(), deletedBy, false);
             }
             if (type.equals(QueryType.VIRTUAL_CORPUS)
                     && VirtualCorpusCache.contains(queryName)) {
@@ -173,7 +182,7 @@
         }
         else {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
-                    "Unauthorized operation for user: " + username, username);
+                    "Unauthorized operation for user: " + deletedBy, deletedBy);
         }
     }
 
@@ -222,11 +231,10 @@
             if (existingQuery.getType().equals(ResourceType.PUBLISHED)) {
                 // withdraw from publication
                 if (!type.equals(ResourceType.PUBLISHED)) {
-                    QueryAccess hiddenAccess = accessDao
-                            .retrieveHiddenAccess(existingQuery.getId());
-                    deleteQueryAccess(hiddenAccess.getId(), "system");
-                    int groupId = hiddenAccess.getUserGroup().getId();
-                    userGroupService.deleteAutoHiddenGroup(groupId, "system");
+                    UserGroup group = userGroupDao
+                            .retrieveHiddenGroupByQueryName(queryName);
+                    int groupId = group.getId();
+                    userGroupDao.deleteGroup(groupId, username, false);
                     // EM: should the users within the hidden group
                     // receive
                     // notifications?
@@ -234,7 +242,7 @@
                 // else remains the same
             }
             else if (type.equals(ResourceType.PUBLISHED)) {
-                publishQuery(existingQuery.getId());
+                publishQuery(existingQuery.getId(), username, queryName);
             }
         }
 
@@ -244,24 +252,26 @@
                 queryLanguage);
     }
 
-    private void publishQuery (int queryId) throws KustvaktException {
+    private void publishQuery (int queryId, String queryCreator,
+            String queryName) throws KustvaktException {
 
-        QueryAccess access = accessDao.retrieveHiddenAccess(queryId);
+//        QueryAccess access = accessDao.retrieveHiddenAccess(queryId);
         // check if hidden access exists
-        if (access == null) {
+//        if (access == null) {
             QueryDO query = queryDao.retrieveQueryById(queryId);
             // create and assign a new hidden group
-            int groupId = userGroupService.createAutoHiddenGroup();
+            int groupId = userGroupService.createAutoHiddenGroup(queryCreator,
+                    queryName);
             UserGroup autoHidden = userGroupService
                     .retrieveUserGroupById(groupId);
-            accessDao.createAccessToQuery(query, autoHidden, "system",
-                    QueryAccessStatus.HIDDEN);
-        }
-        else {
-            // should not happened
-            jlog.error("Cannot publish query with id: " + queryId
-                    + ". Hidden access exists! Access id: " + access.getId());
-        }
+//            accessDao.createAccessToQuery(query, autoHidden);
+            addRoleToQuery(query, autoHidden);
+//        }
+//        else {
+//            // should not happened
+//            jlog.error("Cannot publish query with id: " + queryId
+//                    + ". Hidden access exists! Access id: " + access.getId());
+//        }
     }
 
     public void storeQuery (QueryJson query, String queryName,
@@ -384,7 +394,7 @@
                     cause.getMessage());
         }
         if (type.equals(ResourceType.PUBLISHED)) {
-            publishQuery(queryId);
+            publishQuery(queryId, queryCreator, queryName);
         }
     }
 
@@ -476,153 +486,125 @@
         UserGroup userGroup = userGroupService
                 .retrieveUserGroupByName(groupName);
 
-        if (!isQueryAccessAdmin(userGroup, username)
+        if (!userGroupService.isUserGroupAdmin(username,userGroup)
                 && !adminDao.isAdmin(username)) {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
                     "Unauthorized operation for user: " + username, username);
         }
         else {
             try {
-                accessDao.createAccessToQuery(query, userGroup, username,
-                        QueryAccessStatus.ACTIVE);
+                addRoleToQuery(query, userGroup);
             }
             catch (Exception e) {
                 Throwable cause = e;
                 Throwable lastCause = null;
                 while ((cause = cause.getCause()) != null
                         && !cause.equals(lastCause)) {
-                    if (cause instanceof SQLException) {
-                        break;
-                    }
+//                    if (cause instanceof SQLException) {
+//                        break;
+//                    }
                     lastCause = cause;
                 }
-                throw new KustvaktException(StatusCodes.DB_INSERT_FAILED,
-                        cause.getMessage());
+                throw new KustvaktException(
+                        StatusCodes.DB_UNIQUE_CONSTRAINT_FAILED,
+                        lastCause.getMessage());
             }
 
-            queryDao.editQuery(query, null, ResourceType.PROJECT, null, null,
+            ResourceType queryType = query.getType();
+            if(queryType.equals(ResourceType.PRIVATE)) {
+                queryType = ResourceType.PROJECT;
+            }
+                
+            queryDao.editQuery(query, null, queryType, null, null,
                     null, null, null, query.isCached(), null, null);
         }
     }
-
-    private boolean isQueryAccessAdmin (UserGroup userGroup, String username)
+    
+    public void addRoleToQuery (QueryDO query, UserGroup userGroup)
             throws KustvaktException {
-        List<UserGroupMember> accessAdmins = userGroupService
-                .retrieveQueryAccessAdmins(userGroup);
-        for (UserGroupMember m : accessAdmins) {
-            if (username.equals(m.getUserId())) {
-                return true;
-            }
+    
+        List<UserGroupMember> members = memberDao
+                .retrieveMemberByGroupId(userGroup.getId());
+
+        Role r1 = new Role(PredefinedRole.QUERY_ACCESS,
+                PrivilegeType.READ_QUERY, userGroup, query);
+        roleDao.addRole(r1);
+        
+        for (UserGroupMember member : members) {
+            member.getRoles().add(r1);
+            memberDao.updateMember(member);
         }
-        return false;
     }
 
-    // public void editVCAccess (VirtualCorpusAccess access, String
-    // username)
-    // throws KustvaktException {
-    //
-    // // get all the VCA admins
-    // UserGroup userGroup = access.getUserGroup();
-    // List<UserGroupMember> accessAdmins =
-    // userGroupService.retrieveVCAccessAdmins(userGroup);
-    //
-    // User user = authManager.getUser(username);
-    // if (!user.isSystemAdmin()) {
-    // throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
-    // "Unauthorized operation for user: " + username, username);
-    // }
-    // }
+//    public List<QueryAccessDto> listQueryAccessByUsername (String username)
+//            throws KustvaktException {
+//        List<QueryAccess> accessList = new ArrayList<>();
+//        if (adminDao.isAdmin(username)) {
+//            accessList = accessDao.retrieveAllAccess();
+//        }
+//        else {
+//            List<UserGroup> groups = userGroupService
+//                    .retrieveUserGroup(username);
+//            for (UserGroup g : groups) {
+//                if (userGroupService.isUserGroupAdmin(username, g)) {
+//                    accessList.addAll(
+//                            accessDao.retrieveActiveAccessByGroup(g.getId()));
+//                }
+//            }
+//        }
+//        return accessConverter.createQueryAccessDto(accessList);
+//    }
+//
+//    public List<QueryAccessDto> listQueryAccessByQuery (String username,
+//            String queryCreator, String queryName) throws KustvaktException {
+//
+//        List<QueryAccess> accessList;
+//        if (adminDao.isAdmin(username)) {
+//            accessList = accessDao.retrieveAllAccessByQuery(queryCreator,
+//                    queryName);
+//        }
+//        else {
+//            accessList = accessDao.retrieveActiveAccessByQuery(queryCreator,
+//                    queryName);
+//            List<QueryAccess> filteredAccessList = new ArrayList<>();
+//            for (QueryAccess access : accessList) {
+//                UserGroup userGroup = access.getUserGroup();
+//                if (userGroupService.isUserGroupAdmin(username, userGroup)) {
+//                    filteredAccessList.add(access);
+//                }
+//            }
+//            accessList = filteredAccessList;
+//        }
+//        return accessConverter.createQueryAccessDto(accessList);
+//    }
 
-    public List<QueryAccessDto> listQueryAccessByUsername (String username)
-            throws KustvaktException {
-        List<QueryAccess> accessList = new ArrayList<>();
-        if (adminDao.isAdmin(username)) {
-            accessList = accessDao.retrieveAllAccess();
-        }
-        else {
-            List<UserGroup> groups = userGroupService
-                    .retrieveUserGroup(username);
-            for (UserGroup g : groups) {
-                if (isQueryAccessAdmin(g, username)) {
-                    accessList.addAll(
-                            accessDao.retrieveActiveAccessByGroup(g.getId()));
-                }
-            }
-        }
-        return accessConverter.createQueryAccessDto(accessList);
-    }
-
-    public List<QueryAccessDto> listQueryAccessByQuery (String username,
-            String queryCreator, String queryName) throws KustvaktException {
-
-        List<QueryAccess> accessList;
-        if (adminDao.isAdmin(username)) {
-            accessList = accessDao.retrieveAllAccessByQuery(queryCreator,
-                    queryName);
-        }
-        else {
-            accessList = accessDao.retrieveActiveAccessByQuery(queryCreator,
-                    queryName);
-            List<QueryAccess> filteredAccessList = new ArrayList<>();
-            for (QueryAccess access : accessList) {
-                UserGroup userGroup = access.getUserGroup();
-                if (isQueryAccessAdmin(userGroup, username)) {
-                    filteredAccessList.add(access);
-                }
-            }
-            accessList = filteredAccessList;
-        }
-        return accessConverter.createQueryAccessDto(accessList);
-    }
-
-    @Deprecated
-    public List<QueryAccessDto> listVCAccessByGroup (String username,
-            int groupId) throws KustvaktException {
-        UserGroup userGroup = userGroupService.retrieveUserGroupById(groupId);
-
-        List<QueryAccess> accessList;
-        if (adminDao.isAdmin(username)) {
-            accessList = accessDao.retrieveAllAccessByGroup(groupId);
-        }
-        else if (isQueryAccessAdmin(userGroup, username)) {
-            accessList = accessDao.retrieveActiveAccessByGroup(groupId);
-        }
-        else {
-            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
-                    "Unauthorized operation for user: " + username, username);
-        }
-
-        return accessConverter.createQueryAccessDto(accessList);
-    }
-
-    public List<QueryAccessDto> listQueryAccessByGroup (String username,
-            String groupName) throws KustvaktException {
+    public List<RoleDto> listRolesByGroup (String username,
+            String groupName, boolean hasQuery) throws KustvaktException {
         UserGroup userGroup = userGroupService
                 .retrieveUserGroupByName(groupName);
 
-        List<QueryAccess> accessList;
-        if (adminDao.isAdmin(username)) {
-            accessList = accessDao.retrieveAllAccessByGroup(userGroup.getId());
-        }
-        else if (isQueryAccessAdmin(userGroup, username)) {
-            accessList = accessDao
-                    .retrieveActiveAccessByGroup(userGroup.getId());
+        Set<Role> roles;
+        if (adminDao.isAdmin(username)
+                || userGroupService.isUserGroupAdmin(username, userGroup)) {
+            roles = roleDao.retrieveRoleByGroupId(userGroup.getId(), hasQuery);
+
         }
         else {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
                     "Unauthorized operation for user: " + username, username);
         }
-        return accessConverter.createQueryAccessDto(accessList);
+        return roleConverter.createRoleDto(roles);
     }
 
-    public void deleteQueryAccess (int accessId, String username)
+    @Deprecated
+    public void deleteRoleById (int roleId, String username)
             throws KustvaktException {
 
-        QueryAccess access = accessDao.retrieveAccessById(accessId);
-        UserGroup userGroup = access.getUserGroup();
-        if (isQueryAccessAdmin(userGroup, username)
+        Role role = roleDao.retrieveRoleById(roleId);
+        UserGroup userGroup = role.getUserGroup();
+        if (userGroupService.isUserGroupAdmin(username, userGroup)
                 || adminDao.isAdmin(username)) {
-            accessDao.deleteAccess(access, username);
+            roleDao.deleteRole(roleId);
         }
         else {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
@@ -630,6 +612,23 @@
         }
 
     }
+    
+    public void deleteRoleByGroupAndQuery (String groupName,
+            String queryCreator, String queryName, String deleteBy)
+            throws KustvaktException {
+        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName,
+                false);
+        if (userGroupService.isUserGroupAdmin(deleteBy, userGroup)
+                || adminDao.isAdmin(deleteBy)) {
+            roleDao.deleteRoleByGroupAndQuery(groupName, queryCreator,
+                    queryName);
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + deleteBy, deleteBy);
+        }
+
+    }
 
     public JsonNode retrieveKoralQuery (String username, String queryName,
             String createdBy, QueryType queryType) throws KustvaktException {
@@ -713,7 +712,7 @@
                 && !username.equals(query.getCreatedBy())) {
             if (type.equals(ResourceType.PRIVATE)
                     || (type.equals(ResourceType.PROJECT)
-                            && !hasAccess(username, query.getId()))) {
+                            && !hasReadAccess(username, query.getId()))) {
                 throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
                         "Unauthorized operation for user: " + username,
                         username);
@@ -723,12 +722,29 @@
                     && !username.equals("guest")) {
                 // add user in the query's auto group
                 UserGroup userGroup = userGroupService
-                        .retrieveHiddenUserGroupByQuery(query.getId());
+                        .retrieveHiddenUserGroupByQueryId(query.getId());
                 try {
+                    
+                    Role r1= roleDao.retrieveRoleByGroupIdQueryIdPrivilege(
+                            userGroup.getId(),query.getId(),
+                            PrivilegeType.READ_QUERY);
+                    Set<Role> memberRoles = new HashSet<Role>();
+                    memberRoles.add(r1);
+                    
                     userGroupService.addGroupMember(username, userGroup,
-                            "system", GroupMemberStatus.ACTIVE);
+                            "system", GroupMemberStatus.ACTIVE, memberRoles);    
                     // member roles are not set (not necessary)
                 }
+                catch (NoResultException ne) {
+                    Role r1 = new Role(PredefinedRole.QUERY_ACCESS,
+                            PrivilegeType.READ_QUERY, userGroup);
+                    roleDao.addRole(r1);
+                    Set<Role> memberRoles = new HashSet<Role>();
+                    memberRoles.add(r1);
+                    
+                    userGroupService.addGroupMember(username, userGroup,
+                            "system", GroupMemberStatus.ACTIVE, memberRoles);                
+                }
                 catch (KustvaktException e) {
                     // member exists
                     // skip adding user to hidden group
@@ -738,16 +754,13 @@
         }
     }
 
-    private boolean hasAccess (String username, int queryId)
+    private boolean hasReadAccess (String username, int queryId)
             throws KustvaktException {
-        UserGroup userGroup;
-        List<QueryAccess> accessList = accessDao
-                .retrieveActiveAccessByQuery(queryId);
-        for (QueryAccess access : accessList) {
-            userGroup = access.getUserGroup();
-            if (userGroupService.isMember(username, userGroup)) {
+        Set<Role> roles = roleDao.retrieveRoleByQueryIdAndUsername(queryId,
+                username);
+        for (Role r :roles) {
+            if (r.getPrivilege().equals(PrivilegeType.READ_QUERY))
                 return true;
-            }
         }
         return false;
     }
diff --git a/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java b/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
index 22fe070..52c79b9 100644
--- a/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
+++ b/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
@@ -18,6 +18,7 @@
 import de.ids_mannheim.korap.config.FullConfiguration;
 import de.ids_mannheim.korap.constant.GroupMemberStatus;
 import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.PrivilegeType;
 import de.ids_mannheim.korap.constant.UserGroupStatus;
 import de.ids_mannheim.korap.dao.AdminDao;
 import de.ids_mannheim.korap.dao.RoleDao;
@@ -69,10 +70,8 @@
     @Autowired
     private RandomCodeGenerator random;
 
-    private static Set<Role> memberRoles;
-
     /**
-     * Only users with {@link PredefinedRole#USER_GROUP_ADMIN}
+     * Only users with {@link PredefinedRole#GROUP_ADMIN}
      * are allowed to see the members of the group.
      * 
      * @param username
@@ -115,7 +114,7 @@
     private List<UserGroupMember> retrieveMembers (int groupId, String username)
             throws KustvaktException {
         List<UserGroupMember> groupAdmins = groupMemberDao.retrieveMemberByRole(
-                groupId, PredefinedRole.USER_GROUP_ADMIN.getId());
+                groupId, PredefinedRole.GROUP_ADMIN);
 
         List<UserGroupMember> members = null;
         for (UserGroupMember admin : groupAdmins) {
@@ -138,9 +137,18 @@
         return userGroupDao.retrieveGroupByName(groupName, false);
     }
 
-    public UserGroup retrieveHiddenUserGroupByQuery (int queryId)
+    public UserGroup retrieveHiddenUserGroupByQueryId (int queryId)
             throws KustvaktException {
-        return userGroupDao.retrieveHiddenGroupByQuery(queryId);
+        return userGroupDao.retrieveHiddenGroupByQueryId(queryId);
+    }
+    
+    public UserGroupDto retrieveHiddenUserGroupByQueryName (String queryName)
+            throws KustvaktException {
+        UserGroup group = userGroupDao
+                .retrieveHiddenGroupByQueryName(queryName);
+        List<UserGroupMember> members = groupMemberDao
+                .retrieveMemberByGroupId(group.getId());
+        return converter.createUserGroupDto(group, members, null, null);
     }
 
     public List<UserGroupDto> retrieveUserGroupByStatus (String username,
@@ -161,33 +169,31 @@
         }
         return dtos;
     }
-
-    public List<UserGroupMember> retrieveQueryAccessAdmins (UserGroup userGroup)
-            throws KustvaktException {
-        List<UserGroupMember> groupAdmins = groupMemberDao.retrieveMemberByRole(
-                userGroup.getId(), PredefinedRole.VC_ACCESS_ADMIN.getId());
-        return groupAdmins;
-    }
-
-    private void setMemberRoles () {
-        if (memberRoles == null) {
-            memberRoles = new HashSet<Role>(2);
-            memberRoles.add(roleDao.retrieveRoleById(
-                    PredefinedRole.USER_GROUP_MEMBER.getId()));
-            memberRoles.add(roleDao
-                    .retrieveRoleById(PredefinedRole.VC_ACCESS_MEMBER.getId()));
-        }
+    
+    private Set<Role> prepareMemberRoles (UserGroup userGroup) {
+            Role r1 = new Role(PredefinedRole.GROUP_MEMBER,
+                    PrivilegeType.DELETE_MEMBER, userGroup);
+            roleDao.addRole(r1);
+            Set<Role>memberRoles = new HashSet<Role>();
+            memberRoles.add(r1);
+            
+            Set<Role> roles = 
+                    roleDao.retrieveRolesByGroupIdWithUniqueQuery(userGroup.getId());
+            for(Role r :roles) {
+                memberRoles.add(r);
+            }
+            return memberRoles;
     }
 
     /**
      * Group owner is automatically added when creating a group.
      * Do not include owners in group members.
      * 
-     * {@link PredefinedRole#USER_GROUP_MEMBER} and
+     * {@link PredefinedRole#GROUP_MEMBER} and
      * {@link PredefinedRole#VC_ACCESS_MEMBER} roles are
      * automatically assigned to each group member.
      * 
-     * {@link PredefinedRole#USER_GROUP_MEMBER} restrict users
+     * {@link PredefinedRole#GROUP_MEMBER} restrict users
      * to see other group members and allow users to remove
      * themselves from the groups.
      * 
@@ -218,7 +224,7 @@
         UserGroup userGroup = null;
         boolean groupExists = false;
         try {
-            userGroup = userGroupDao.retrieveGroupByName(groupName, false);
+            userGroup = retrieveUserGroupByName(groupName);
             groupExists = true;
         }
         catch (KustvaktException e) {
@@ -231,7 +237,7 @@
             try {
                 userGroupDao.createGroup(groupName, description, createdBy,
                         UserGroupStatus.ACTIVE);
-                userGroup = userGroupDao.retrieveGroupByName(groupName, false);
+                userGroup = retrieveUserGroupByName(groupName);
             }
             // handle DB exceptions, e.g. unique constraint
             catch (Exception e) {
@@ -257,8 +263,7 @@
 
     public void deleteGroup (String groupName, String username)
             throws KustvaktException {
-        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName,
-                false);
+        UserGroup userGroup = retrieveUserGroupByName(groupName);
         if (userGroup.getStatus() == UserGroupStatus.DELETED) {
             // EM: should this be "not found" instead?
             throw new KustvaktException(StatusCodes.GROUP_DELETED,
@@ -277,22 +282,17 @@
         }
     }
 
-    public int createAutoHiddenGroup () throws KustvaktException {
+    public int createAutoHiddenGroup (String queryCreator, String queryName) 
+            throws KustvaktException {
         String code = random.createRandomCode();
         String groupName = "auto-" + code;
-        int groupId = userGroupDao.createGroup(groupName, "auto-hidden-group",
+        int groupId = userGroupDao.createGroup(groupName, "auto-hidden-group for "
+                + "~"+queryCreator+"/"+queryName,
                 "system", UserGroupStatus.HIDDEN);
 
         return groupId;
     }
 
-    public void deleteAutoHiddenGroup (int groupId, String deletedBy)
-            throws KustvaktException {
-        // default hard delete
-        userGroupDao.deleteGroup(groupId, deletedBy,
-                config.isSoftDeleteAutoGroup());
-    }
-
     /**
      * Adds a user to the specified usergroup. If the username with
      * {@link GroupMemberStatus} DELETED exists as a member of the
@@ -332,6 +332,12 @@
     public void addGroupMember (String username, UserGroup userGroup,
             String createdBy, GroupMemberStatus status)
             throws KustvaktException {
+        addGroupMember(username, userGroup, createdBy, status, null);
+    }
+    
+    public void addGroupMember (String username, UserGroup userGroup,
+            String createdBy, GroupMemberStatus status, Set<Role> roles)
+            throws KustvaktException {
         int groupId = userGroup.getId();
         ParameterChecker.checkIntegerValue(groupId, "userGroupId");
 
@@ -350,6 +356,9 @@
         member.setGroup(userGroup);
         member.setStatus(status);
         member.setUserId(username);
+        if (roles !=null) {
+            member.setRoles(roles);
+        }
         groupMemberDao.addMember(member);
     }
 
@@ -402,12 +411,12 @@
         }
     }
 
-    private boolean isUserGroupAdmin (String username, UserGroup userGroup)
+    public boolean isUserGroupAdmin (String username, UserGroup userGroup)
             throws KustvaktException {
 
         List<UserGroupMember> userGroupAdmins = groupMemberDao
                 .retrieveMemberByRole(userGroup.getId(),
-                        PredefinedRole.USER_GROUP_ADMIN.getId());
+                        PredefinedRole.GROUP_ADMIN);
 
         for (UserGroupMember admin : userGroupAdmins) {
             if (username.equals(admin.getUserId())) {
@@ -417,6 +426,18 @@
         return false;
     }
 
+    public boolean isUserGroupAdmin (UserGroupMember member)
+            throws KustvaktException {
+
+        for (Role r : member.getRoles()) {
+            if (r.getName().equals(PredefinedRole.GROUP_ADMIN)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    
     /**
      * Updates the {@link GroupMemberStatus} of a pending member
      * to {@link GroupMemberStatus#ACTIVE} and add default member
@@ -432,10 +453,9 @@
             throws KustvaktException {
 
         ParameterChecker.checkStringValue(username, "userId");
-        ParameterChecker.checkStringValue(groupName, "groupId");
+        ParameterChecker.checkStringValue(groupName, "groupName");
 
-        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName,
-                false);
+        UserGroup userGroup = retrieveUserGroupByName(groupName);
         if (userGroup.getStatus() == UserGroupStatus.DELETED) {
             throw new KustvaktException(StatusCodes.GROUP_DELETED,
                     "Group " + userGroup.getName() + " has been deleted.",
@@ -471,7 +491,7 @@
 
             if (expiration.isAfter(now)) {
                 member.setStatus(GroupMemberStatus.ACTIVE);
-                setMemberRoles();
+                Set<Role> memberRoles = prepareMemberRoles(userGroup);
                 member.setRoles(memberRoles);
                 groupMemberDao.updateMember(member);
             }
@@ -497,8 +517,7 @@
     public void deleteGroupMember (String memberId, String groupName,
             String deletedBy) throws KustvaktException {
 
-        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName,
-                false);
+        UserGroup userGroup = retrieveUserGroupByName(groupName);
         if (userGroup.getStatus() == UserGroupStatus.DELETED) {
             throw new KustvaktException(StatusCodes.GROUP_DELETED,
                     "Group " + userGroup.getName() + " has been deleted.",
@@ -563,10 +582,8 @@
         return groupDto;
     }
 
-    public void editMemberRoles (String username, String groupName,
-            String memberUsername, List<Integer> roleIds)
-            throws KustvaktException {
-
+    public void addAdminRole (String username, String groupName,
+            String memberUsername) throws KustvaktException {
         ParameterChecker.checkStringValue(username, "username");
         ParameterChecker.checkStringValue(groupName, "groupName");
         ParameterChecker.checkStringValue(memberUsername, "memberUsername");
@@ -577,7 +594,8 @@
             throw new KustvaktException(StatusCodes.GROUP_DELETED,
                     "Usergroup has been deleted.");
         }
-        else if (isUserGroupAdmin(username, userGroup)
+
+        if (isUserGroupAdmin(username, userGroup)
                 || adminDao.isAdmin(username)) {
 
             UserGroupMember member = groupMemberDao
@@ -589,12 +607,39 @@
                         memberUsername, member.getStatus().name());
             }
 
-            Set<Role> roles = new HashSet<>();
-            for (int i = 0; i < roleIds.size(); i++) {
-                roles.add(roleDao.retrieveRoleById(roleIds.get(i)));
+            if (!isUserGroupAdmin(memberUsername, userGroup)) {
+                Set<Role> existingRoles = member.getRoles();
+                PredefinedRole role = PredefinedRole.GROUP_ADMIN;
+
+                Role r1 = new Role(role, PrivilegeType.READ_MEMBER, userGroup);
+                roleDao.addRole(r1);
+                existingRoles.add(r1);
+
+                Role r2 = new Role(role, PrivilegeType.DELETE_MEMBER,
+                        userGroup);
+                roleDao.addRole(r2);
+                existingRoles.add(r2);
+
+                Role r3 = new Role(role, PrivilegeType.WRITE_MEMBER, userGroup);
+                roleDao.addRole(r3);
+                existingRoles.add(r3);
+
+                Role r4 = new Role(role, PrivilegeType.SHARE_QUERY, userGroup);
+                roleDao.addRole(r4);
+                existingRoles.add(r4);
+
+                Role r5 = new Role(role, PrivilegeType.DELETE_QUERY, userGroup);
+                roleDao.addRole(r5);
+                existingRoles.add(r5);
+
+                member.setRoles(existingRoles);
+                groupMemberDao.updateMember(member);
             }
-            member.setRoles(roles);
-            groupMemberDao.updateMember(member);
+            else {
+                throw new KustvaktException(StatusCodes.GROUP_ADMIN_EXISTS,
+                        "Username " + memberUsername
+                         + " is already a group admin.");
+            }
 
         }
         else {
@@ -602,49 +647,9 @@
                     "Unauthorized operation for user: " + username, username);
         }
     }
-
-    public void addMemberRoles (String username, String groupName,
-            String memberUsername, List<Integer> roleIds)
-            throws KustvaktException {
-
-        ParameterChecker.checkStringValue(username, "username");
-        ParameterChecker.checkStringValue(groupName, "groupName");
-        ParameterChecker.checkStringValue(memberUsername, "memberUsername");
-
-        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName, true);
-        UserGroupStatus groupStatus = userGroup.getStatus();
-        if (groupStatus == UserGroupStatus.DELETED) {
-            throw new KustvaktException(StatusCodes.GROUP_DELETED,
-                    "Usergroup has been deleted.");
-        }
-        else if (isUserGroupAdmin(username, userGroup)
-                || adminDao.isAdmin(username)) {
-
-            UserGroupMember member = groupMemberDao
-                    .retrieveMemberById(memberUsername, userGroup.getId());
-
-            if (!member.getStatus().equals(GroupMemberStatus.ACTIVE)) {
-                throw new KustvaktException(StatusCodes.GROUP_MEMBER_INACTIVE,
-                        memberUsername + " has status " + member.getStatus(),
-                        memberUsername, member.getStatus().name());
-            }
-
-            Set<Role> roles = member.getRoles();
-            for (int i = 0; i < roleIds.size(); i++) {
-                roles.add(roleDao.retrieveRoleById(roleIds.get(i)));
-            }
-            member.setRoles(roles);
-            groupMemberDao.updateMember(member);
-
-        }
-        else {
-            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
-                    "Unauthorized operation for user: " + username, username);
-        }
-    }
-
+    
     public void deleteMemberRoles (String username, String groupName,
-            String memberUsername, List<Integer> roleIds)
+            String memberUsername, List<PredefinedRole> rolesToBeDeleted)
             throws KustvaktException {
 
         ParameterChecker.checkStringValue(username, "username");
@@ -662,7 +667,7 @@
             Set<Role> roles = member.getRoles();
             Iterator<Role> i = roles.iterator();
             while (i.hasNext()) {
-                if (roleIds.contains(i.next().getId())) {
+                if (rolesToBeDeleted.contains(i.next().getName())) {
                     i.remove();
                 }
             }
@@ -676,4 +681,5 @@
                     "Unauthorized operation for user: " + username, username);
         }
     }
+
 }
diff --git a/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java b/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
index 1118b26..ba8a81e 100644
--- a/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
+++ b/src/main/java/de/ids_mannheim/korap/web/KustvaktResponseHandler.java
@@ -28,6 +28,10 @@
             r = Response.status(Response.Status.BAD_REQUEST)
                     .entity(e.getNotification()).build();
         }
+        else if (e.getStatusCode() == StatusCodes.DB_UNIQUE_CONSTRAINT_FAILED) {
+            r = Response.status(Response.Status.CONFLICT)
+                    .entity(e.getNotification()).build();
+        }
         else if (e.getStatusCode() == StatusCodes.USER_REAUTHENTICATION_REQUIRED
                 || e.getStatusCode() == StatusCodes.AUTHORIZATION_FAILED
                 || e.getStatusCode() >= StatusCodes.AUTHENTICATION_FAILED) {
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupAdminController.java b/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupAdminController.java
index 4ed6266..6af97be 100644
--- a/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupAdminController.java
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupAdminController.java
@@ -59,6 +59,21 @@
             throw kustvaktResponseHandler.throwit(e);
         }
     }
+    
+    @POST
+    @Path("hidden")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public UserGroupDto getHiddenUserGroupForQuery (
+            @FormParam("queryName") String queryName) {
+        try {
+            return service.retrieveHiddenUserGroupByQueryName(queryName);
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+    
+    
 
     /**
      * Retrieves a specific user-group. Only system admins are
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java b/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
index 00cbf9a..33db5f6 100644
--- a/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
@@ -7,6 +7,7 @@
 import org.springframework.stereotype.Controller;
 
 import de.ids_mannheim.korap.constant.OAuth2Scope;
+import de.ids_mannheim.korap.constant.PredefinedRole;
 import de.ids_mannheim.korap.dto.UserGroupDto;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
@@ -221,67 +222,28 @@
         }
     }
 
-    /**
-     * Very similar to addMemberRoles web-service, but allows deletion
-     * as well.
-     * 
-     * @param securityContext
-     * @param groupName
-     *            the group name
-     * @param memberUsername
-     *            the username of a group-member
-     * @param roleId
-     *            a role id or multiple role ids
-     * @return
-     */
-    @POST
-    @Path("@{groupName}/role/edit")
-    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-    public Response editMemberRoles (@Context SecurityContext securityContext,
-            @PathParam("groupName") String groupName,
-            @FormParam("memberUsername") String memberUsername,
-            @FormParam("roleId") List<Integer> roleIds) {
-        TokenContext context = (TokenContext) securityContext
-                .getUserPrincipal();
-        try {
-            scopeService.verifyScope(context,
-                    OAuth2Scope.EDIT_USER_GROUP_MEMBER_ROLE);
-            service.editMemberRoles(context.getUsername(), groupName,
-                    memberUsername, roleIds);
-            return Response.ok("SUCCESS").build();
-        }
-        catch (KustvaktException e) {
-            throw kustvaktResponseHandler.throwit(e);
-        }
-    }
-
-    /**
-     * Adds roles of an active member of a user-group. Only user-group
-     * admins and system admins are allowed.
+    /**Add group admin role to a member in a group 
      * 
      * @param securityContext
      * @param groupName
      *            a group name
      * @param memberUsername
      *            a username of a group member
-     * @param roleId
-     *            a role id or multiple role ids
-     * @return if successful, HTTP response status OK
+     * @return HTTP status 200, if successful 
      */
     @POST
-    @Path("@{groupName}/role/add")
+    @Path("@{groupName}/role/add/admin")
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-    public Response addMemberRoles (@Context SecurityContext securityContext,
+    public Response addAdminRole (@Context SecurityContext securityContext,
             @PathParam("groupName") String groupName,
-            @FormParam("memberUsername") String memberUsername,
-            @FormParam("roleId") List<Integer> roleIds) {
+            @FormParam("memberUsername") String memberUsername) {
         TokenContext context = (TokenContext) securityContext
                 .getUserPrincipal();
         try {
             scopeService.verifyScope(context,
                     OAuth2Scope.ADD_USER_GROUP_MEMBER_ROLE);
-            service.addMemberRoles(context.getUsername(), groupName,
-                    memberUsername, roleIds);
+            service.addAdminRole(context.getUsername(), groupName,
+                    memberUsername);
             return Response.ok("SUCCESS").build();
         }
         catch (KustvaktException e) {
@@ -309,14 +271,14 @@
     public Response deleteMemberRoles (@Context SecurityContext securityContext,
             @PathParam("groupName") String groupName,
             @FormParam("memberUsername") String memberUsername,
-            @FormParam("roleId") List<Integer> roleIds) {
+            @FormParam("role") List<PredefinedRole> roles) {
         TokenContext context = (TokenContext) securityContext
                 .getUserPrincipal();
         try {
             scopeService.verifyScope(context,
                     OAuth2Scope.DELETE_USER_GROUP_MEMBER_ROLE);
             service.deleteMemberRoles(context.getUsername(), groupName,
-                    memberUsername, roleIds);
+                    memberUsername, roles);
             return Response.ok("SUCCESS").build();
         }
         catch (KustvaktException e) {
diff --git a/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java b/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
index 6c91dd6..cb1ceda 100644
--- a/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
@@ -9,7 +9,7 @@
 
 import de.ids_mannheim.korap.constant.OAuth2Scope;
 import de.ids_mannheim.korap.constant.QueryType;
-import de.ids_mannheim.korap.dto.QueryAccessDto;
+import de.ids_mannheim.korap.dto.RoleDto;
 import de.ids_mannheim.korap.dto.QueryDto;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
@@ -27,6 +27,7 @@
 import de.ids_mannheim.korap.web.utils.ResourceFilters;
 import jakarta.ws.rs.Consumes;
 import jakarta.ws.rs.DELETE;
+import jakarta.ws.rs.DefaultValue;
 import jakarta.ws.rs.GET;
 import jakarta.ws.rs.POST;
 import jakarta.ws.rs.PUT;
@@ -44,13 +45,13 @@
  * VirtualCorpusController defines web APIs related to virtual corpus
  * (VC) such as creating, deleting and listing user virtual corpora.
  * All the APIs in this class are available to logged-in users, except
- * retrieving info of system VC.
+ * retrieving a VC info.
  * 
- * This class also includes APIs related to virtual corpus access
- * (VCA) such as sharing and publishing VC. When a VC is published,
- * it is shared with all users, but not always listed like system
- * VC. It is listed for a user, once when he/she have searched for the
- * VC. A VC can be published by creating or editing the VC.
+ * This class also includes web-services to share and publish VC. When
+ * a VC is published, it is shared with all users, but not always listed 
+ * like system VC. It is listed for a user, once when he/she have searched 
+ * for the VC. A VC can be published by creating a new VC with type PUBLISHED
+ * or editing an existing VC.
  * 
  * VC name must follow the following regex [a-zA-Z_0-9-.], other
  * characters are not allowed.
@@ -202,18 +203,17 @@
     }
 
     /**
-     * Lists all virtual corpora available to the user.
-     *
-     * System-admins can list available vc for a specific user by
-     * specifiying the username parameter.
+     * Lists all virtual corpora (VC) available to the authenticated
+     * user including PRIVATE VC created by the user, SYSTEM VC
+     * defined by system-admins, and PROJECT VC available to
+     * user-groups, wherein the user is a member of. The list can be
+     * filtered to show only SYSTEM VC or VC owned by the user.
      * 
-     * Normal users cannot list virtual corpora
-     * available for other users. Thus, username parameter is optional
-     * and must be identical to the authenticated username.
+     * This web-service requires OAuth2 access token with scope:
+     * vc_info.
      * 
      * @param securityContext
-     * @param username
-     *            a username (optional)
+     * @param filter filter the list by system, own, or empty (default)
      * @return a list of virtual corpora
      */
     @GET
@@ -282,9 +282,7 @@
     }
 
     /**
-     * Only the VC owner and system admins can delete VC. VCA admins
-     * can delete VC-accesses e.g. of project VC, but not the VC
-     * themselves.
+     * Group and system admins can delete VC. 
      * 
      * @param securityContext
      * @param createdBy
@@ -313,7 +311,7 @@
 
     /**
      * VC can only be shared with a group, not individuals.
-     * Only VCA admins are allowed to share VC and the VC must have
+     * Only group admins are allowed to share VC and the VC must have
      * been created by themselves.
      * 
      * <br /><br />
@@ -346,6 +344,36 @@
         }
         return Response.ok("SUCCESS").build();
     }
+    
+    /**
+     * Delete all roles for a given group name and vc. Only Group and
+     * system admin are eligible.
+     * 
+     * @param securityContext
+     * @param vcCreator
+     * @param vcName
+     * @param groupName
+     * @return HTTP status 200, if successful
+     */
+    @DELETE
+    @Path("~{vcCreator}/{vcName}/delete/@{groupName}")
+    public Response deleteRoleByGroupAndQuery (
+            @Context SecurityContext securityContext,
+            @PathParam("vcCreator") String vcCreator,
+            @PathParam("vcName") String vcName,
+            @PathParam("groupName") String groupName) {
+        TokenContext context = (TokenContext) securityContext
+                .getUserPrincipal();
+        try {
+            scopeService.verifyScope(context, OAuth2Scope.DELETE_VC_ACCESS);
+            service.deleteRoleByGroupAndQuery(groupName, vcCreator, vcName,
+                    context.getUsername());
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+        return Response.ok().build();
+    }
 
     /**
      * Only VCA Admins and system admins are allowed to delete a
@@ -358,52 +386,46 @@
      * @param accessId
      * @return
      */
+    @Deprecated
     @DELETE
     @Path("access/{accessId}")
-    public Response deleteVCAccessById (
+    public Response deleteAccessById (
             @Context SecurityContext securityContext,
             @PathParam("accessId") int accessId) {
         TokenContext context = (TokenContext) securityContext
                 .getUserPrincipal();
         try {
             scopeService.verifyScope(context, OAuth2Scope.DELETE_VC_ACCESS);
-            service.deleteQueryAccess(accessId, context.getUsername());
+            service.deleteRoleById(accessId, context.getUsername());
         }
         catch (KustvaktException e) {
             throw kustvaktResponseHandler.throwit(e);
         }
         return Response.ok().build();
     }
-
+    
     /**
-     * Lists active VC-accesses available to user.
+     * Lists all member roles in a group.
      * 
-     * Only available to VCA and system admins.
-     * For system admins, list all VCA regardless of status.
+     * Only available to group and system admins.
      * 
      * @param securityContext
      * @return a list of VC accesses
      */
     @GET
     @Path("access")
-    public List<QueryAccessDto> listVCAccesses (
-            @Context SecurityContext securityContext,
-            @QueryParam("groupName") String groupName) {
+    public List<RoleDto> listRoles (@Context SecurityContext securityContext,
+            @QueryParam("groupName") String groupName,
+            @DefaultValue("true") @QueryParam("hasQuery") boolean hasQuery) {
         TokenContext context = (TokenContext) securityContext
                 .getUserPrincipal();
         try {
             scopeService.verifyScope(context, OAuth2Scope.VC_ACCESS_INFO);
-            if (groupName != null && !groupName.isEmpty()) {
-                return service.listQueryAccessByGroup(context.getUsername(),
-                        groupName);
-            }
-            else {
-                return service.listQueryAccessByUsername(context.getUsername());
-            }
+            return service.listRolesByGroup(context.getUsername(), groupName,
+                    hasQuery);
         }
         catch (KustvaktException e) {
             throw kustvaktResponseHandler.throwit(e);
         }
     }
-
 }
diff --git a/src/main/resources/db/predefined/V2.1__insert_predefined_roles.sql b/src/main/resources/db/predefined/V2.1__insert_predefined_roles.sql
index 0c307cb..37e5be3 100644
--- a/src/main/resources/db/predefined/V2.1__insert_predefined_roles.sql
+++ b/src/main/resources/db/predefined/V2.1__insert_predefined_roles.sql
@@ -1,28 +1,28 @@
--- roles
-INSERT INTO role(name) VALUES ("USER_GROUP_ADMIN");
-INSERT INTO role(name) VALUES ("USER_GROUP_MEMBER");
-INSERT INTO role(name) VALUES ("VC_ACCESS_ADMIN");
-INSERT INTO role(name) VALUES ("VC_ACCESS_MEMBER");
-INSERT INTO role(name) VALUES ("QUERY_ACCESS_ADMIN");
-INSERT INTO role(name) VALUES ("QUERY_ACCESS_MEMBER");
+---- roles
+--INSERT INTO role(name) VALUES ("USER_GROUP_ADMIN");
+--INSERT INTO role(name) VALUES ("USER_GROUP_MEMBER");
+--INSERT INTO role(name) VALUES ("VC_ACCESS_ADMIN");
+--INSERT INTO role(name) VALUES ("VC_ACCESS_MEMBER");
+--INSERT INTO role(name) VALUES ("QUERY_ACCESS_ADMIN");
+--INSERT INTO role(name) VALUES ("QUERY_ACCESS_MEMBER");
 
--- privileges
-INSERT INTO privilege(name,role_id)
-	VALUES("READ", 1);
-INSERT INTO privilege(name,role_id)
-	VALUES("WRITE", 1);
-INSERT INTO privilege(name,role_id)
-	VALUES("DELETE", 1);
-	
-INSERT INTO privilege(name,role_id)
-	VALUES("DELETE",2);
-	
-INSERT INTO privilege(name,role_id)
-	VALUES("READ",3);
-INSERT INTO privilege(name,role_id)
-	VALUES("WRITE",3);
-INSERT INTO privilege(name,role_id)
-	VALUES("DELETE",3);
-
-INSERT INTO privilege(name,role_id)
-	VALUES("READ",4);	
\ No newline at end of file
+---- privileges
+--INSERT INTO privilege(name,role_id)
+--	VALUES("READ", 1);
+--INSERT INTO privilege(name,role_id)
+--	VALUES("WRITE", 1);
+--INSERT INTO privilege(name,role_id)
+--	VALUES("DELETE", 1);
+--	
+--INSERT INTO privilege(name,role_id)
+--	VALUES("DELETE",2);
+--	
+--INSERT INTO privilege(name,role_id)
+--	VALUES("READ",3);
+--INSERT INTO privilege(name,role_id)
+--	VALUES("WRITE",3);
+--INSERT INTO privilege(name,role_id)
+--	VALUES("DELETE",3);
+--
+--INSERT INTO privilege(name,role_id)
+--	VALUES("READ",4);	
\ No newline at end of file
diff --git a/src/main/resources/db/sqlite/V1.13__user_group_alteration.sql b/src/main/resources/db/sqlite/V1.13__user_group_alteration.sql
new file mode 100644
index 0000000..f977c4e
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.13__user_group_alteration.sql
@@ -0,0 +1,69 @@
+--DROP INDEX IF EXISTS group_member_role_index;
+--DROP INDEX IF EXISTS user_group_member_index;
+--DROP INDEX IF EXISTS user_group_member_status_index;
+DROP INDEX IF EXISTS role_index;
+
+-- please commented out the triggers in V1.2__triggers.sql later
+--DROP TRIGGER IF EXISTS insert_member_status;
+--DROP TRIGGER IF EXISTS update_member_status;
+--DROP TRIGGER IF EXISTS delete_member;
+
+--ALTER TABLE user_group
+--DROP COLUMN deleted_by;
+--
+ALTER TABLE user_group
+ADD COLUMN created_date TIMESTAMP;
+--
+--ALTER TABLE user_group_member
+--DROP COLUMN created_by;
+--
+--ALTER TABLE user_group_member
+--DROP COLUMN deleted_by;
+--
+--ALTER TABLE user_group_member
+--DROP COLUMN status;
+--
+--ALTER TABLE user_group_member
+--DROP COLUMN status_date;
+
+
+CREATE TABLE IF NOT EXISTS role_new (
+  id INTEGER PRIMARY KEY AUTOINCREMENT,
+  name VARCHAR(100) NOT NULL,
+  privilege VARCHAR(100) NOT NULL,
+  group_id INTEGER,
+  query_id INTEGER,
+  FOREIGN KEY (group_id) 
+  	REFERENCES user_group (id)
+  	ON DELETE CASCADE
+  FOREIGN KEY (query_id) 
+  	REFERENCES query (id)
+  	ON DELETE CASCADE	
+);
+
+INSERT INTO role_new (name, privilege, group_id, query_id)
+  SELECT DISTINCT r.name, p.name, ug.id, qa.query_id
+  FROM user_group ug 
+  JOIN query_access qa ON ug.id=qa.user_group_id
+  JOIN user_group_member ugm ON ugm.group_id = ug.id
+  JOIN group_member_role gmr ON gmr.group_member_id = ugm.id
+  JOIN role r ON gmr.role_id = r.id
+  JOIN privilege p ON p.role_id = r.id;
+
+DROP INDEX IF EXISTS privilege_index;
+DROP INDEX IF EXISTS virtual_corpus_access_unique_index;
+DROP INDEX IF EXISTS virtual_corpus_status_index;
+
+DROP TABLE role;
+
+ALTER TABLE role_new RENAME TO role;
+
+DROP TABLE privilege;
+DROP TABLE query_access;
+
+CREATE UNIQUE INDEX IF NOT EXISTS role_index_null_query
+ON role (name, privilege, group_id)
+WHERE query_id IS 0;
+
+CREATE UNIQUE INDEX IF NOT EXISTS role_index on role(name, 
+  privilege, group_id, query_id);
\ No newline at end of file
diff --git a/src/main/resources/db/test/V3.1__insert_virtual_corpus.sql b/src/main/resources/db/test/V3.1__insert_virtual_corpus.sql
index d9d2c13..8be3e86 100644
--- a/src/main/resources/db/test/V3.1__insert_virtual_corpus.sql
+++ b/src/main/resources/db/test/V3.1__insert_virtual_corpus.sql
@@ -1,63 +1,63 @@
 -- dummy data only for testing
 
 -- user groups
-INSERT INTO user_group(name,status,created_by) 
-	VALUES ("marlin-group","ACTIVE","marlin");
+--INSERT INTO user_group(name,status,created_by,created_date) 
+--	VALUES ("marlin-group","ACTIVE","marlin",CURRENT_TIMESTAMP);
 	
-INSERT INTO user_group(name,status,created_by) 
-	VALUES ("dory-group","ACTIVE","dory");
+--INSERT INTO user_group(name,status,created_by,created_date) 
+--	VALUES ("dory-group","ACTIVE","dory",CURRENT_TIMESTAMP);
 
-INSERT INTO user_group(name,status,created_by) 
-	VALUES ("auto-group","HIDDEN","system");
+INSERT INTO user_group(name,status,created_by,created_date) 
+	VALUES ("auto-group","HIDDEN","system",CURRENT_TIMESTAMP);
 
 --INSERT INTO user_group(name,status,created_by) 
 --	VALUES ("all users","HIDDEN","system");
 
-INSERT INTO user_group(name,status,created_by, deleted_by) 
-	VALUES ("deleted-group","DELETED","dory", "dory");
+--INSERT INTO user_group(name,status,created_by,deleted_by,created_date) 
+--	VALUES ("deleted-group","DELETED","dory", "dory",CURRENT_TIMESTAMP);
 
 
 
 -- user group members
-INSERT INTO user_group_member(user_id, group_id, status, created_by)
-	SELECT "marlin",
-		(SELECT id from user_group where name = "marlin-group"),
-		"ACTIVE","marlin";
-
-INSERT INTO user_group_member(user_id, group_id, status, created_by)
-	SELECT "dory",
-		(SELECT id from user_group where name = "marlin-group"),
-		"ACTIVE","marlin";
-		
-INSERT INTO user_group_member(user_id, group_id, status, created_by)
-	SELECT "dory",
-		(SELECT id from user_group where name = "dory-group"),
-		"ACTIVE","dory";
-
-INSERT INTO user_group_member(user_id, group_id, status, created_by)
-	SELECT "nemo",
-		(SELECT id from user_group where name = "dory-group"),
-		"ACTIVE","dory";
-
-INSERT INTO user_group_member(user_id, group_id, status, created_by)
-	SELECT "marlin",
-		(SELECT id from user_group where name = "dory-group"),
-		"PENDING","dory";
-	
-INSERT INTO user_group_member(user_id, group_id, status, created_by, deleted_by)
-	SELECT "pearl",
-		(SELECT id from user_group where name = "dory-group"),
-		"DELETED","dory", "pearl";
-
-INSERT INTO user_group_member(user_id, group_id, status, created_by)
-	SELECT "pearl",
-		(SELECT id from user_group where name = "auto-group"),
-		"ACTIVE","system";
-
-INSERT INTO user_group_member(user_id, group_id, status, created_by)
-	SELECT "dory",
-		(SELECT id from user_group where name = "deleted-group"),
-		"ACTIVE","dory";
+--INSERT INTO user_group_member(user_id, group_id, status, created_by)
+--	SELECT "marlin",
+--		(SELECT id from user_group where name = "marlin-group"),
+--		"ACTIVE","marlin";
+--
+--INSERT INTO user_group_member(user_id, group_id, status, created_by)
+--	SELECT "dory",
+--		(SELECT id from user_group where name = "marlin-group"),
+--		"ACTIVE","marlin";
+--		
+--INSERT INTO user_group_member(user_id, group_id, status, created_by)
+--	SELECT "dory",
+--		(SELECT id from user_group where name = "dory-group"),
+--		"ACTIVE","dory";
+--
+--INSERT INTO user_group_member(user_id, group_id, status, created_by)
+--	SELECT "nemo",
+--		(SELECT id from user_group where name = "dory-group"),
+--		"ACTIVE","dory";
+--
+--INSERT INTO user_group_member(user_id, group_id, status, created_by)
+--	SELECT "marlin",
+--		(SELECT id from user_group where name = "dory-group"),
+--		"PENDING","dory";
+--	
+--INSERT INTO user_group_member(user_id, group_id, status, created_by, deleted_by)
+--	SELECT "pearl",
+--		(SELECT id from user_group where name = "dory-group"),
+--		"DELETED","dory", "pearl";
+--
+--INSERT INTO user_group_member(user_id, group_id, status, created_by)
+--	SELECT "pearl",
+--		(SELECT id from user_group where name = "auto-group"),
+--		"ACTIVE","system";
+--
+--INSERT INTO user_group_member(user_id, group_id, status, created_by)
+--	SELECT "dory",
+--		(SELECT id from user_group where name = "deleted-group"),
+--		"ACTIVE","dory";
 
 		
 -- virtual corpora
@@ -86,11 +86,11 @@
 	'{"collection":{"@type":"koral:doc","value":"GOE","match":"match:eq","key":"corpusSigle"}}');	
 	
 -- virtual corpus access
-INSERT INTO query_access(query_id, user_group_id, status, created_by) 
-	SELECT 
-		(SELECT id from query where name = "group-vc"), 
-		(SELECT id from user_group where name = "dory-group"), 
-		"ACTIVE", "dory";
+--INSERT INTO query_access(query_id, user_group_id, status, created_by) 
+--	SELECT 
+--		(SELECT id from query where name = "group-vc"), 
+--		(SELECT id from user_group where name = "dory-group"), 
+--		"ACTIVE", "dory";
 
 --INSERT INTO query_access(query_id, user_group_id, status, created_by) 
 --	SELECT 
@@ -98,17 +98,17 @@
 --		(SELECT id from user_group where name = "all users"),
 --		"ACTIVE", "system";
 
-INSERT INTO query_access(query_id, user_group_id, status, created_by) 
-	SELECT 
-		(SELECT id from query where name = "published-vc"),
-		(SELECT id from user_group where name = "marlin-group"),
-		"ACTIVE", "marlin";
+--INSERT INTO query_access(query_id, user_group_id, status, created_by) 
+--	SELECT 
+--		(SELECT id from query where name = "published-vc"),
+--		(SELECT id from user_group where name = "marlin-group"),
+--		"ACTIVE", "marlin";
 
-INSERT INTO query_access(query_id, user_group_id, status, created_by) 
-	SELECT 
-		(SELECT id from query where name = "published-vc"),
-		(SELECT id from user_group where name = "auto-group"),
-		"HIDDEN", "system";
+--INSERT INTO query_access(query_id, user_group_id, status, created_by) 
+--	SELECT 
+--		(SELECT id from query where name = "published-vc"),
+--		(SELECT id from user_group where name = "auto-group"),
+--		"HIDDEN", "system";
 
 	
 -- Summary user VC Lists
diff --git a/src/main/resources/db/test/V3.3__insert_member_roles.sql b/src/main/resources/db/test/V3.3__insert_member_roles.sql
index effbbcb..b1db4b6 100644
--- a/src/main/resources/db/test/V3.3__insert_member_roles.sql
+++ b/src/main/resources/db/test/V3.3__insert_member_roles.sql
@@ -1,52 +1,52 @@
 -- member roles
 
 -- marlin group
-INSERT INTO group_member_role(group_member_id,role_id)
-SELECT
-	(SELECT id FROM user_group_member WHERE user_id="marlin" AND group_id=1),
-	(SELECT id FROM role WHERE name = "USER_GROUP_ADMIN");
-	
-INSERT INTO group_member_role(group_member_id,role_id)
-SELECT
-	(SELECT id FROM user_group_member WHERE user_id="marlin" AND group_id=1),
-	(SELECT id FROM role WHERE name = "VC_ACCESS_ADMIN");
-	
-INSERT INTO group_member_role(group_member_id,role_id)
-SELECT
-	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=1),
-	(SELECT id FROM role WHERE name = "USER_GROUP_ADMIN");
-	
-INSERT INTO group_member_role(group_member_id,role_id)
-SELECT
-	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=1),
-	(SELECT id FROM role WHERE name = "VC_ACCESS_ADMIN");
+--INSERT INTO group_member_role(group_member_id,role_id)
+--SELECT
+--	(SELECT id FROM user_group_member WHERE user_id="marlin" AND group_id=1),
+--	(SELECT id FROM role WHERE name = "USER_GROUP_ADMIN");
+--	
+--INSERT INTO group_member_role(group_member_id,role_id)
+--SELECT
+--	(SELECT id FROM user_group_member WHERE user_id="marlin" AND group_id=1),
+--	(SELECT id FROM role WHERE name = "VC_ACCESS_ADMIN");
+--	
+--INSERT INTO group_member_role(group_member_id,role_id)
+--SELECT
+--	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=1),
+--	(SELECT id FROM role WHERE name = "USER_GROUP_ADMIN");
+--	
+--INSERT INTO group_member_role(group_member_id,role_id)
+--SELECT
+--	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=1),
+--	(SELECT id FROM role WHERE name = "VC_ACCESS_ADMIN");
 	
 	
 -- dory group
-INSERT INTO group_member_role(group_member_id,role_id)
-SELECT
-	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=2),
-	(SELECT id FROM role WHERE name = "USER_GROUP_ADMIN");
-	
-INSERT INTO group_member_role(group_member_id,role_id)
-SELECT
-	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=2),
-	(SELECT id FROM role WHERE name = "VC_ACCESS_ADMIN");
-	
-INSERT INTO group_member_role(group_member_id,role_id)
-SELECT
-	(SELECT id FROM user_group_member WHERE user_id="nemo" AND group_id=2),
-	(SELECT id FROM role WHERE name = "USER_GROUP_MEMBER");
-	
-INSERT INTO group_member_role(group_member_id,role_id)
-SELECT
-	(SELECT id FROM user_group_member WHERE user_id="nemo" AND group_id=2),
-	(SELECT id FROM role WHERE name = "VC_ACCESS_MEMBER");
+--INSERT INTO group_member_role(group_member_id,role_id)
+--SELECT
+--	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=2),
+--	(SELECT id FROM role WHERE name = "USER_GROUP_ADMIN");
+--	
+--INSERT INTO group_member_role(group_member_id,role_id)
+--SELECT
+--	(SELECT id FROM user_group_member WHERE user_id="dory" AND group_id=2),
+--	(SELECT id FROM role WHERE name = "VC_ACCESS_ADMIN");
+--	
+--INSERT INTO group_member_role(group_member_id,role_id)
+--SELECT
+--	(SELECT id FROM user_group_member WHERE user_id="nemo" AND group_id=2),
+--	(SELECT id FROM role WHERE name = "USER_GROUP_MEMBER");
+--	
+--INSERT INTO group_member_role(group_member_id,role_id)
+--SELECT
+--	(SELECT id FROM user_group_member WHERE user_id="nemo" AND group_id=2),
+--	(SELECT id FROM role WHERE name = "VC_ACCESS_MEMBER");
 
 
 -- auto group
-INSERT INTO group_member_role(group_member_id,role_id)
-SELECT
-	(SELECT id FROM user_group_member WHERE user_id="pearl" AND group_id=3),
-	(SELECT id FROM role WHERE name = "VC_ACCESS_MEMBER");
+--INSERT INTO group_member_role(group_member_id,role_id)
+--SELECT
+--	(SELECT id FROM user_group_member WHERE user_id="pearl" AND group_id=3),
+--	(SELECT id FROM role WHERE name = "VC_ACCESS_MEMBER");
 
diff --git a/src/test/java/de/ids_mannheim/korap/dao/DaoTestBase.java b/src/test/java/de/ids_mannheim/korap/dao/DaoTestBase.java
new file mode 100644
index 0000000..31cf098
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/dao/DaoTestBase.java
@@ -0,0 +1,59 @@
+package de.ids_mannheim.korap.dao;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import org.springframework.beans.factory.annotation.Autowired;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.UserGroupStatus;
+import de.ids_mannheim.korap.entity.UserGroup;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.service.UserGroupService;
+
+public class DaoTestBase {
+    
+    @Autowired
+    protected UserGroupDao userGroupDao;
+    @Autowired
+    protected UserGroupService userGroupService;
+
+    protected UserGroup createUserGroup (String groupName, String createdBy)
+            throws KustvaktException {
+        int groupId = userGroupDao.createGroup(groupName, null, createdBy,
+                UserGroupStatus.ACTIVE);
+        // retrieve group
+        UserGroup group = userGroupDao.retrieveGroupById(groupId, true);
+        assertEquals(groupName, group.getName());
+        assertEquals(createdBy, group.getCreatedBy());
+        assertEquals(UserGroupStatus.ACTIVE, group.getStatus());
+        assertNotNull(group.getCreatedDate());
+        return group;
+    }
+    
+    protected UserGroup createDoryGroup () throws KustvaktException {
+        UserGroup group = createUserGroup("dory-group", "dory");
+        userGroupService.addGroupMember("nemo", group, "dory",
+                GroupMemberStatus.ACTIVE);
+        userGroupService.addGroupMember("marlin", group, "dory",
+                GroupMemberStatus.PENDING);
+        userGroupService.addGroupMember("pearl", group, "dory",
+                GroupMemberStatus.DELETED);
+
+        return group;
+    }
+    
+    protected void deleteUserGroup (int groupId, String username)
+            throws KustvaktException {
+        userGroupDao.deleteGroup(groupId, username, false);
+        KustvaktException exception = assertThrows(KustvaktException.class,
+                () -> {
+                    userGroupDao.retrieveGroupById(groupId);
+                });
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                exception.getStatusCode().intValue());
+
+    }
+}
diff --git a/src/test/java/de/ids_mannheim/korap/dao/RoleDaoTest.java b/src/test/java/de/ids_mannheim/korap/dao/RoleDaoTest.java
new file mode 100644
index 0000000..fc81b5e
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/dao/RoleDaoTest.java
@@ -0,0 +1,84 @@
+package de.ids_mannheim.korap.dao;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit.jupiter.SpringExtension;
+import org.sqlite.SQLiteException;
+
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.PrivilegeType;
+import de.ids_mannheim.korap.entity.QueryDO;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.UserGroup;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import jakarta.persistence.PersistenceException;
+
+@Disabled
+@ExtendWith(SpringExtension.class)
+@ContextConfiguration("classpath:test-config.xml")
+public class RoleDaoTest extends DaoTestBase {
+
+    @Autowired
+    private RoleDao roleDao;
+    @Autowired
+    private QueryDao queryDao;
+
+    @Test
+    public void testUniqueRoleWithoutQuery () throws KustvaktException {
+        UserGroup group = createDoryGroup();
+
+        Role r = new Role(PredefinedRole.GROUP_ADMIN, PrivilegeType.READ_MEMBER,
+                group);
+
+        Exception exception = assertThrows(PersistenceException.class, () -> {
+            roleDao.addRole(r);
+        });
+
+        Throwable rootCause = exception;
+        while (rootCause.getCause() != null) {
+            rootCause = rootCause.getCause();
+        }
+
+        assertEquals(SQLiteException.class, rootCause.getClass());
+        assertTrue(rootCause.getMessage()
+                .startsWith("[SQLITE_CONSTRAINT_UNIQUE]"));
+
+        deleteUserGroup(group.getId(), "dory");
+    }
+
+    @Test
+    public void testUniqueRoleWithQuery () throws KustvaktException {
+        QueryDO query = queryDao.retrieveQueryByName("dory-vc", "dory");
+
+        UserGroup group = createDoryGroup();
+
+        Role r1 = new Role(PredefinedRole.GROUP_ADMIN,
+                PrivilegeType.READ_MEMBER, group, query);
+        roleDao.addRole(r1);
+
+        Role r2 = new Role(PredefinedRole.GROUP_ADMIN,
+                PrivilegeType.READ_MEMBER, group, query);
+
+        Exception exception = assertThrows(PersistenceException.class, () -> {
+            roleDao.addRole(r2);
+        });
+        
+        Throwable rootCause = exception;
+        while (rootCause.getCause() != null) {
+            rootCause = rootCause.getCause();
+        }
+
+        assertEquals(SQLiteException.class, rootCause.getClass());
+        assertTrue(rootCause.getMessage()
+                .startsWith("[SQLITE_CONSTRAINT_UNIQUE]"));
+
+        deleteUserGroup(group.getId(), "dory");
+    }
+}
diff --git a/src/test/java/de/ids_mannheim/korap/dao/RolePrivilegeDaoTest.java b/src/test/java/de/ids_mannheim/korap/dao/RolePrivilegeDaoTest.java
deleted file mode 100644
index 13b2c91..0000000
--- a/src/test/java/de/ids_mannheim/korap/dao/RolePrivilegeDaoTest.java
+++ /dev/null
@@ -1,78 +0,0 @@
-package de.ids_mannheim.korap.dao;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.context.junit.jupiter.SpringExtension;
-
-import de.ids_mannheim.korap.constant.PredefinedRole;
-import de.ids_mannheim.korap.constant.PrivilegeType;
-import de.ids_mannheim.korap.entity.Privilege;
-import de.ids_mannheim.korap.entity.Role;
-
-@ExtendWith(SpringExtension.class)
-@ContextConfiguration("classpath:test-config.xml")
-public class RolePrivilegeDaoTest {
-
-    @Autowired
-    private RoleDao roleDao;
-
-    @Autowired
-    private PrivilegeDao privilegeDao;
-
-    @Test
-    public void retrievePredefinedRole () {
-        Role r = roleDao
-                .retrieveRoleById(PredefinedRole.USER_GROUP_ADMIN.getId());
-        assertEquals(1, r.getId());
-    }
-
-    @Test
-    public void createDeleteRole () {
-        String roleName = "vc editor";
-        List<PrivilegeType> privileges = new ArrayList<PrivilegeType>();
-        privileges.add(PrivilegeType.READ);
-        privileges.add(PrivilegeType.WRITE);
-        roleDao.createRole(roleName, privileges);
-        Role r = roleDao.retrieveRoleByName(roleName);
-        assertEquals(roleName, r.getName());
-        assertEquals(2, r.getPrivileges().size());
-        roleDao.deleteRole(r.getId());
-    }
-
-    @Test
-    public void updateRole () {
-        Role role = roleDao.retrieveRoleByName("USER_GROUP_MEMBER");
-        roleDao.editRoleName(role.getId(), "USER_GROUP_MEMBER role");
-        role = roleDao.retrieveRoleById(role.getId());
-        assertEquals(role.getName(), "USER_GROUP_MEMBER role");
-        roleDao.editRoleName(role.getId(), "USER_GROUP_MEMBER");
-        role = roleDao.retrieveRoleById(role.getId());
-        assertEquals(role.getName(), "USER_GROUP_MEMBER");
-    }
-
-    @Test
-    public void addDeletePrivilegeOfExistingRole () {
-        Role role = roleDao.retrieveRoleByName("USER_GROUP_MEMBER");
-        List<Privilege> privileges = role.getPrivileges();
-        assertEquals(1, role.getPrivileges().size());
-        assertEquals(privileges.get(0).getName(), PrivilegeType.DELETE);
-        // add privilege
-        List<PrivilegeType> privilegeTypes = new ArrayList<PrivilegeType>();
-        privilegeTypes.add(PrivilegeType.READ);
-        privilegeDao.addPrivilegesToRole(role, privilegeTypes);
-        role = roleDao.retrieveRoleByName("USER_GROUP_MEMBER");
-        assertEquals(2, role.getPrivileges().size());
-        // delete privilege
-        privilegeDao.deletePrivilegeFromRole(role.getId(), PrivilegeType.READ);
-        role = roleDao.retrieveRoleByName("USER_GROUP_MEMBER");
-        assertEquals(1, role.getPrivileges().size());
-        assertEquals(privileges.get(0).getName(), PrivilegeType.DELETE);
-    }
-}
diff --git a/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java b/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
index c698321..067ee6a 100644
--- a/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
+++ b/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
@@ -1,11 +1,7 @@
 package de.ids_mannheim.korap.dao;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertNull;
-import static org.junit.jupiter.api.Assertions.assertThrows;
 
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Set;
 
@@ -15,131 +11,70 @@
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit.jupiter.SpringExtension;
 
-import de.ids_mannheim.korap.config.FullConfiguration;
-import de.ids_mannheim.korap.constant.GroupMemberStatus;
-import de.ids_mannheim.korap.constant.PredefinedRole;
-import de.ids_mannheim.korap.constant.QueryAccessStatus;
-import de.ids_mannheim.korap.constant.QueryType;
-import de.ids_mannheim.korap.constant.ResourceType;
-import de.ids_mannheim.korap.constant.UserGroupStatus;
-import de.ids_mannheim.korap.entity.QueryDO;
 import de.ids_mannheim.korap.entity.Role;
 import de.ids_mannheim.korap.entity.UserGroup;
 import de.ids_mannheim.korap.entity.UserGroupMember;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.user.User.CorpusAccess;
 
 @ExtendWith(SpringExtension.class)
 @ContextConfiguration("classpath:test-config.xml")
-public class UserGroupDaoTest {
-
-    @Autowired
-    private UserGroupDao userGroupDao;
-
-    @Autowired
-    private QueryDao virtualCorpusDao;
+public class UserGroupDaoTest extends DaoTestBase {
 
     @Autowired
     private RoleDao roleDao;
 
-    @Autowired
-    private FullConfiguration config;
-
     @Test
     public void createDeleteNewUserGroup () throws KustvaktException {
-        String groupName = "test group";
-        String createdBy = "test class";
-        // create group
-        int groupId = userGroupDao.createGroup(groupName, null, createdBy,
-                UserGroupStatus.ACTIVE);
-        // retrieve group
-        UserGroup group = userGroupDao.retrieveGroupById(groupId, true);
-        assertEquals(groupName, group.getName());
-        assertEquals(createdBy, group.getCreatedBy());
-        assertEquals(UserGroupStatus.ACTIVE, group.getStatus());
-        assertNull(group.getDeletedBy());
+        String groupName = "test-group";
+        String createdBy = "test-user";
+        UserGroup group = createUserGroup(groupName, createdBy);
+
         // group member
         List<UserGroupMember> members = group.getMembers();
         assertEquals(1, members.size());
         UserGroupMember m = members.get(0);
-        assertEquals(GroupMemberStatus.ACTIVE, m.getStatus());
-        assertEquals(createdBy, m.getCreatedBy());
         assertEquals(createdBy, m.getUserId());
+
         // member roles
         Set<Role> roles = roleDao.retrieveRoleByGroupMemberId(m.getId());
-        assertEquals(2, roles.size());
-        ArrayList<Role> roleList = new ArrayList<>(2);
-        roleList.addAll(roles);
-        Collections.sort(roleList);
-        assertEquals(PredefinedRole.USER_GROUP_ADMIN.getId(),
-                roleList.get(0).getId());
-        assertEquals(PredefinedRole.VC_ACCESS_ADMIN.getId(),
-                roleList.get(1).getId());
-        // retrieve VC by group
-        List<QueryDO> vc = virtualCorpusDao.retrieveQueryByGroup(groupId);
-        assertEquals(0, vc.size());
-        // soft delete group
-        userGroupDao.deleteGroup(groupId, createdBy,
-                config.isSoftDeleteGroup());
-        group = userGroupDao.retrieveGroupById(groupId);
-        assertEquals(UserGroupStatus.DELETED, group.getStatus());
-        // hard delete
-        userGroupDao.deleteGroup(groupId, createdBy, false);
-        KustvaktException exception = assertThrows(KustvaktException.class,
-                () -> {
-                    userGroupDao.retrieveGroupById(groupId);
-                });
-        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
-                exception.getStatusCode().intValue());
+        assertEquals(5, roles.size());
+
+        int groupId = group.getId();
+        //        // retrieve VC by group
+        //        List<QueryDO> vc = virtualCorpusDao.retrieveQueryByGroup(groupId);
+        //        assertEquals(0, vc.size());
+
+        deleteUserGroup(groupId, createdBy);
     }
 
     @Test
     public void retrieveGroupWithMembers () throws KustvaktException {
+        UserGroup group = createDoryGroup();
         // dory group
-        List<UserGroupMember> members = userGroupDao.retrieveGroupById(2, true)
-                .getMembers();
+        List<UserGroupMember> members = userGroupDao
+                .retrieveGroupById(group.getId(), true).getMembers();
         assertEquals(4, members.size());
+
         UserGroupMember m = members.get(1);
         Set<Role> roles = m.getRoles();
-        assertEquals(2, roles.size());
-        List<Role> sortedRoles = new ArrayList<>(roles);
-        Collections.sort(sortedRoles);
-        assertEquals(PredefinedRole.USER_GROUP_MEMBER.name(),
-                sortedRoles.get(0).getName());
-        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
-                sortedRoles.get(1).getName());
+        assertEquals(0, roles.size());
+        //        assertEquals(2, roles.size());
+
+        //        List<Role> sortedRoles = new ArrayList<>(roles);
+        //        Collections.sort(sortedRoles);
+        //        assertEquals(PredefinedRole.USER_GROUP_MEMBER.name(),
+        //                sortedRoles.get(0).getName());
+        //        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
+        //                sortedRoles.get(1).getName());
+
+        retrieveGroupByUserId();
+        deleteUserGroup(group.getId(), "dory");
     }
 
-    @Test
-    public void retrieveGroupByUserId () throws KustvaktException {
+    private void retrieveGroupByUserId () throws KustvaktException {
         List<UserGroup> group = userGroupDao.retrieveGroupByUserId("dory");
-        assertEquals(2, group.size());
+        assertEquals(1, group.size());
         group = userGroupDao.retrieveGroupByUserId("pearl");
         assertEquals(0, group.size());
     }
-
-    @Test
-    public void addVCToGroup () throws KustvaktException {
-        // dory group
-        int groupId = 2;
-        UserGroup group = userGroupDao.retrieveGroupById(groupId);
-        String createdBy = "dory";
-        String name = "dory new vc";
-        int id = virtualCorpusDao.createQuery(name, ResourceType.PROJECT,
-                QueryType.VIRTUAL_CORPUS, CorpusAccess.PUB, "corpusSigle=WPD15",
-                "", "", "", false, createdBy, null, null);
-        QueryDO virtualCorpus = virtualCorpusDao.retrieveQueryById(id);
-        userGroupDao.addQueryToGroup(virtualCorpus, createdBy,
-                QueryAccessStatus.ACTIVE, group);
-        List<QueryDO> vc = virtualCorpusDao.retrieveQueryByGroup(groupId);
-        assertEquals(2, vc.size());
-        assertEquals(name, vc.get(1).getName());
-        // delete vc from group
-        userGroupDao.deleteQueryFromGroup(virtualCorpus.getId(), groupId);
-        vc = virtualCorpusDao.retrieveQueryByGroup(groupId);
-        assertEquals(1, vc.size());
-        // delete vc
-        virtualCorpusDao.deleteQuery(virtualCorpus);
-    }
 }
diff --git a/src/test/java/de/ids_mannheim/korap/dao/UserGroupMemberDaoTest.java b/src/test/java/de/ids_mannheim/korap/dao/UserGroupMemberDaoTest.java
deleted file mode 100644
index e3fdc87..0000000
--- a/src/test/java/de/ids_mannheim/korap/dao/UserGroupMemberDaoTest.java
+++ /dev/null
@@ -1,52 +0,0 @@
-package de.ids_mannheim.korap.dao;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.util.List;
-import java.util.Set;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.context.junit.jupiter.SpringExtension;
-
-import de.ids_mannheim.korap.constant.PredefinedRole;
-import de.ids_mannheim.korap.entity.Role;
-import de.ids_mannheim.korap.entity.UserGroupMember;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-
-@ExtendWith(SpringExtension.class)
-@ContextConfiguration("classpath:test-config.xml")
-public class UserGroupMemberDaoTest {
-
-    @Autowired
-    private UserGroupMemberDao dao;
-
-    @Autowired
-    private RoleDao roleDao;
-
-    @Test
-    public void testRetrieveMemberByRole () throws KustvaktException {
-        // dory group
-        List<UserGroupMember> vcaAdmins = dao.retrieveMemberByRole(2,
-                PredefinedRole.VC_ACCESS_ADMIN.getId());
-        // System.out.println(vcaAdmins);
-        assertEquals(1, vcaAdmins.size());
-        assertEquals(vcaAdmins.get(0).getUserId(), "dory");
-    }
-
-    @Test
-    public void testAddSameMemberRole () throws KustvaktException {
-        UserGroupMember member = dao.retrieveMemberById("dory", 1);
-        Set<Role> roles = member.getRoles();
-        Role adminRole = roleDao
-                .retrieveRoleById(PredefinedRole.USER_GROUP_ADMIN.getId());
-        roles.add(adminRole);
-        member.setRoles(roles);
-        dao.updateMember(member);
-        member = dao.retrieveMemberById("dory", 1);
-        member.getRoles();
-        assertEquals(2, roles.size());
-    }
-}
diff --git a/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDaoTest.java b/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDaoTest.java
deleted file mode 100644
index 72f1e0f..0000000
--- a/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusAccessDaoTest.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package de.ids_mannheim.korap.dao;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.util.List;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.context.junit.jupiter.SpringExtension;
-
-import de.ids_mannheim.korap.constant.QueryAccessStatus;
-import de.ids_mannheim.korap.entity.QueryAccess;
-import de.ids_mannheim.korap.entity.UserGroup;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-
-@ExtendWith(SpringExtension.class)
-@ContextConfiguration("classpath:test-config.xml")
-public class VirtualCorpusAccessDaoTest {
-
-    @Autowired
-    private QueryAccessDao dao;
-
-    @Test
-    public void getAccessByVC () throws KustvaktException {
-        List<QueryAccess> vcaList = dao.retrieveActiveAccessByQuery(2);
-        QueryAccess access = vcaList.get(0);
-        assertEquals(QueryAccessStatus.ACTIVE, access.getStatus());
-        assertEquals(access.getCreatedBy(), "dory");
-        UserGroup group = access.getUserGroup();
-        assertEquals(2, group.getId());
-    }
-}
diff --git a/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusDaoTest.java b/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusDaoTest.java
index cfd226b..b46ff3a 100644
--- a/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusDaoTest.java
+++ b/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusDaoTest.java
@@ -90,14 +90,12 @@
     public void retrieveVCByUserDory () throws KustvaktException {
         List<QueryDO> virtualCorpora = dao.retrieveQueryByUser("dory",
                 QueryType.VIRTUAL_CORPUS);
-        // System.out.println(virtualCorpora);
-        assertEquals(4, virtualCorpora.size());
+        assertEquals(3, virtualCorpora.size());
         // ordered by id
         Iterator<QueryDO> i = virtualCorpora.iterator();
         assertEquals(i.next().getName(), "dory-vc");
         assertEquals(i.next().getName(), "group-vc");
         assertEquals(i.next().getName(), "system-vc");
-        assertEquals(i.next().getName(), "published-vc");
     }
 
     /**
@@ -110,9 +108,8 @@
     public void retrieveVCByUserNemo () throws KustvaktException {
         List<QueryDO> virtualCorpora = dao.retrieveQueryByUser("nemo",
                 QueryType.VIRTUAL_CORPUS);
-        assertEquals(3, virtualCorpora.size());
+        assertEquals(2, virtualCorpora.size());
         Iterator<QueryDO> i = virtualCorpora.iterator();
-        assertEquals(i.next().getName(), "group-vc");
         assertEquals(i.next().getName(), "system-vc");
         assertEquals(i.next().getName(), "nemo-vc");
     }
@@ -144,9 +141,8 @@
     public void retrieveVCByUserPearl () throws KustvaktException {
         List<QueryDO> virtualCorpora = dao.retrieveQueryByUser("pearl",
                 QueryType.VIRTUAL_CORPUS);
-        assertEquals(2, virtualCorpora.size());
+        assertEquals(1, virtualCorpora.size());
         Iterator<QueryDO> i = virtualCorpora.iterator();
         assertEquals(i.next().getName(), "system-vc");
-        assertEquals(i.next().getName(), "published-vc");
     }
 }
diff --git a/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java b/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java
deleted file mode 100644
index 8799ad7..0000000
--- a/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java
+++ /dev/null
@@ -1,125 +0,0 @@
-package de.ids_mannheim.korap.service;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import java.util.List;
-
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.context.ContextConfiguration;
-import org.springframework.test.context.junit.jupiter.SpringExtension;
-
-import de.ids_mannheim.korap.constant.QueryType;
-import de.ids_mannheim.korap.constant.ResourceType;
-import de.ids_mannheim.korap.constant.UserGroupStatus;
-import de.ids_mannheim.korap.dto.QueryAccessDto;
-import de.ids_mannheim.korap.dto.QueryDto;
-import de.ids_mannheim.korap.entity.QueryDO;
-import de.ids_mannheim.korap.entity.UserGroup;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.web.input.QueryJson;
-
-@ExtendWith(SpringExtension.class)
-@ContextConfiguration("classpath:test-config.xml")
-public class VirtualCorpusServiceTest {
-
-    @Autowired
-    private QueryService vcService;
-
-    @Autowired
-    private UserGroupService groupService;
-
-    @Test
-    public void testCreateNonUniqueVC () throws KustvaktException {
-        // EM: message differs depending on the database used
-        // for testing. The message below is from sqlite.
-        // thrown.expectMessage("A UNIQUE constraint failed "
-        // + "(UNIQUE constraint failed: virtual_corpus.name, "
-        // + "virtual_corpus.created_by)");
-        QueryJson vc = new QueryJson();
-        vc.setCorpusQuery("corpusSigle=GOE");
-        vc.setType(ResourceType.PRIVATE);
-        vc.setQueryType(QueryType.VIRTUAL_CORPUS);
-        assertThrows(KustvaktException.class,
-                () -> vcService.storeQuery(vc, "dory-vc", "dory", "dory"));
-    }
-
-    @Test
-    public void createDeletePublishVC () throws KustvaktException {
-        String vcName = "new-published-vc";
-        QueryJson vc = new QueryJson();
-        vc.setCorpusQuery("corpusSigle=GOE");
-        vc.setType(ResourceType.PUBLISHED);
-        vc.setQueryType(QueryType.VIRTUAL_CORPUS);
-        String username = "VirtualCorpusServiceTest";
-        vcService.storeQuery(vc, vcName, username, username);
-        List<QueryAccessDto> accesses = vcService
-                .listQueryAccessByUsername("admin");
-        int size = accesses.size();
-        QueryAccessDto dto = accesses.get(accesses.size() - 1);
-        assertEquals(vcName, dto.getQueryName());
-        assertEquals(dto.getCreatedBy(), "system");
-        assertTrue(dto.getUserGroupName().startsWith("auto"));
-        // check hidden group
-        int groupId = dto.getUserGroupId();
-        UserGroup group = groupService.retrieveUserGroupById(groupId);
-        assertEquals(UserGroupStatus.HIDDEN, group.getStatus());
-        // delete vc
-        vcService.deleteQueryByName(username, vcName, username,
-                QueryType.VIRTUAL_CORPUS);
-        // check hidden access
-        accesses = vcService.listQueryAccessByUsername("admin");
-        assertEquals(size - 1, accesses.size());
-        // check hidden group
-        KustvaktException e = assertThrows(KustvaktException.class,
-                () -> groupService.retrieveUserGroupById(groupId));
-        assertEquals("Group with id " + groupId + " is not found",
-                e.getMessage());
-    }
-
-    @Test
-    public void testEditPublishVC () throws KustvaktException {
-        String username = "dory";
-        int vcId = 2;
-        String vcName = "group-vc";
-        QueryDO existingVC = vcService.searchQueryByName(username, vcName,
-                username, QueryType.VIRTUAL_CORPUS);
-        QueryJson vcJson = new QueryJson();
-        vcJson.setType(ResourceType.PUBLISHED);
-        vcService.editQuery(existingVC, vcJson, vcName, username);
-        // check VC
-        QueryDto vcDto = vcService.searchQueryById("dory", vcId);
-        assertEquals(vcName, vcDto.getName());
-        assertEquals(ResourceType.PUBLISHED.displayName(), vcDto.getType());
-        // check access
-        List<QueryAccessDto> accesses = vcService
-                .listQueryAccessByUsername("admin");
-        int size = accesses.size();
-        QueryAccessDto dto = accesses.get(accesses.size() - 1);
-        assertEquals(vcName, dto.getQueryName());
-        assertEquals(dto.getCreatedBy(), "system");
-        assertTrue(dto.getUserGroupName().startsWith("auto"));
-        // check auto hidden group
-        int groupId = dto.getUserGroupId();
-        UserGroup group = groupService.retrieveUserGroupById(groupId);
-        assertEquals(UserGroupStatus.HIDDEN, group.getStatus());
-        // 2nd edit (withdraw from publication)
-        vcJson = new QueryJson();
-        vcJson.setType(ResourceType.PROJECT);
-        vcService.editQuery(existingVC, vcJson, vcName, username);
-        // check VC
-        vcDto = vcService.searchQueryById("dory", vcId);
-        assertEquals(vcDto.getName(), "group-vc");
-        assertEquals(ResourceType.PROJECT.displayName(), vcDto.getType());
-        // check access
-        accesses = vcService.listQueryAccessByUsername("admin");
-        assertEquals(size - 1, accesses.size());
-        KustvaktException e = assertThrows(KustvaktException.class,
-                () -> groupService.retrieveUserGroupById(groupId));
-        assertEquals("Group with id " + groupId + " is not found",
-                e.getMessage());
-    }
-}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
index 6b5627d..d8dff83 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AccessTokenTest.java
@@ -24,8 +24,9 @@
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.web.controller.usergroup.UserGroupTestBase;
 
-public class OAuth2AccessTokenTest extends OAuth2TestBase {
+public class OAuth2AccessTokenTest extends UserGroupTestBase {
 
     private String userAuthHeader;
 
@@ -46,6 +47,12 @@
         JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
         assertEquals(node.at("/scope").asText(), "all");
         String accessToken = node.at("/access_token").asText();
+        
+        createDoryGroup();
+        createMarlinGroup();
+        inviteMember(marlinGroupName, "marlin", "dory");
+        subscribe(marlinGroupName, "dory");
+        
         // test list user group
         response = target().path(API_VERSION).path("group").request()
                 .header(Attributes.AUTHORIZATION, "Bearer " + accessToken)
@@ -53,6 +60,9 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         node = JsonUtils.readTree(response.readEntity(String.class));
         assertEquals(2, node.size());
+        
+        deleteGroupByName(doryGroupName, "dory");
+        deleteGroupByName(marlinGroupName, "marlin");
     }
 
     @Test
@@ -72,7 +82,7 @@
                 .header(Attributes.AUTHORIZATION, "Bearer " + token).get();
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         node = JsonUtils.readTree(response.readEntity(String.class));
-        assertEquals(4, node.size());
+        assertEquals(3, node.size());
     }
 
     @Test
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
deleted file mode 100644
index 5495c85..0000000
--- a/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
+++ /dev/null
@@ -1,853 +0,0 @@
-package de.ids_mannheim.korap.web.controller;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.util.Set;
-
-import jakarta.ws.rs.core.Form;
-import jakarta.ws.rs.core.MediaType;
-
-import org.junit.jupiter.api.Test;
-import org.springframework.beans.factory.annotation.Autowired;
-import com.fasterxml.jackson.databind.JsonNode;
-import com.google.common.net.HttpHeaders;
-import jakarta.ws.rs.ProcessingException;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.core.Response.Status;
-import jakarta.ws.rs.client.Entity;
-
-import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
-import de.ids_mannheim.korap.config.Attributes;
-import de.ids_mannheim.korap.config.SpringJerseyTest;
-import de.ids_mannheim.korap.constant.GroupMemberStatus;
-import de.ids_mannheim.korap.constant.PredefinedRole;
-import de.ids_mannheim.korap.dao.UserGroupMemberDao;
-import de.ids_mannheim.korap.entity.Role;
-import de.ids_mannheim.korap.entity.UserGroupMember;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.utils.JsonUtils;
-
-/**
- * @author margaretha
- */
-public class UserGroupControllerTest extends SpringJerseyTest {
-
-    @Autowired
-    private UserGroupMemberDao memberDao;
-
-    private String username = "UserGroupControllerTest";
-
-    private String admin = "admin";
-
-    private JsonNode retrieveUserGroups (String username)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").get();
-        String entity = response.readEntity(String.class);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        return JsonUtils.readTree(entity);
-    }
-
-    private void deleteGroupByName (String groupName) throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-    }
-
-    // dory is a group admin in dory-group
-    @Test
-    public void testListDoryGroups () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").get();
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        JsonNode node = JsonUtils.readTree(entity);
-        JsonNode group = node.get(1);
-        assertEquals(2, group.at("/id").asInt());
-        assertEquals(group.at("/name").asText(), "dory-group");
-        assertEquals(group.at("/owner").asText(), "dory");
-        assertEquals(3, group.at("/members").size());
-    }
-
-    // nemo is a group member in dory-group
-    @Test
-    public void testListNemoGroups () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("nemo", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").get();
-        String entity = response.readEntity(String.class);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(2, node.at("/0/id").asInt());
-        assertEquals(node.at("/0/name").asText(), "dory-group");
-        assertEquals(node.at("/0/owner").asText(), "dory");
-        // group members are not allowed to see other members
-        assertEquals(0, node.at("/0/members").size());
-    }
-
-    // marlin has 2 groups
-    @Test
-    public void testListMarlinGroups () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").get();
-        String entity = response.readEntity(String.class);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(2, node.size());
-    }
-
-    @Test
-    public void testListGroupGuest () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").get();
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Unauthorized operation for user: guest");
-    }
-
-    @Test
-    public void testCreateGroupEmptyDescription ()
-            throws ProcessingException, KustvaktException {
-        String groupName = "empty_group";
-        Response response = testCreateUserGroup(groupName, "");
-        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
-        deleteGroupByName(groupName);
-    }
-
-    @Test
-    public void testCreateGroupMissingDescription ()
-            throws ProcessingException, KustvaktException {
-        String groupName = "missing-desc-group";
-        Response response = testCreateGroupWithoutDescription(groupName);
-        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
-        deleteGroupByName(groupName);
-    }
-
-    private Response testCreateUserGroup (String groupName, String description)
-            throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("description", description);
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .put(Entity.form(form));
-        return response;
-    }
-
-    private Response testCreateGroupWithoutDescription (String groupName)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .put(Entity.form(new Form()));
-        return response;
-    }
-
-    @Test
-    public void testCreateGroupInvalidName ()
-            throws ProcessingException, KustvaktException {
-        String groupName = "invalid-group-name$";
-        Response response = testCreateGroupWithoutDescription(groupName);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
-        assertEquals(StatusCodes.INVALID_ARGUMENT,
-                node.at("/errors/0/0").asInt());
-        // assertEquals("User-group name must only contains letters, numbers, "
-        // + "underscores, hypens and spaces", node.at("/errors/0/1").asText());
-        assertEquals(node.at("/errors/0/2").asText(), "invalid-group-name$");
-    }
-
-    @Test
-    public void testCreateGroupNameTooShort ()
-            throws ProcessingException, KustvaktException {
-        String groupName = "a";
-        Response response = testCreateGroupWithoutDescription(groupName);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
-        assertEquals(StatusCodes.INVALID_ARGUMENT,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "groupName must contain at least 3 characters");
-        assertEquals(node.at("/errors/0/2").asText(), "groupName");
-    }
-
-    @Test
-    public void testUserGroup () throws ProcessingException, KustvaktException {
-        String groupName = "new-user-group";
-        String description = "This is new-user-group.";
-        Response response = testCreateUserGroup(groupName, description);
-        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
-        // same name
-        response = testCreateGroupWithoutDescription(groupName);
-        assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
-        // list user group
-        JsonNode node = retrieveUserGroups(username);
-        assertEquals(1, node.size());
-        node = node.get(0);
-        assertEquals(node.get("name").asText(), "new-user-group");
-        assertEquals(description, node.get("description").asText());
-        assertEquals(username, node.get("owner").asText());
-        assertEquals(1, node.get("members").size());
-        assertEquals(username, node.at("/members/0/userId").asText());
-        assertEquals(GroupMemberStatus.ACTIVE.name(),
-                node.at("/members/0/status").asText());
-        assertEquals(PredefinedRole.VC_ACCESS_ADMIN.name(),
-                node.at("/members/0/roles/1").asText());
-        assertEquals(PredefinedRole.USER_GROUP_ADMIN.name(),
-                node.at("/members/0/roles/0").asText());
-        testUpdateUserGroup(groupName);
-        testInviteMember(groupName);
-        testDeleteMemberUnauthorized(groupName);
-        testDeleteMember(groupName);
-        testDeleteGroup(groupName);
-        testSubscribeToDeletedGroup(groupName);
-        testUnsubscribeToDeletedGroup(groupName);
-    }
-
-    private void testUpdateUserGroup (String groupName)
-            throws ProcessingException, KustvaktException {
-        String description = "Description is updated.";
-        Response response = testCreateUserGroup(groupName, description);
-        assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
-        JsonNode node = retrieveUserGroups(username);
-        assertEquals(1, node.size());
-        assertEquals(description, node.get(0).get("description").asText());
-    }
-
-    private void testDeleteMember (String groupName)
-            throws ProcessingException, KustvaktException {
-        // delete darla from group
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("~darla").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
-        // check group member
-        response = target().path(API_VERSION).path("group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").get();
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        node = node.get(0);
-        assertEquals(1, node.get("members").size());
-    }
-
-    private void testDeleteMemberUnauthorized (String groupName)
-            throws ProcessingException, KustvaktException {
-        // nemo is a group member
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("~darla").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("nemo", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Unauthorized operation for user: nemo");
-    }
-
-    // EM: same as cancel invitation
-    private void testDeletePendingMember ()
-            throws ProcessingException, KustvaktException {
-        // dory delete pearl
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("~pearl").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // check member
-        JsonNode node = retrieveUserGroups("pearl");
-        assertEquals(0, node.size());
-    }
-
-    @Test
-    public void testDeleteDeletedMember ()
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("~pearl").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.GROUP_MEMBER_DELETED,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "pearl has already been deleted from the group dory-group");
-        assertEquals(node.at("/errors/0/2").asText(), "[pearl, dory-group]");
-    }
-
-    private void testDeleteGroup (String groupName)
-            throws ProcessingException, KustvaktException {
-        // delete group
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        Form f = new Form();
-        f.param("username", username);
-        f.param("status", "DELETED");
-        // EM: this is so complicated because the group retrieval are not allowed
-        // for delete groups
-        // check group
-        response = target().path(API_VERSION).path("admin").path("group")
-                .path("list").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(admin, "pass"))
-                .header(HttpHeaders.CONTENT_TYPE,
-                        MediaType.APPLICATION_FORM_URLENCODED)
-                .post(Entity.form(f));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        for (int j = 0; j < node.size(); j++) {
-            JsonNode group = node.get(j);
-            // check group members
-            for (int i = 0; i < group.at("/0/members").size(); i++) {
-                assertEquals(GroupMemberStatus.DELETED.name(),
-                        group.at("/0/members/" + i + "/status").asText());
-            }
-        }
-    }
-
-    @Test
-    public void testDeleteGroupUnauthorized ()
-            throws ProcessingException, KustvaktException {
-        // dory is a group admin in marlin-group
-        Response response = target().path(API_VERSION).path("group")
-                .path("@marlin-group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Unauthorized operation for user: dory");
-    }
-
-    @Test
-    public void testDeleteDeletedGroup ()
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@deleted-group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Group deleted-group has been deleted.");
-        assertEquals(node.at("/errors/0/2").asText(), "deleted-group");
-    }
-
-    @Test
-    public void testDeleteGroupOwner ()
-            throws ProcessingException, KustvaktException {
-        // delete marlin from marlin-group
-        // dory is a group admin in marlin-group
-        Response response = target().path(API_VERSION).path("group")
-                .path("@marlin-group").path("~marlin").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.NOT_ALLOWED, node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Operation 'delete group owner'is not allowed.");
-    }
-
-    private void testInviteMember (String groupName)
-            throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("members", "darla");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("invite").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // list group
-        response = target().path(API_VERSION).path("group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").get();
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        node = node.get(0);
-        assertEquals(2, node.get("members").size());
-        assertEquals(node.at("/members/1/userId").asText(), "darla");
-        assertEquals(GroupMemberStatus.PENDING.name(),
-                node.at("/members/1/status").asText());
-        assertEquals(0, node.at("/members/1/roles").size());
-    }
-
-    private void testInviteDeletedMember ()
-            throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("members", "marlin");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("invite").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // check member
-        JsonNode node = retrieveUserGroups("marlin");
-        assertEquals(2, node.size());
-        JsonNode group = node.get(1);
-        assertEquals(GroupMemberStatus.PENDING.name(),
-                group.at("/userMemberStatus").asText());
-    }
-
-    @Test
-    public void testInviteDeletedMember2 ()
-            throws ProcessingException, KustvaktException {
-        // pearl has status deleted in dory-group
-        Form form = new Form();
-        form.param("members", "pearl");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("invite").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // check member
-        JsonNode node = retrieveUserGroups("pearl");
-        assertEquals(1, node.size());
-        JsonNode group = node.get(0);
-        assertEquals(GroupMemberStatus.PENDING.name(),
-                group.at("/userMemberStatus").asText());
-        testDeletePendingMember();
-    }
-
-    @Test
-    public void testInvitePendingMember ()
-            throws ProcessingException, KustvaktException {
-        // marlin has status PENDING in dory-group
-        Form form = new Form();
-        form.param("members", "marlin");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("invite").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .post(Entity.form(form));
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.GROUP_MEMBER_EXISTS,
-                node.at("/errors/0/0").asInt());
-        assertEquals(
-                "Username marlin with status PENDING exists in the user-group "
-                        + "dory-group",
-                node.at("/errors/0/1").asText());
-        assertEquals(node.at("/errors/0/2").asText(),
-                "[marlin, PENDING, dory-group]");
-    }
-
-    @Test
-    public void testInviteActiveMember ()
-            throws ProcessingException, KustvaktException {
-        // nemo has status active in dory-group
-        Form form = new Form();
-        form.param("members", "nemo");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("invite").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.GROUP_MEMBER_EXISTS,
-                node.at("/errors/0/0").asInt());
-        assertEquals(
-                "Username nemo with status ACTIVE exists in the user-group "
-                        + "dory-group",
-                node.at("/errors/0/1").asText());
-        assertEquals(node.at("/errors/0/2").asText(),
-                "[nemo, ACTIVE, dory-group]");
-    }
-
-    @Test
-    public void testInviteMemberToDeletedGroup ()
-            throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("members", "nemo");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@deleted-group").path("invite").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Group deleted-group has been deleted.");
-        assertEquals(node.at("/errors/0/2").asText(), "deleted-group");
-    }
-
-    // marlin has GroupMemberStatus.PENDING in dory-group
-    @Test
-    public void testSubscribePendingMember () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("subscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .post(Entity.form(new Form()));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // retrieve marlin group
-        JsonNode node = retrieveUserGroups("marlin");
-        // System.out.println(node);
-        assertEquals(2, node.size());
-        JsonNode group = node.get(1);
-        assertEquals(2, group.at("/id").asInt());
-        assertEquals(group.at("/name").asText(), "dory-group");
-        assertEquals(group.at("/owner").asText(), "dory");
-        // group members are not allowed to see other members
-        assertEquals(0, group.at("/members").size());
-        assertEquals(GroupMemberStatus.ACTIVE.name(),
-                group.at("/userMemberStatus").asText());
-        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
-                group.at("/userRoles/1").asText());
-        assertEquals(PredefinedRole.USER_GROUP_MEMBER.name(),
-                group.at("/userRoles/0").asText());
-        // unsubscribe marlin from dory-group
-        testUnsubscribeActiveMember("dory-group");
-        checkGroupMemberRole("dory-group", "marlin");
-        // invite marlin to dory-group to set back the
-        // GroupMemberStatus.PENDING
-        testInviteDeletedMember();
-    }
-
-    // pearl has GroupMemberStatus.DELETED in dory-group
-    @Test
-    public void testSubscribeDeletedMember () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("subscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("pearl", "pass"))
-                .post(Entity.form(new Form()));
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.GROUP_MEMBER_DELETED,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "pearl has already been deleted from the group dory-group");
-    }
-
-    @Test
-    public void testSubscribeMissingGroupName () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("subscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("bruce", "pass"))
-                .post(Entity.form(new Form()));
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-    }
-
-    @Test
-    public void testSubscribeNonExistentMember () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("subscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("bruce", "pass"))
-                .post(Entity.form(new Form()));
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.GROUP_MEMBER_NOT_FOUND,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "bruce is not found in the group");
-    }
-
-    @Test
-    public void testSubscribeToNonExistentGroup () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@non-existent").path("subscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("pearl", "pass"))
-                .post(Entity.form(new Form()));
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Group non-existent is not found");
-    }
-
-    private void testSubscribeToDeletedGroup (String groupName)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("subscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("nemo", "pass"))
-                .post(Entity.form(new Form()));
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Group new-user-group has been deleted.");
-    }
-
-    private void testUnsubscribeActiveMember (String groupName)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("unsubscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .delete();
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        JsonNode node = retrieveUserGroups("marlin");
-        assertEquals(1, node.size());
-    }
-
-    private void checkGroupMemberRole (String groupName,
-            String deletedMemberName) throws KustvaktException {
-        Response response = target().path(API_VERSION).path("admin")
-                .path("group").path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(admin, "pass"))
-                .post(null);
-        String entity = response.readEntity(String.class);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        JsonNode node = JsonUtils.readTree(entity).at("/members");
-        JsonNode member;
-        for (int i = 0; i < node.size(); i++) {
-            member = node.get(i);
-            if (deletedMemberName.equals(member.at("/userId").asText())) {
-                assertEquals(0, node.at("/roles").size());
-                break;
-            }
-        }
-    }
-
-    @Test
-    public void testUnsubscribeDeletedMember ()
-            throws ProcessingException, KustvaktException {
-        // pearl unsubscribes from dory-group
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("unsubscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("pearl", "pass"))
-                .delete();
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.GROUP_MEMBER_DELETED,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "pearl has already been deleted from the group dory-group");
-        assertEquals(node.at("/errors/0/2").asText(), "[pearl, dory-group]");
-    }
-
-    @Test
-    public void testUnsubscribePendingMember ()
-            throws ProcessingException, KustvaktException {
-        JsonNode node = retrieveUserGroups("marlin");
-        assertEquals(2, node.size());
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("unsubscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .delete();
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        node = retrieveUserGroups("marlin");
-        assertEquals(1, node.size());
-        // invite marlin to dory-group to set back the
-        // GroupMemberStatus.PENDING
-        testInviteDeletedMember();
-    }
-
-    @Test
-    public void testUnsubscribeMissingGroupName () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("unsubscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .delete();
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-    }
-
-    @Test
-    public void testUnsubscribeNonExistentMember () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("unsubscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("bruce", "pass"))
-                .delete();
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.GROUP_MEMBER_NOT_FOUND,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "bruce is not found in the group");
-    }
-
-    @Test
-    public void testUnsubscribeToNonExistentGroup () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@tralala-group").path("unsubscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("pearl", "pass"))
-                .delete();
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Group tralala-group is not found");
-    }
-
-    private void testUnsubscribeToDeletedGroup (String groupName)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("unsubscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("nemo", "pass"))
-                .delete();
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Group new-user-group has been deleted.");
-    }
-
-    @Test
-    public void testAddSameMemberRole ()
-            throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("memberUsername", "dory");
-        form.param("roleId", "1");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@marlin-group").path("role").path("add").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        UserGroupMember member = memberDao.retrieveMemberById("dory", 1);
-        Set<Role> roles = member.getRoles();
-        assertEquals(2, roles.size());
-    }
-
-    @Test
-    public void testDeleteAddMemberRole ()
-            throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("memberUsername", "dory");
-        form.param("roleId", "1");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@marlin-group").path("role").path("delete").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        UserGroupMember member = memberDao.retrieveMemberById("dory", 1);
-        Set<Role> roles = member.getRoles();
-        assertEquals(1, roles.size());
-        testAddSameMemberRole();
-    }
-
-    @Test
-    public void testEditMemberRoleEmpty ()
-            throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("memberUsername", "dory");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@marlin-group").path("role").path("edit").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        UserGroupMember member = memberDao.retrieveMemberById("dory", 1);
-        Set<Role> roles = member.getRoles();
-        assertEquals(0, roles.size());
-        testEditMemberRole();
-    }
-
-    private void testEditMemberRole ()
-            throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("memberUsername", "dory");
-        form.param("roleId", "1");
-        form.param("roleId", "3");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@marlin-group").path("role").path("edit").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        UserGroupMember member = memberDao.retrieveMemberById("dory", 1);
-        Set<Role> roles = member.getRoles();
-        assertEquals(2, roles.size());
-    }
-}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusAccessTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusAccessTest.java
deleted file mode 100644
index a1594fc..0000000
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusAccessTest.java
+++ /dev/null
@@ -1,165 +0,0 @@
-package de.ids_mannheim.korap.web.controller;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import jakarta.ws.rs.ProcessingException;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.core.Response.Status;
-
-import org.apache.http.HttpStatus;
-import org.junit.jupiter.api.Test;
-import com.fasterxml.jackson.databind.JsonNode;
-import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
-import de.ids_mannheim.korap.config.Attributes;
-import de.ids_mannheim.korap.constant.ResourceType;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.utils.JsonUtils;
-
-public class VirtualCorpusAccessTest extends VirtualCorpusTestBase {
-
-    private String testUser = "VirtualCorpusAccessTest";
-
-    @Test
-    public void testlistAccessByNonVCAAdmin () throws KustvaktException {
-        JsonNode node = listAccessByGroup("nemo", "dory-group");
-        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Unauthorized operation for user: nemo");
-    }
-
-    // @Test
-    // public void testlistAccessMissingId () throws KustvaktException
-    // {
-    // Response response =
-    // target().path(API_VERSION).path("vc")
-    // .path("access")
-    // .request().header(Attributes.AUTHORIZATION,
-    // HttpAuthorizationHandler
-    // .createBasicAuthorizationHeaderValue(
-    // testUser, "pass"))
-    // .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-    // .get();
-    // String entity = response.readEntity(String.class);
-    // JsonNode node = JsonUtils.readTree(entity);
-    // assertEquals(Status.BAD_REQUEST.getStatusCode(),
-    // response.getStatus());
-    // assertEquals(StatusCodes.MISSING_PARAMETER,
-    // node.at("/errors/0/0").asInt());
-    // assertEquals("vcId", node.at("/errors/0/1").asText());
-    // }
-    @Test
-    public void testlistAccessByGroup () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("vc").path("access")
-                .queryParam("groupName", "dory-group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .get();
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(1, node.at("/0/accessId").asInt());
-        assertEquals(2, node.at("/0/queryId").asInt());
-        assertEquals(node.at("/0/queryName").asText(), "group-vc");
-        assertEquals(2, node.at("/0/userGroupId").asInt());
-        assertEquals(node.at("/0/userGroupName").asText(), "dory-group");
-    }
-
-    @Test
-    public void testDeleteSharedVC () throws KustvaktException {
-        String json = "{\"type\": \"PROJECT\""
-                + ",\"queryType\": \"VIRTUAL_CORPUS\""
-                + ",\"corpusQuery\": \"corpusSigle=GOE\"}";
-        String vcName = "new_project_vc";
-        String username = "dory";
-        String authHeader = HttpAuthorizationHandler
-                .createBasicAuthorizationHeaderValue(username, "pass");
-        createVC(authHeader, username, vcName, json);
-        String groupName = "dory-group";
-        testShareVCByCreator(username, vcName, groupName);
-        JsonNode node = listAccessByGroup(username, groupName);
-        assertEquals(2, node.size());
-        // delete project VC
-        deleteVC(vcName, username, username);
-        node = listAccessByGroup(username, groupName);
-        assertEquals(1, node.size());
-    }
-
-    @Test
-    public void testCreateDeleteAccess ()
-            throws ProcessingException, KustvaktException {
-        String vcName = "marlin-vc";
-        String groupName = "marlin-group";
-        // check the vc type
-        JsonNode node = retrieveVCInfo("marlin", "marlin", vcName);
-        assertEquals(vcName, node.at("/name").asText());
-        assertEquals(node.at("/type").asText(), "private");
-        // share vc to group
-        Response response = testShareVCByCreator("marlin", vcName, groupName);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // check the vc type
-        node = retrieveVCInfo("marlin", "marlin", vcName);
-        assertEquals(node.at("/type").asText(), "project");
-        // list vc access by marlin
-        node = listAccessByGroup("marlin", groupName);
-        assertEquals(2, node.size());
-        // get access id
-        node = node.get(1);
-        assertEquals(5, node.at("/queryId").asInt());
-        assertEquals(vcName, node.at("/queryName").asText());
-        assertEquals(1, node.at("/userGroupId").asInt());
-        assertEquals(groupName, node.at("/userGroupName").asText());
-        String accessId = node.at("/accessId").asText();
-        testShareVC_nonUniqueAccess("marlin", vcName, groupName);
-        // delete unauthorized
-        response = testDeleteAccess(testUser, accessId);
-        testResponseUnauthorized(response, testUser);
-        // delete access by vc-admin
-        // dory is a vc-admin in marlin group
-        response = testDeleteAccess("dory", accessId);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // list vc access by dory
-        node = listAccessByGroup("dory", groupName);
-        assertEquals(1, node.size());
-        // edit VC back to private
-        String json = "{\"type\": \"" + ResourceType.PRIVATE + "\"}";
-        editVC("marlin", "marlin", vcName, json);
-        node = retrieveVCInfo("marlin", "marlin", vcName);
-        assertEquals(ResourceType.PRIVATE.displayName(),
-                node.at("/type").asText());
-    }
-
-    private void testShareVC_nonUniqueAccess (String vcCreator, String vcName,
-            String groupName) throws ProcessingException, KustvaktException {
-        Response response = testShareVCByCreator(vcCreator, vcName, groupName);
-        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
-        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatus());
-        assertEquals(StatusCodes.DB_INSERT_FAILED,
-                node.at("/errors/0/0").asInt());
-        // EM: message differs depending on the database used
-        // for testing. The message below is from sqlite.
-        // assertTrue(node.at("/errors/0/1").asText()
-        // .startsWith("[SQLITE_CONSTRAINT_UNIQUE]"));
-    }
-
-    private Response testDeleteAccess (String username, String accessId)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("vc").path("access")
-                .path(accessId).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .delete();
-        return response;
-    }
-
-    @Test
-    public void testDeleteNonExistingAccess ()
-            throws ProcessingException, KustvaktException {
-        Response response = testDeleteAccess("dory", "100");
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
-        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
-                node.at("/errors/0/0").asInt());
-    }
-}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusSharingTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusSharingTest.java
deleted file mode 100644
index d7de5a3..0000000
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusSharingTest.java
+++ /dev/null
@@ -1,202 +0,0 @@
-package de.ids_mannheim.korap.web.controller;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-
-import jakarta.ws.rs.ProcessingException;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.Form;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.core.Response.Status;
-
-import org.apache.http.HttpStatus;
-import org.junit.jupiter.api.Test;
-import com.fasterxml.jackson.databind.JsonNode;
-import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
-import de.ids_mannheim.korap.config.Attributes;
-import de.ids_mannheim.korap.constant.GroupMemberStatus;
-import de.ids_mannheim.korap.constant.PredefinedRole;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.utils.JsonUtils;
-
-public class VirtualCorpusSharingTest extends VirtualCorpusTestBase {
-
-    private String testUser = "VirtualCorpusSharingTest";
-
-    @Test
-    public void testShareUnknownVC ()
-            throws ProcessingException, KustvaktException {
-        Response response = testShareVCByCreator("marlin", "non-existing-vc",
-                "marlin group");
-        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
-        assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatus());
-        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
-                node.at("/errors/0/0").asInt());
-    }
-
-    @Test
-    public void testShareUnknownGroup ()
-            throws ProcessingException, KustvaktException {
-        Response response = testShareVCByCreator("marlin", "marlin-vc",
-                "non-existing-group");
-        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
-        assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatus());
-        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
-                node.at("/errors/0/0").asInt());
-    }
-
-    @Test
-    public void testShareVC_notOwner ()
-            throws ProcessingException, KustvaktException {
-        // dory is VCA in marlin group
-        Response response = target().path(API_VERSION).path("vc")
-                .path("~marlin").path("marlin-vc").path("share")
-                .path("@marlin group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .post(Entity.form(new Form()));
-        testResponseUnauthorized(response, "dory");
-    }
-
-    @Test
-    public void testShareVC_byMember ()
-            throws ProcessingException, KustvaktException {
-        // nemo is not VCA in marlin group
-        Response response = target().path(API_VERSION).path("vc").path("~nemo")
-                .path("nemo-vc").path("share").path("@marlin-group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("nemo", "pass"))
-                .post(Entity.form(new Form()));
-        testResponseUnauthorized(response, "nemo");
-    }
-
-    @Test
-    public void testCreateShareProjectVC () throws KustvaktException {
-        String json = "{\"type\": \"PROJECT\""
-                + ",\"queryType\": \"VIRTUAL_CORPUS\""
-                + ",\"corpusQuery\": \"corpusSigle=GOE\"}";
-        String vcName = "new_project_vc";
-        String authHeader = HttpAuthorizationHandler
-                .createBasicAuthorizationHeaderValue(testUser, "pass");
-        createVC(authHeader, testUser, vcName, json);
-        // retrieve vc info
-        JsonNode vcInfo = retrieveVCInfo(testUser, testUser, vcName);
-        assertEquals(vcName, vcInfo.get("name").asText());
-        // list user VC
-        JsonNode node = listVC(testUser);
-        assertEquals(2, node.size());
-        assertEquals(vcName, node.get(1).get("name").asText());
-        // search by non member
-        Response response = searchWithVCRef("dory", testUser, vcName);
-        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
-        // create user group
-        String groupName = "owidGroup";
-        String memberName = "darla";
-        response = createUserGroup(testUser, groupName, "Owid users");
-        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
-        listUserGroup(testUser, groupName);
-        testInviteMember(testUser, groupName, "darla");
-        subscribeToGroup(memberName, groupName);
-        checkMemberInGroup(memberName, testUser, groupName);
-        // share vc to group
-        testShareVCByCreator(testUser, vcName, groupName);
-        node = listAccessByGroup(testUser, groupName);
-        // search by member
-        response = searchWithVCRef(memberName, testUser, vcName);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        node = JsonUtils.readTree(response.readEntity(String.class));
-        assertTrue(node.at("/matches").size() > 0);
-        // delete project VC
-        deleteVC(vcName, testUser, testUser);
-        // list VC
-        node = listVC(testUser);
-        assertEquals(1, node.size());
-        // search by member
-        response = searchWithVCRef(memberName, testUser, vcName);
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-        node = JsonUtils.readTree(response.readEntity(String.class));
-        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
-                node.at("/errors/0/0").asInt());
-    }
-
-    private Response createUserGroup (String username, String groupName,
-            String description) throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("description", description);
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .put(Entity.form(form));
-        return response;
-    }
-
-    private JsonNode listUserGroup (String username, String groupName)
-            throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .get();
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        return node;
-    }
-
-    private void testInviteMember (String username, String groupName,
-            String memberName) throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("members", memberName);
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("invite").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // list group
-        JsonNode node = listUserGroup(username, groupName);
-        node = node.get(0);
-        assertEquals(2, node.get("members").size());
-        assertEquals(memberName, node.at("/members/1/userId").asText());
-        assertEquals(GroupMemberStatus.PENDING.name(),
-                node.at("/members/1/status").asText());
-        assertEquals(0, node.at("/members/1/roles").size());
-    }
-
-    private void subscribeToGroup (String username, String groupName)
-            throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("subscribe").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .post(Entity.form(new Form()));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-    }
-
-    private void checkMemberInGroup (String memberName, String testUser,
-            String groupName) throws KustvaktException {
-        JsonNode node = listUserGroup(testUser, groupName).get(0);
-        assertEquals(2, node.get("members").size());
-        assertEquals(memberName, node.at("/members/1/userId").asText());
-        assertEquals(GroupMemberStatus.ACTIVE.name(),
-                node.at("/members/1/status").asText());
-        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
-                node.at("/members/1/roles/1").asText());
-        assertEquals(PredefinedRole.USER_GROUP_MEMBER.name(),
-                node.at("/members/1/roles/0").asText());
-    }
-
-    private Response searchWithVCRef (String username, String vcCreator,
-            String vcName) throws KustvaktException {
-        Response response = target().path(API_VERSION).path("search")
-                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
-                .queryParam("cq",
-                        "referTo \"" + vcCreator + "/" + vcName + "\"")
-                .request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .get();
-        return response;
-    }
-}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusTestBase.java b/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusTestBase.java
deleted file mode 100644
index 04538e9..0000000
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusTestBase.java
+++ /dev/null
@@ -1,180 +0,0 @@
-package de.ids_mannheim.korap.web.controller;
-
-import static org.hamcrest.CoreMatchers.hasItem;
-import static org.hamcrest.CoreMatchers.not;
-import static org.hamcrest.MatcherAssert.assertThat;
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-import java.util.List;
-import java.util.Map.Entry;
-import java.util.Set;
-
-import jakarta.ws.rs.ProcessingException;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.Form;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.core.Response.Status;
-
-import org.apache.http.entity.ContentType;
-import org.glassfish.jersey.server.ContainerRequest;
-
-import com.fasterxml.jackson.databind.JsonNode;
-import com.google.common.net.HttpHeaders;
-
-import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
-import de.ids_mannheim.korap.config.Attributes;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.utils.JsonUtils;
-
-public abstract class VirtualCorpusTestBase extends OAuth2TestBase {
-
-    protected JsonNode retrieveVCInfo (String username, String vcCreator,
-            String vcName) throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("vc")
-                .path("~" + vcCreator).path(vcName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .get();
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-
-        return JsonUtils.readTree(entity);
-    }
-
-    protected void createVC (String authHeader, String username, String vcName,
-            String vcJson) throws KustvaktException {
-        Response response = target().path(API_VERSION).path("vc")
-                .path("~" + username).path(vcName).request()
-                .header(Attributes.AUTHORIZATION, authHeader)
-                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
-                .put(Entity.json(vcJson));
-
-        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
-    }
-
-    protected void editVC (String username, String vcCreator, String vcName,
-            String vcJson) throws KustvaktException {
-        Response response = target().path(API_VERSION).path("vc")
-                .path("~" + vcCreator).path(vcName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
-                .put(Entity.json(vcJson));
-
-        assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
-    }
-
-    protected JsonNode listVC (String username)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("vc").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .get();
-
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        return JsonUtils.readTree(entity);
-    }
-
-    protected JsonNode listVCWithAuthHeader (String authHeader)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("vc").request()
-                .header(Attributes.AUTHORIZATION, authHeader).get();
-
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        return JsonUtils.readTree(entity);
-    }
-
-    protected JsonNode testListOwnerVC (String username)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("vc")
-                .queryParam("filter-by", "own").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .get();
-
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-
-        String entity = response.readEntity(String.class);
-        return JsonUtils.readTree(entity);
-    }
-
-    protected JsonNode listSystemVC (String username) throws KustvaktException {
-        Response response = target().path(API_VERSION).path("vc")
-                .queryParam("filter-by", "system").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("pearl", "pass"))
-                .get();
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        return node;
-    }
-
-    protected Response testShareVCByCreator (String vcCreator, String vcName,
-            String groupName) throws ProcessingException, KustvaktException {
-
-        return target().path(API_VERSION).path("vc").path("~" + vcCreator)
-                .path(vcName).path("share").path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(vcCreator, "pass"))
-                .post(Entity.form(new Form()));
-    }
-
-    protected JsonNode listAccessByGroup (String username, String groupName)
-            throws KustvaktException {
-        Response response = target().path(API_VERSION).path("vc").path("access")
-                .queryParam("groupName", groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .get();
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        return node;
-    }
-
-    protected void deleteVC (String vcName, String vcCreator, String username)
-            throws KustvaktException {
-        Response response = target().path(API_VERSION).path("vc")
-                .path("~" + vcCreator).path(vcName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .delete();
-
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-    }
-
-    protected void testResponseUnauthorized (Response response, String username)
-            throws KustvaktException {
-        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
-
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-
-        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
-                node.at("/errors/0/0").asInt());
-        assertEquals("Unauthorized operation for user: " + username,
-                node.at("/errors/0/1").asText());
-
-        checkWWWAuthenticateHeader(response);
-    }
-
-    protected void checkWWWAuthenticateHeader (Response response) {
-        Set<Entry<String, List<Object>>> headers = response.getHeaders()
-                .entrySet();
-
-        for (Entry<String, List<Object>> header : headers) {
-            if (header.getKey().equals(ContainerRequest.WWW_AUTHENTICATE)) {
-                assertThat(header.getValue(),
-                        not(hasItem("Api realm=\"Kustvakt\"")));
-                assertThat(header.getValue(),
-                        hasItem("Bearer realm=\"Kustvakt\""));
-                assertThat(header.getValue(),
-                        hasItem("Basic realm=\"Kustvakt\""));
-            }
-        }
-    }
-}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerAdminTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerAdminTest.java
similarity index 69%
rename from src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerAdminTest.java
rename to src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerAdminTest.java
index aa6a4c6..c5af1a5 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerAdminTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerAdminTest.java
@@ -1,34 +1,31 @@
-package de.ids_mannheim.korap.web.controller;
+package de.ids_mannheim.korap.web.controller.usergroup;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import jakarta.ws.rs.core.Form;
-import jakarta.ws.rs.core.MediaType;
-
 import org.junit.jupiter.api.Test;
+
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.net.HttpHeaders;
-import jakarta.ws.rs.ProcessingException;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.core.Response.Status;
-import jakarta.ws.rs.client.Entity;
 
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
 import de.ids_mannheim.korap.config.Attributes;
-import de.ids_mannheim.korap.config.SpringJerseyTest;
 import de.ids_mannheim.korap.constant.GroupMemberStatus;
 import de.ids_mannheim.korap.constant.PredefinedRole;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.service.UserGroupService;
 import de.ids_mannheim.korap.utils.JsonUtils;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
 
 /**
  * @author margaretha
  */
-public class UserGroupControllerAdminTest extends SpringJerseyTest {
-
-    private String sysAdminUser = "admin";
+public class UserGroupControllerAdminTest extends UserGroupTestBase {
 
     private String testUser = "group-admin";
 
@@ -46,6 +43,19 @@
 
     @Test
     public void testListUserGroupsUsingAdminToken () throws KustvaktException {
+        createDoryGroup();
+        
+        createMarlinGroup();
+        inviteMember(marlinGroupName, "marlin", "dory");
+        subscribe(marlinGroupName, "dory");
+        
+        String testGroup = "test-group"; 
+        createUserGroup("test-group", "Test group to be deleted.", "marlin");
+        inviteMember(testGroup, "marlin", "dory");
+        subscribe(testGroup, "dory");
+        deleteGroupByName("test-group", "marlin");
+
+        
         Form f = new Form();
         f.param("username", "dory");
         f.param("token", "secret");
@@ -57,7 +67,12 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         String entity = response.readEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(3, node.size());
+        assertEquals(2, node.size());
+        
+        testListUserGroupsWithStatus();
+        
+        deleteGroupByName(doryGroupName, "dory");
+        deleteGroupByName(marlinGroupName, "marlin");
     }
 
     /**
@@ -100,24 +115,23 @@
                 node.at("/errors/0/0").asInt());
     }
 
-    @Test
-    public void testListUserGroupsWithStatus () throws KustvaktException {
+    private void testListUserGroupsWithStatus () throws KustvaktException {
         Form f = new Form();
         f.param("username", "dory");
         f.param("status", "ACTIVE");
+        
         Response response = target().path(API_VERSION).path("admin")
                 .path("group").path("list").queryParam("username", "dory")
                 .queryParam("status", "ACTIVE").request()
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "pass"))
+                                        admin, "pass"))
                 .header(HttpHeaders.CONTENT_TYPE,
                         MediaType.APPLICATION_FORM_URLENCODED)
                 .post(Entity.form(f));
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         String entity = response.readEntity(String.class);
-        // System.out.println(entity);
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(2, node.size());
     }
@@ -130,7 +144,7 @@
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "pass"))
+                                        admin, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").get();
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         String entity = response.readEntity(String.class);
@@ -145,7 +159,7 @@
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "pass"))
+                                        admin, "pass"))
                 .header(HttpHeaders.CONTENT_TYPE,
                         MediaType.APPLICATION_FORM_URLENCODED)
                 .post(null);
@@ -162,40 +176,17 @@
     }
 
     @Test
-    public void testListByStatusHidden ()
+    public void testListHiddenGroups ()
             throws ProcessingException, KustvaktException {
-        Form f = new Form();
-        f.param("status", "HIDDEN");
-        Response response = target().path(API_VERSION).path("admin")
-                .path("group").path("list").queryParam("status", "HIDDEN")
-                .request()
-                .header(Attributes.AUTHORIZATION,
-                        HttpAuthorizationHandler
-                                .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(HttpHeaders.CONTENT_TYPE,
-                        MediaType.APPLICATION_FORM_URLENCODED)
-                .post(Entity.form(f));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
+        JsonNode node = listHiddenGroup();
         assertEquals(1, node.size());
-        assertEquals(3, node.at("/0/id").asInt());
     }
 
     @Test
     public void testUserGroupAdmin ()
             throws ProcessingException, KustvaktException {
         String groupName = "admin-test-group";
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION,
-                        HttpAuthorizationHandler
-                                .createBasicAuthorizationHeaderValue(testUser,
-                                        "password"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .put(Entity.form(new Form()));
+        Response response = createUserGroup(groupName, "test group", testUser);
         assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
         // list user group
         JsonNode node = listGroup(testUser);
@@ -203,52 +194,30 @@
         node = node.get(0);
         assertEquals(groupName, node.get("name").asText());
         testInviteMember(groupName);
-        testMemberRole("marlin", groupName);
+        subscribe(groupName, "marlin");
+        testAddAdminRole(groupName, "marlin");
+        testDeleteMemberRoles(groupName, "marlin");
         testDeleteMember(groupName);
-        testDeleteGroup(groupName);
+        
+        // delete group
+        deleteGroupByName(groupName, admin);
+        // check group
+        node = listGroup(testUser);
+        assertEquals(0, node.size());
     }
 
-    private void testMemberRole (String memberUsername, String groupName)
-            throws ProcessingException, KustvaktException {
-        // accept invitation
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("subscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION,
-                        HttpAuthorizationHandler
-                                .createBasicAuthorizationHeaderValue(
-                                        memberUsername, "pass"))
-                .post(Entity.form(new Form()));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        testAddMemberRoles(groupName, memberUsername);
-        testDeleteMemberRoles(groupName, memberUsername);
-    }
 
-    private void testAddMemberRoles (String groupName, String memberUsername)
+    private void testAddAdminRole (String groupName, String memberUsername)
             throws ProcessingException, KustvaktException {
-        Form form = new Form();
-        form.param("memberUsername", memberUsername);
-        // USER_GROUP_ADMIN
-        form.param("roleId", "1");
-        // USER_GROUP_MEMBER
-        form.param("roleId", "2");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("role").path("add").request()
-                .header(Attributes.AUTHORIZATION,
-                        HttpAuthorizationHandler
-                                .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "password"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .post(Entity.form(form));
+        Response response = addAdminRole(groupName, memberUsername, admin);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
         JsonNode node = retrieveGroup(groupName).at("/members");
         JsonNode member;
         for (int i = 0; i < node.size(); i++) {
             member = node.get(i);
             if (member.at("/userId").asText().equals(memberUsername)) {
-                assertEquals(3, member.at("/roles").size());
-                assertEquals(PredefinedRole.USER_GROUP_ADMIN.name(),
-                        member.at("/roles/0").asText());
+                assertEquals(6, member.at("/privileges").size());
                 break;
             }
         }
@@ -259,14 +228,13 @@
         Form form = new Form();
         form.param("memberUsername", memberUsername);
         // USER_GROUP_ADMIN
-        form.param("roleId", "1");
+        form.param("role", PredefinedRole.GROUP_ADMIN.name());
         Response response = target().path(API_VERSION).path("group")
                 .path("@" + groupName).path("role").path("delete").request()
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "password"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                                        admin, "password"))
                 .post(Entity.form(form));
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         JsonNode node = retrieveGroup(groupName).at("/members");
@@ -274,7 +242,7 @@
         for (int i = 0; i < node.size(); i++) {
             member = node.get(i);
             if (member.at("/userId").asText().equals(memberUsername)) {
-                assertEquals(2, member.at("/roles").size());
+                assertEquals(1, member.at("/privileges").size());
                 break;
             }
         }
@@ -287,7 +255,7 @@
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "pass"))
+                                        admin, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").post(null);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         String entity = response.readEntity(String.class);
@@ -295,22 +263,6 @@
         return node;
     }
 
-    private void testDeleteGroup (String groupName)
-            throws ProcessingException, KustvaktException {
-        // delete group
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION,
-                        HttpAuthorizationHandler
-                                .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // check group
-        JsonNode node = listGroup(testUser);
-        assertEquals(0, node.size());
-    }
-
     private void testDeleteMember (String groupName)
             throws ProcessingException, KustvaktException {
         // delete marlin from group
@@ -319,7 +271,7 @@
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "pass"))
+                                        admin, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         // check group member
@@ -341,7 +293,7 @@
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "pass"))
+                                        admin, "pass"))
                 .post(Entity.form(form));
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         // list group
@@ -351,6 +303,6 @@
         assertEquals(node.at("/members/3/userId").asText(), "darla");
         assertEquals(GroupMemberStatus.PENDING.name(),
                 node.at("/members/1/status").asText());
-        assertEquals(0, node.at("/members/1/roles").size());
+        assertEquals(0, node.at("/members/1/privileges").size());
     }
 }
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerTest.java
new file mode 100644
index 0000000..4806241
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerTest.java
@@ -0,0 +1,293 @@
+package de.ids_mannheim.korap.web.controller.usergroup;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.net.HttpHeaders;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+/**
+ * @author margaretha
+ */
+public class UserGroupControllerTest extends UserGroupTestBase {
+
+    private String username = "UserGroupControllerTest";
+
+    private String admin = "admin";
+
+    
+    @Test
+    public void testCreateGroupEmptyDescription ()
+            throws ProcessingException, KustvaktException {
+        String groupName = "empty_group";
+        Response response = createUserGroup(groupName, "", username);
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+        deleteGroupByName(groupName,username);
+    }
+
+    @Test
+    public void testCreateGroupMissingDescription ()
+            throws ProcessingException, KustvaktException {
+        String groupName = "missing-desc-group";
+        Response response = testCreateGroupWithoutDescription(groupName);
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+        deleteGroupByName(groupName,username);
+    }
+
+    private Response testCreateGroupWithoutDescription (String groupName)
+            throws ProcessingException, KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("@" + groupName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .put(Entity.form(new Form()));
+        return response;
+    }
+
+    @Test
+    public void testCreateGroupInvalidName ()
+            throws ProcessingException, KustvaktException {
+        String groupName = "invalid-group-name$";
+        Response response = testCreateGroupWithoutDescription(groupName);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
+        assertEquals(StatusCodes.INVALID_ARGUMENT,
+                node.at("/errors/0/0").asInt());
+        // assertEquals("User-group name must only contains letters, numbers, "
+        // + "underscores, hypens and spaces", node.at("/errors/0/1").asText());
+        assertEquals(node.at("/errors/0/2").asText(), "invalid-group-name$");
+    }
+
+    @Test
+    public void testCreateGroupNameTooShort ()
+            throws ProcessingException, KustvaktException {
+        String groupName = "a";
+        Response response = testCreateGroupWithoutDescription(groupName);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
+        assertEquals(StatusCodes.INVALID_ARGUMENT,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "groupName must contain at least 3 characters");
+        assertEquals(node.at("/errors/0/2").asText(), "groupName");
+    }
+
+    @Test
+    public void testUserGroup () throws ProcessingException, KustvaktException {
+        String groupName = "new-user-group";
+        String description = "This is new-user-group.";
+        Response response = createUserGroup(groupName, description, username);
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+        // same name
+        response = testCreateGroupWithoutDescription(groupName);
+        assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
+        // list user group
+        JsonNode node = listUserGroups(username);
+        assertEquals(1, node.size());
+        node = node.get(0);
+        assertEquals(node.get("name").asText(), "new-user-group");
+        assertEquals(description, node.get("description").asText());
+        assertEquals(username, node.get("owner").asText());
+        assertEquals(1, node.get("members").size());
+        assertEquals(username, node.at("/members/0/userId").asText());
+        assertEquals(GroupMemberStatus.ACTIVE.name(),
+                node.at("/members/0/status").asText());
+        assertEquals(5,  node.at("/members/0/privileges").size());
+
+        testUpdateUserGroup(groupName);
+        testInviteMember(groupName, username, "darla");
+        
+        testDeleteMemberUnauthorizedByNonMember(groupName,"darla");
+        testDeleteMemberUnauthorizedByMember(groupName, "darla");
+
+        testDeleteMember(groupName, username);
+        testDeleteGroup(groupName,username);
+//        testSubscribeToDeletedGroup(groupName);
+//        testUnsubscribeToDeletedGroup(groupName);
+    }
+    
+    private void testUpdateUserGroup (String groupName)
+            throws ProcessingException, KustvaktException {
+        String description = "Description is updated.";
+        Response response = createUserGroup(groupName, description, username);
+        assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
+        JsonNode node = listUserGroups(username);
+        assertEquals(1, node.size());
+        assertEquals(description, node.get(0).get("description").asText());
+    }
+
+    private void testDeleteMember (String groupName, String username)
+            throws ProcessingException, KustvaktException {
+        // delete darla from group
+        deleteMember(groupName, "darla", username);
+        // check group member
+        JsonNode node = listUserGroups(username);
+        node = node.get(0);
+        assertEquals(1, node.get("members").size());
+    }
+
+    private void testDeleteMemberUnauthorizedByNonMember (String groupName,
+            String memberName) throws ProcessingException, KustvaktException {
+
+        Response response = deleteMember(groupName, memberName, "nemo");
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "Unauthorized operation for user: nemo");
+    }
+    
+    private void testDeleteMemberUnauthorizedByMember (String groupName,
+            String memberName) throws ProcessingException, KustvaktException {
+        inviteMember(groupName, "dory", "nemo");
+        subscribe(groupName, "nemo");
+        // nemo is a group member
+        Response response = deleteMember(groupName, memberName, "nemo");
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "Unauthorized operation for user: nemo");
+    }
+
+    @Test
+    public void testDeletePendingMember ()
+            throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        inviteMember(doryGroupName, "dory", "pearl");
+        // dory delete pearl
+        Response response = deleteMember(doryGroupName, "pearl", "dory");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        // check member
+        JsonNode node = listUserGroups("pearl");
+        assertEquals(0, node.size());
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+
+    @Test
+    public void testDeleteDeletedMember ()
+            throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        inviteMember(doryGroupName, "dory", "pearl");
+        subscribe(doryGroupName, "pearl");
+        deleteMember(doryGroupName, "pearl", "pearl");
+        
+        Response response = deleteMember(doryGroupName, "pearl", "pearl");
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.GROUP_MEMBER_DELETED,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "pearl has already been deleted from the group dory-group");
+        assertEquals(node.at("/errors/0/2").asText(), "[pearl, dory-group]");
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+
+    private void testDeleteGroup (String groupName, String username)
+            throws ProcessingException, KustvaktException {
+        deleteGroupByName(groupName, username);
+        Form f = new Form();
+        f.param("username", username);
+        f.param("status", "DELETED");
+        // EM: this is so complicated because the group retrieval are not allowed
+        // for delete groups
+        // check group
+        Response response = target().path(API_VERSION).path("admin").path("group")
+                .path("list").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(admin, "pass"))
+                .header(HttpHeaders.CONTENT_TYPE,
+                        MediaType.APPLICATION_FORM_URLENCODED)
+                .post(Entity.form(f));
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        for (int j = 0; j < node.size(); j++) {
+            JsonNode group = node.get(j);
+            // check group members
+            for (int i = 0; i < group.at("/0/members").size(); i++) {
+                assertEquals(GroupMemberStatus.DELETED.name(),
+                        group.at("/0/members/" + i + "/status").asText());
+            }
+        }
+    }
+
+    @Test
+    public void testDeleteGroupUnauthorized ()
+            throws ProcessingException, KustvaktException {
+        createMarlinGroup();
+        inviteMember(marlinGroupName, "marlin", "dory");
+        subscribe(marlinGroupName, "dory");
+        
+        addAdminRole(marlinGroupName, "dory", "marlin");
+        
+        // dory is a group admin in marlin-group
+        Response response = target().path(API_VERSION).path("group")
+                .path("@marlin-group").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("dory", "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
+        String entity = response.readEntity(String.class);
+        // System.out.println(entity);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "Unauthorized operation for user: dory");
+        
+        deleteGroupByName(marlinGroupName, "marlin");
+    }
+
+    @Test
+    public void testDeleteDeletedGroup ()
+            throws ProcessingException, KustvaktException {
+        createMarlinGroup();
+        deleteGroupByName(marlinGroupName, "marlin");
+        Response response = deleteGroupByName(marlinGroupName, "marlin");
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+    }
+
+    @Test
+    public void testDeleteGroupOwner ()
+            throws ProcessingException, KustvaktException {
+        createMarlinGroup();
+        // delete marlin from marlin-group
+        // dory is a group admin in marlin-group
+        Response response = target().path(API_VERSION).path("group")
+                .path("@marlin-group").path("~marlin").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("dory", "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").delete();
+        String entity = response.readEntity(String.class);
+        // System.out.println(entity);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.NOT_ALLOWED, node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "Operation 'delete group owner'is not allowed.");
+        deleteGroupByName(marlinGroupName, "marlin");
+    }
+}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupListTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupListTest.java
new file mode 100644
index 0000000..3344bf9
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupListTest.java
@@ -0,0 +1,70 @@
+package de.ids_mannheim.korap.web.controller.usergroup;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.net.HttpHeaders;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+public class UserGroupListTest extends UserGroupTestBase{
+
+    @Test
+    public void testListDoryGroups () throws KustvaktException {
+        createDoryGroup();
+        inviteMember(doryGroupName, "dory", "marlin");
+        inviteMember(doryGroupName, "dory", "nemo");
+        
+        JsonNode node = listUserGroups("dory");
+        JsonNode group = node.get(0);
+        assertEquals(group.at("/name").asText(), "dory-group");
+        assertEquals(group.at("/owner").asText(), "dory");
+        assertEquals(3, group.at("/members").size());
+        
+        testListNemoGroups();
+        testListMarlinGroups();
+        
+        deleteGroupByName(doryGroupName,"dory");
+        deleteGroupByName(marlinGroupName, "marlin");
+    }
+    
+    public void testListNemoGroups () throws KustvaktException {
+        Response response = subscribe(doryGroupName, "nemo");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = listUserGroups("nemo");
+        assertEquals(node.at("/0/name").asText(), "dory-group");
+        assertEquals(node.at("/0/owner").asText(), "dory");
+        // group members are not allowed to see other members
+        assertTrue(node.at("/0/members").isMissingNode());
+        System.out.println(node.toPrettyString());
+    }
+    
+    // marlin has 2 groups
+    public void testListMarlinGroups () throws KustvaktException {
+        createMarlinGroup();
+        Response response = subscribe(doryGroupName, "marlin");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = listUserGroups("marlin");
+        assertEquals(2, node.size());
+    }
+    
+    @Test
+    public void testListGroupGuest () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").get();
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "Unauthorized operation for user: guest");
+    }
+}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupMemberTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupMemberTest.java
new file mode 100644
index 0000000..b12ff69
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupMemberTest.java
@@ -0,0 +1,179 @@
+package de.ids_mannheim.korap.web.controller.usergroup;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.Set;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.net.HttpHeaders;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.dao.UserGroupMemberDao;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.UserGroupMember;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+public class UserGroupMemberTest extends UserGroupTestBase {
+
+    @Autowired
+    private UserGroupMemberDao memberDao;
+
+    @Test
+    public void testInvitePendingMember ()
+            throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        inviteMember(doryGroupName, "dory", "marlin");
+        
+        // marlin has status PENDING in dory-group
+        Response response = inviteMember(doryGroupName, "dory", "marlin");
+        String entity = response.readEntity(String.class);
+        // System.out.println(entity);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.GROUP_MEMBER_EXISTS,
+                node.at("/errors/0/0").asInt());
+        assertEquals(
+                "Username marlin with status PENDING exists in the user-group "
+                        + "dory-group",
+                node.at("/errors/0/1").asText());
+        assertEquals(node.at("/errors/0/2").asText(),
+                "[marlin, PENDING, dory-group]");
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+
+    @Test
+    public void testInviteActiveMember ()
+            throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        inviteMember(doryGroupName, "dory", "nemo");
+        subscribe(doryGroupName, "nemo");
+        // nemo has status active in dory-group
+        Form form = new Form();
+        form.param("members", "nemo");
+        Response response = target().path(API_VERSION).path("group")
+                .path("@dory-group").path("invite").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("dory", "pass"))
+                .post(Entity.form(form));
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.GROUP_MEMBER_EXISTS,
+                node.at("/errors/0/0").asInt());
+        assertEquals(
+                "Username nemo with status ACTIVE exists in the user-group "
+                        + "dory-group",
+                node.at("/errors/0/1").asText());
+        assertEquals(node.at("/errors/0/2").asText(),
+                "[nemo, ACTIVE, dory-group]");
+        
+        deleteGroupByName(doryGroupName, "dory");
+        
+        testInviteMemberToDeletedGroup();
+    }
+
+    private void testInviteMemberToDeletedGroup () throws KustvaktException {
+        Response response = inviteMember(doryGroupName, "dory", "nemo");
+
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+//        String entity = response.readEntity(String.class);
+//        JsonNode node = JsonUtils.readTree(entity);
+//        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
+//        assertEquals(node.at("/errors/0/1").asText(),
+//                "Group deleted-group has been deleted.");
+//        assertEquals(node.at("/errors/0/2").asText(), "deleted-group");
+    }
+    
+//    @Deprecated
+//    @Test
+//    public void testAddMutipleRoles () throws KustvaktException {
+//        createDoryGroup();
+//        inviteMember(doryGroupName, "dory", "marlin");
+//        subscribe(doryGroupName, "marlin");
+//        JsonNode marlinGroup = listUserGroups("marlin");
+//        int groupId = marlinGroup.at("/0/id").asInt();
+//        
+//        Form form = new Form();
+//        form.param("memberUsername", "marlin");
+//        form.param("role", PredefinedRole.GROUP_ADMIN.name());
+//        form.param("role", PredefinedRole.QUERY_ACCESS.name());
+//        addMemberRole(doryGroupName, "dory", form);
+//        
+//        UserGroupMember member = memberDao.retrieveMemberById("marlin",
+//                groupId);
+//        Set<Role> roles = member.getRoles();
+//        assertEquals(6, roles.size());
+//        
+//        deleteGroupByName(doryGroupName, "dory");
+//    }
+    
+    @Test
+    public void testAddMemberRole () throws KustvaktException {
+        createMarlinGroup();
+        inviteMember(marlinGroupName, "marlin", "dory");
+        subscribe(marlinGroupName, "dory");
+        JsonNode marlinGroup = listUserGroups("marlin");
+        int groupId = marlinGroup.at("/0/id").asInt();
+        
+        UserGroupMember member = memberDao.retrieveMemberById("dory", groupId);
+        Set<Role> roles = member.getRoles();
+        assertEquals(1, roles.size());
+        
+        Response response = addAdminRole(marlinGroupName, "dory", "marlin");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        member = memberDao.retrieveMemberById("dory", groupId);
+        roles = member.getRoles();
+        assertEquals(6, roles.size());
+        
+        testAddSameMemberRole(groupId);
+        testDeleteMemberRole(groupId);
+
+        deleteGroupByName(marlinGroupName, "marlin");
+    }
+
+    private void testAddSameMemberRole (int groupId)
+            throws ProcessingException, KustvaktException {
+        Response response = addAdminRole(marlinGroupName, "dory", "marlin");
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.GROUP_ADMIN_EXISTS,
+                node.at("/errors/0/0").asInt());
+        
+        UserGroupMember member = memberDao.retrieveMemberById("dory", groupId);
+        Set<Role> roles = member.getRoles();
+        assertEquals(6, roles.size());
+    }
+
+    private void testDeleteMemberRole (int groupId)
+            throws ProcessingException, KustvaktException {
+        Form form = new Form();
+        form.param("memberUsername", "dory");
+        form.param("role", PredefinedRole.GROUP_ADMIN.name());
+        Response response = target().path(API_VERSION).path("group")
+                .path("@marlin-group").path("role").path("delete").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
+                .post(Entity.form(form));
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        UserGroupMember member = memberDao.retrieveMemberById("dory", groupId);
+        Set<Role> roles = member.getRoles();
+        assertEquals(1, roles.size());
+    }
+}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupSubscriptionTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupSubscriptionTest.java
new file mode 100644
index 0000000..5ffd446
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupSubscriptionTest.java
@@ -0,0 +1,268 @@
+package de.ids_mannheim.korap.web.controller.usergroup;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.net.HttpHeaders;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+public class UserGroupSubscriptionTest extends UserGroupTestBase {
+    
+    @Test
+    public void testSubscribeNonExistentMember () throws KustvaktException {
+        createDoryGroup();
+        
+        Response response = subscribe(doryGroupName, "bruce");
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.GROUP_MEMBER_NOT_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "bruce is not found in the group");
+        
+        testSubscribeDeletedMember();
+        deleteGroupByName(doryGroupName, "dory");
+    }
+    
+    // pearl has GroupMemberStatus.DELETED in dory-group
+    private void testSubscribeDeletedMember () throws KustvaktException {
+        inviteMember(doryGroupName, "dory", "pearl");
+        // delete pending member
+        deleteMember(doryGroupName, "pearl", "dory");
+        
+        Response response = subscribe(doryGroupName, "pearl");
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.GROUP_MEMBER_DELETED,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "pearl has already been deleted from the group dory-group");
+        
+        testUnsubscribeDeletedMember();
+        testInviteDeletedMember("pearl", "dory");
+    }
+    
+    // marlin has GroupMemberStatus.PENDING in dory-group
+    @Test
+    public void testSubscribePendingMember () throws KustvaktException {
+        createDoryGroup();
+        testInviteMember(doryGroupName, "dory", "marlin");
+        Response response = subscribe(doryGroupName, "marlin");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        // retrieve marlin group
+        JsonNode node = listUserGroups("marlin");
+        assertEquals(1, node.size());
+        JsonNode group = node.get(0);
+        assertEquals(group.at("/name").asText(), "dory-group");
+        assertEquals(group.at("/owner").asText(), "dory");
+        // group members are not allowed to see other members
+        assertEquals(0, group.at("/members").size());
+        assertEquals(GroupMemberStatus.ACTIVE.name(),
+                group.at("/userMemberStatus").asText());
+        
+        assertEquals(1, group.at("/userPrivileges").size());
+        
+        // unsubscribe marlin from dory-group
+        testUnsubscribeActiveMember("dory-group");
+        checkGroupMemberRole("dory-group", "marlin");
+        testInviteDeletedMember("marlin", "dory");
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+    
+    private void testInviteDeletedMember (String invitee, String invitor)
+            throws ProcessingException, KustvaktException {
+        
+        Response response = inviteMember(doryGroupName, invitor, invitee);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        // check member
+        JsonNode node = listUserGroups(invitee);
+        assertEquals(1, node.size());
+        JsonNode group = node.get(0);
+        assertEquals(GroupMemberStatus.PENDING.name(),
+                group.at("/userMemberStatus").asText());
+//        testDeletePendingMember();
+    }
+    
+    private void checkGroupMemberRole (String groupName,
+            String deletedMemberName) throws KustvaktException {
+        Response response = target().path(API_VERSION).path("admin")
+                .path("group").path("@" + groupName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("admin", "pass"))
+                .post(null);
+        String entity = response.readEntity(String.class);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(entity).at("/members");
+        JsonNode member;
+        for (int i = 0; i < node.size(); i++) {
+            member = node.get(i);
+            if (deletedMemberName.equals(member.at("/userId").asText())) {
+                assertEquals(0, node.at("/privileges").size());
+                break;
+            }
+        }
+    }
+    
+
+    @Test
+    public void testSubscribeMissingGroupName () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("subscribe").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("bruce", "pass"))
+                .post(Entity.form(new Form()));
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+    }
+
+
+    @Test
+    public void testSubscribeToNonExistentGroup () throws KustvaktException {
+        Response response = subscribe("non-existent", "pearl");
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "Group non-existent is not found");
+    }
+
+    @Test
+    public void testSubscribeToDeletedGroup ()
+            throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        // hard delete
+        deleteGroupByName(doryGroupName, "dory");
+        
+        Response response = subscribe(doryGroupName, "nemo");
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+//        String entity = response.readEntity(String.class);
+//        JsonNode node = JsonUtils.readTree(entity);
+//        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
+//        assertEquals(node.at("/errors/0/1").asText(),
+//                "Group new-user-group has been deleted.");
+        testUnsubscribeToDeletedGroup(doryGroupName);
+        
+    }
+
+    private void testUnsubscribeToDeletedGroup (String groupName)
+            throws ProcessingException, KustvaktException {
+        Response response = unsubscribe(doryGroupName, "nemo");
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+//        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+//        String entity = response.readEntity(String.class);
+//        JsonNode node = JsonUtils.readTree(entity);
+//        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
+//        assertEquals(node.at("/errors/0/1").asText(),
+//                "Group new-user-group has been deleted.");
+    }
+
+    private void testUnsubscribeActiveMember (String groupName)
+            throws ProcessingException, KustvaktException {
+        Response response = unsubscribe(groupName, "marlin");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = listUserGroups("marlin");
+        assertEquals(0, node.size());
+    }
+    
+    @Test
+    public void testUnsubscribePendingMember ()
+            throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        testInviteMember(doryGroupName, "dory", "marlin");
+        JsonNode node = listUserGroups("marlin");
+        assertEquals(1, node.size());
+
+        Response response = unsubscribe(doryGroupName, "marlin");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        node = listUserGroups("marlin");
+        assertEquals(0, node.size());
+        // invite marlin to dory-group to set back the
+        // GroupMemberStatus.PENDING
+        testInviteDeletedMember("marlin","dory");
+        deleteGroupByName(doryGroupName, "dory");
+    }
+
+    private void testUnsubscribeDeletedMember ()
+            throws ProcessingException, KustvaktException {
+        // pearl unsubscribes from dory-group
+        Response response = unsubscribe(doryGroupName, "pearl");
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.GROUP_MEMBER_DELETED,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "pearl has already been deleted from the group dory-group");
+        assertEquals(node.at("/errors/0/2").asText(), "[pearl, dory-group]");
+    }
+
+
+    @Test
+    public void testUnsubscribeMissingGroupName () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("unsubscribe").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
+                .delete();
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+    }
+
+    @Test
+    public void testUnsubscribeNonExistentMember () throws KustvaktException {
+        createDoryGroup();
+        Response response = target().path(API_VERSION).path("group")
+                .path("@dory-group").path("unsubscribe").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("bruce", "pass"))
+                .delete();
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.GROUP_MEMBER_NOT_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "bruce is not found in the group");
+        deleteGroupByName(doryGroupName, "dory");
+    }
+
+    @Test
+    public void testUnsubscribeToNonExistentGroup () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("@tralala-group").path("unsubscribe").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("pearl", "pass"))
+                .delete();
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "Group tralala-group is not found");
+    }
+
+}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupTestBase.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupTestBase.java
new file mode 100644
index 0000000..4a5c6bd
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupTestBase.java
@@ -0,0 +1,196 @@
+package de.ids_mannheim.korap.web.controller.usergroup;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.net.HttpHeaders;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.web.controller.OAuth2TestBase;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.MediaType;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+public abstract class UserGroupTestBase extends OAuth2TestBase {
+
+    protected String doryGroupName = "dory-group";
+    protected String marlinGroupName = "marlin-group";
+    protected String admin = "admin";
+
+    protected Response createUserGroup (String groupName, String description,
+            String username) throws ProcessingException, KustvaktException {
+        Form form = new Form();
+        form.param("description", description);
+        Response response = target().path(API_VERSION).path("group")
+                .path("@" + groupName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .put(Entity.form(form));
+        return response;
+    }
+
+    protected Response deleteGroupByName (String groupName,String username)
+            throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("@" + groupName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .delete();
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        return response;
+    }
+
+    protected JsonNode listUserGroups (String username)
+            throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .get();
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        return node;
+    }
+
+    protected Response inviteMember (String groupName, String invitor,
+            String invitee) throws KustvaktException {
+        Form form = new Form();
+        form.param("members", invitee);
+        Response response = target().path(API_VERSION).path("group")
+                .path("@" + groupName).path("invite").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(invitor, "pass"))
+                .post(Entity.form(form));
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        return response;
+    }
+    
+    protected void testInviteMember (String groupName, String invitor,
+            String invitee)
+            throws ProcessingException, KustvaktException {
+        Response response = inviteMember(groupName, invitor, invitee);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        // list group
+        JsonNode node = listUserGroups(invitor);
+        node = node.get(0);
+        assertEquals(2, node.get("members").size());
+        assertEquals(node.at("/members/1/userId").asText(), invitee);
+        assertEquals(GroupMemberStatus.PENDING.name(),
+                node.at("/members/1/status").asText());
+        assertEquals(0, node.at("/members/1/privileges").size());
+    }
+
+    protected Response subscribe (String groupName, String username)
+            throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("@"+groupName).path("subscribe").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .post(Entity.form(new Form()));
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        return response;
+    }
+    
+    protected Response unsubscribe (String groupName, String username)
+            throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("@" + groupName).path("unsubscribe").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .delete();
+        return response;
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+
+    protected Response addAdminRole (String groupName, String memberName,
+            String addedBy) throws KustvaktException {
+        Form form = new Form();
+        form.param("memberUsername", memberName);
+        form.param("role", PredefinedRole.GROUP_ADMIN.name());
+
+        Response response = target().path(API_VERSION).path("group")
+                .path("@" + groupName).path("role").path("add").path("admin")
+                .request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(addedBy, "pass"))
+                .post(Entity.form(form));
+        return response;
+    }
+
+    protected Response deleteMember (String groupName, String memberName,
+            String deletedBy) throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("@" + groupName).path("~"+memberName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(deletedBy, "pass"))
+                .delete();
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        return response;
+    }
+    
+    protected JsonNode createDoryGroup ()
+            throws ProcessingException, KustvaktException {
+        Response response = createUserGroup(doryGroupName,
+                "This is dory-group.", "dory");
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        return node;
+    }
+
+    protected JsonNode createMarlinGroup () throws KustvaktException {
+        Response response = createUserGroup(marlinGroupName,
+                "This is marlin-group.", "marlin");
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        return node;
+    }
+    
+    protected JsonNode getHiddenGroup (String queryName)
+            throws KustvaktException {
+        Form f = new Form();
+        f.param("queryName", queryName);
+        Response response = target().path(API_VERSION).path("admin")
+                .path("group").path("hidden").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("admin", "pass"))
+                .header(HttpHeaders.CONTENT_TYPE,
+                        MediaType.APPLICATION_FORM_URLENCODED)
+                .post(Entity.form(f));
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        return node;
+    }
+    
+    protected JsonNode listHiddenGroup () throws KustvaktException {
+        Form f = new Form();
+        f.param("status", "HIDDEN");
+        Response response = target().path(API_VERSION).path("admin")
+                .path("group").path("list")
+                .request()
+                .header(Attributes.AUTHORIZATION,
+                        HttpAuthorizationHandler
+                                .createBasicAuthorizationHeaderValue(
+                                        "admin", "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        MediaType.APPLICATION_FORM_URLENCODED)
+                .post(Entity.form(f));
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        return node;
+    }
+
+}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusAccessTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusAccessTest.java
new file mode 100644
index 0000000..8eca508
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusAccessTest.java
@@ -0,0 +1,48 @@
+package de.ids_mannheim.korap.web.controller.vc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.ResourceType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+public class VirtualCorpusAccessTest extends VirtualCorpusTestBase {
+
+    private String testUser = "VirtualCorpusAccessTest";
+
+
+
+    private void testShareVC_nonUniqueAccess (String vcCreator, String vcName,
+            String groupName) throws ProcessingException, KustvaktException {
+        Response response = shareVCByCreator(vcCreator, vcName, groupName);
+        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
+        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatus());
+        assertEquals(StatusCodes.DB_INSERT_FAILED,
+                node.at("/errors/0/0").asInt());
+        // EM: message differs depending on the database used
+        // for testing. The message below is from sqlite.
+        // assertTrue(node.at("/errors/0/1").asText()
+        // .startsWith("[SQLITE_CONSTRAINT_UNIQUE]"));
+    }
+
+    @Test
+    public void testDeleteNonExistingAccess ()
+            throws ProcessingException, KustvaktException {
+        Response response = deleteAccess("dory", "100");
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+    }
+}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusClientTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusClientTest.java
similarity index 98%
rename from src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusClientTest.java
rename to src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusClientTest.java
index a3b7e12..00ad027 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusClientTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusClientTest.java
@@ -1,4 +1,4 @@
-package de.ids_mannheim.korap.web.controller;
+package de.ids_mannheim.korap.web.controller.vc;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerAdminTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusControllerAdminTest.java
similarity index 80%
rename from src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerAdminTest.java
rename to src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusControllerAdminTest.java
index 90f9d4a..673e1cd 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerAdminTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusControllerAdminTest.java
@@ -1,4 +1,4 @@
-package de.ids_mannheim.korap.web.controller;
+package de.ids_mannheim.korap.web.controller.vc;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
@@ -161,34 +161,31 @@
     // 
     // return node.at("/accessId").asText();
     // }
-    private JsonNode testlistAccessByGroup (String groupName)
-            throws KustvaktException {
-        Response response = target().path(API_VERSION).path("vc").path("access")
-                .queryParam("groupName", groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(admin, "pass"))
-                .get();
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(2, node.size());
-        return node.get(node.size() - 1);
-    }
 
     @Test
-    public void testVCSharing () throws ProcessingException, KustvaktException {
+    public void testShareVC () throws ProcessingException, KustvaktException {
+        createMarlinGroup();
+        
         String vcCreator = "marlin";
         String vcName = "marlin-vc";
         String groupName = "marlin-group";
         JsonNode node2 = testAdminListVC_UsingAdminToken(vcCreator,
                 ResourceType.PROJECT);
         assertEquals(0, node2.size());
-        testCreateVCAccess(vcCreator, vcName, groupName);
-        JsonNode node = testlistAccessByGroup(groupName);
-        String accessId = node.at("/accessId").asText();
-        testDeleteVCAccess(accessId);
+        createAccess(vcCreator, vcName, groupName, admin);
+        
+        JsonNode node = listRolesByGroup("admin",groupName);
+        assertEquals(1, node.size());
+        String roleId = node.at("/0/roleId").asText();
         node2 = testAdminListVC_UsingAdminToken(vcCreator,
                 ResourceType.PROJECT);
         assertEquals(1, node2.size());
+        
+        Response response = deleteAccess("admin",roleId);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        node = listRolesByGroup("admin",groupName);
+        assertEquals(0, node.size());
+        
         String json = "{\"type\": \"" + ResourceType.PRIVATE + "\"}";
         editVC(admin, vcCreator, vcName, json);
         node = retrieveVCInfo(admin, vcCreator, vcName);
@@ -197,27 +194,7 @@
         node2 = testAdminListVC_UsingAdminToken(vcCreator,
                 ResourceType.PROJECT);
         assertEquals(0, node2.size());
-    }
-
-    private void testCreateVCAccess (String vcCreator, String vcName,
-            String groupName) throws ProcessingException, KustvaktException {
-        Response response;
-        // share VC
-        response = target().path(API_VERSION).path("vc").path("~" + vcCreator)
-                .path(vcName).path("share").path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(admin, "pass"))
-                .post(Entity.form(new Form()));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-    }
-
-    private void testDeleteVCAccess (String accessId)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("vc").path("access")
-                .path(accessId).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(admin, "pass"))
-                .delete();
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        deleteGroupByName(marlinGroupName, "marlin");
     }
 }
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusControllerTest.java
similarity index 83%
rename from src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
rename to src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusControllerTest.java
index a9ff786..13ce875 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusControllerTest.java
@@ -1,4 +1,4 @@
-package de.ids_mannheim.korap.web.controller;
+package de.ids_mannheim.korap.web.controller.vc;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -8,15 +8,12 @@
 import java.io.InputStream;
 import java.io.InputStreamReader;
 
-import jakarta.ws.rs.ProcessingException;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.core.Response.Status;
-
 import org.apache.http.entity.ContentType;
 import org.junit.jupiter.api.Test;
+
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.net.HttpHeaders;
+
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
 import de.ids_mannheim.korap.config.Attributes;
 import de.ids_mannheim.korap.constant.AuthenticationScheme;
@@ -24,6 +21,10 @@
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.utils.JsonUtils;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
 
 /**
  * @author margaretha
@@ -40,66 +41,103 @@
     }
 
     @Test
+    public void testDeleteVC_unauthorized () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("vc").path("~dory")
+                .path("dory-vc").request()
+                .header(Attributes.AUTHORIZATION, authHeader).delete();
+        testResponseUnauthorized(response, testUser);
+    }
+    
+    private void testDeleteSystemVC (String vcName) throws KustvaktException {
+        Response response = deleteVC(vcName, "system", "admin");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+
+    private void testDeleteSystemVC_unauthorized (String vcName,
+            String username) throws KustvaktException {
+        Response response = deleteVC(vcName, "system", username);
+        testResponseUnauthorized(response, "dory");
+    }
+    
+    @Test
     public void testCreatePrivateVC () throws KustvaktException {
-        String json = "{\"type\": \"PRIVATE\""
-                + ",\"queryType\": \"VIRTUAL_CORPUS\""
-                + ",\"corpusQuery\": \"corpusSigle=GOE\"}";
-        createVC(authHeader, testUser, "new_vc", json);
+        createPrivateVC(testUser, "new_vc");
+        
         // list user VC
         JsonNode node = listVC(testUser);
         assertEquals(2, node.size());
         assertEquals(node.get(1).get("name").asText(), "new_vc");
+        
+        testCreateVC_sameName(testUser, "new_vc", ResourceType.PRIVATE);
+        
         // delete new VC
         deleteVC("new_vc", testUser, testUser);
         // list VC
         node = listVC(testUser);
         assertEquals(1, node.size());
     }
-
+    
     @Test
-    public void testCreatePublishedVC () throws KustvaktException {
-        String json = "{\"type\": \"PUBLISHED\""
+    public void testCreateSystemVC () throws KustvaktException {
+        String json = "{\"type\": \"SYSTEM\""
                 + ",\"queryType\": \"VIRTUAL_CORPUS\""
-                + ",\"corpusQuery\": \"corpusSigle=GOE\"}";
-        String vcName = "new-published-vc";
-        createVC(authHeader, testUser, vcName, json);
-        // test list owner vc
-        JsonNode node = retrieveVCInfo(testUser, testUser, vcName);
-        assertEquals(vcName, node.get("name").asText());
-        // EM: check hidden access
-        node = listAccessByGroup("admin", "");
-        node = node.get(node.size() - 1);
-        assertEquals(node.at("/createdBy").asText(), "system");
-        assertEquals(vcName, node.at("/queryName").asText());
-        assertTrue(node.at("/userGroupName").asText().startsWith("auto"));
-        assertEquals(vcName, node.at("/queryName").asText());
-        String groupName = node.at("/userGroupName").asText();
-        // EM: check if hidden group has been created
-        node = testCheckHiddenGroup(groupName);
-        assertEquals(node.at("/status").asText(), "HIDDEN");
-        // EM: delete vc
-        deleteVC(vcName, testUser, testUser);
-        // EM: check if the hidden groups are deleted as well
-        node = testCheckHiddenGroup(groupName);
-        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
-                node.at("/errors/0/0").asInt());
-        assertEquals("Group " + groupName + " is not found",
-                node.at("/errors/0/1").asText());
-    }
-
-    private JsonNode testCheckHiddenGroup (String groupName)
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("admin")
-                .path("group").path("@" + groupName).request()
+                + ",\"corpusQuery\": \"pubDate since 1820\"}";
+        String vcName = "new_system_vc";
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~system").path(vcName).request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("admin", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").post(null);
-        String entity = response.readEntity(String.class);
-        return JsonUtils.readTree(entity);
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .put(Entity.json(json));
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+        JsonNode node = listSystemVC("pearl");
+        assertEquals(2, node.size());
+        assertEquals(ResourceType.SYSTEM.displayName(),
+                node.at("/0/type").asText());
+        assertEquals(ResourceType.SYSTEM.displayName(),
+                node.at("/1/type").asText());
+        
+        testDeleteSystemVC_unauthorized(vcName, "dory");
+        testDeleteSystemVC(vcName);
+        
+        node = listSystemVC("pearl");
+        assertEquals(1, node.size());
     }
 
     @Test
-    public void testCreateVCWithInvalidToken ()
+    public void testCreateSystemVC_unauthorized () throws KustvaktException {
+        String json = "{\"type\": \"SYSTEM\""
+                + ",\"queryType\": \"VIRTUAL_CORPUS\""
+                + ",\"corpusQuery\": \"creationDate since 1820\"}";
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~" + testUser).path("new_vc").request()
+                .header(Attributes.AUTHORIZATION, authHeader)
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .put(Entity.json(json));
+        testResponseUnauthorized(response, testUser);
+    }
+
+    
+    private void testCreateVC_sameName (String username, String vcName,
+            ResourceType vcType) throws KustvaktException {
+        String vcJson = "{\"type\": \"" + vcType + "\""
+                + ",\"queryType\": \"VIRTUAL_CORPUS\""
+                + ",\"corpusQuery\": \"corpusSigle=GOE\"}";
+
+        String authHeader = HttpAuthorizationHandler
+                .createBasicAuthorizationHeaderValue(username, "pass");
+
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~" + username).path(vcName).request()
+                .header(Attributes.AUTHORIZATION, authHeader)
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .put(Entity.json(vcJson));
+
+        assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
+    }
+    
+    @Test
+    public void testCreateVC_invalidToken ()
             throws IOException, KustvaktException {
         String json = "{\"type\": \"PRIVATE\","
                 + "\"corpusQuery\": \"corpusSigle=GOE\"}";
@@ -129,7 +167,7 @@
     }
 
     @Test
-    public void testCreateVCWithExpiredToken ()
+    public void testCreateVC_expiredToken ()
             throws IOException, KustvaktException {
         String json = "{\"type\": \"PRIVATE\","
                 + "\"corpusQuery\": \"corpusSigle=GOE\"}";
@@ -156,43 +194,6 @@
     }
 
     @Test
-    public void testCreateSystemVC () throws KustvaktException {
-        String json = "{\"type\": \"SYSTEM\""
-                + ",\"queryType\": \"VIRTUAL_CORPUS\""
-                + ",\"corpusQuery\": \"pubDate since 1820\"}";
-        String vcName = "new_system_vc";
-        Response response = target().path(API_VERSION).path("vc")
-                .path("~system").path(vcName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("admin", "pass"))
-                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
-                .put(Entity.json(json));
-        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
-        JsonNode node = listSystemVC("pearl");
-        assertEquals(2, node.size());
-        assertEquals(ResourceType.SYSTEM.displayName(),
-                node.at("/0/type").asText());
-        assertEquals(ResourceType.SYSTEM.displayName(),
-                node.at("/1/type").asText());
-        deleteVC(vcName, "system", "admin");
-        node = listSystemVC("pearl");
-        assertEquals(1, node.size());
-    }
-
-    @Test
-    public void testCreateSystemVC_unauthorized () throws KustvaktException {
-        String json = "{\"type\": \"SYSTEM\""
-                + ",\"queryType\": \"VIRTUAL_CORPUS\""
-                + ",\"corpusQuery\": \"creationDate since 1820\"}";
-        Response response = target().path(API_VERSION).path("vc")
-                .path("~" + testUser).path("new_vc").request()
-                .header(Attributes.AUTHORIZATION, authHeader)
-                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
-                .put(Entity.json(json));
-        testResponseUnauthorized(response, testUser);
-    }
-
-    @Test
     public void testCreateVC_invalidName () throws KustvaktException {
         String json = "{\"type\": \"PRIVATE\""
                 + ",\"queryType\": \"VIRTUAL_CORPUS\""
@@ -326,7 +327,7 @@
                 + ",\"queryType\": \"VIRTUAL_CORPUS\""
                 + ",\"corpusQuery\": \"corpusSigle=GOE\"}";
         for (int i = 1; i < 6; i++) {
-            createVC(authHeader, testUser, "new_vc_" + i, json);
+            createPrivateVC(testUser, "new_vc_" + i);
         }
         Response response = target().path(API_VERSION).path("vc")
                 .path("~" + testUser).path("new_vc_6").request()
@@ -356,14 +357,6 @@
     }
 
     @Test
-    public void testDeleteVC_unauthorized () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("vc").path("~dory")
-                .path("dory-vc").request()
-                .header(Attributes.AUTHORIZATION, authHeader).delete();
-        testResponseUnauthorized(response, testUser);
-    }
-
-    @Test
     public void testEditVC () throws KustvaktException {
         // 1st edit
         String json = "{\"description\": \"edited vc\"}";
@@ -443,37 +436,4 @@
                 node.at("/errors/0/1").asText());
         checkWWWAuthenticateHeader(response);
     }
-
-    @Test
-    public void testPublishProjectVC () throws KustvaktException {
-        String vcName = "group-vc";
-        // check the vc type
-        JsonNode node = retrieveVCInfo("dory", "dory", vcName);
-        assertEquals(ResourceType.PROJECT.displayName(),
-                node.get("type").asText());
-        // edit vc
-        String json = "{\"type\": \"PUBLISHED\"}";
-        editVC("dory", "dory", vcName, json);
-        // check VC
-        node = testListOwnerVC("dory");
-        JsonNode n = node.get(1);
-        assertEquals(ResourceType.PUBLISHED.displayName(),
-                n.get("type").asText());
-        // check hidden VC access
-        node = listAccessByGroup("admin", "");
-        assertEquals(4, node.size());
-        node = node.get(node.size() - 1);
-        assertEquals(vcName, node.at("/queryName").asText());
-        assertEquals(node.at("/createdBy").asText(), "system");
-        assertTrue(node.at("/userGroupName").asText().startsWith("auto"));
-        // edit 2nd
-        json = "{\"type\": \"PROJECT\"}";
-        editVC("dory", "dory", vcName, json);
-        node = testListOwnerVC("dory");
-        assertEquals(ResourceType.PROJECT.displayName(),
-                node.get(1).get("type").asText());
-        // check VC access
-        node = listAccessByGroup("admin", "");
-        assertEquals(3, node.size());
-    }
 }
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusFieldTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusFieldTest.java
similarity index 98%
rename from src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusFieldTest.java
rename to src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusFieldTest.java
index a600bba..a56c34d 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusFieldTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusFieldTest.java
@@ -1,4 +1,4 @@
-package de.ids_mannheim.korap.web.controller;
+package de.ids_mannheim.korap.web.controller.vc;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
@@ -86,7 +86,8 @@
         testRetrieveProhibitedField("system", "named-vc1", "tokens");
         testRetrieveProhibitedField("system", "named-vc1", "base");
         VirtualCorpusCache.delete("named-vc1");
-        deleteVcFromDB("named-vc1");
+        
+        deleteVC("named-vc1", "system", "admin");
     }
 
     private void testRetrieveUnknownTokens ()
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusInfoTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusInfoTest.java
similarity index 74%
rename from src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusInfoTest.java
rename to src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusInfoTest.java
index 4d20c0f..6d98009 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusInfoTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusInfoTest.java
@@ -1,23 +1,20 @@
-package de.ids_mannheim.korap.web.controller;
+package de.ids_mannheim.korap.web.controller.vc;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
-import jakarta.ws.rs.ProcessingException;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.Form;
-import jakarta.ws.rs.core.MediaType;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.core.Response.Status;
-
 import org.junit.jupiter.api.Test;
+
 import com.fasterxml.jackson.databind.JsonNode;
-import com.google.common.net.HttpHeaders;
+
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
 import de.ids_mannheim.korap.config.Attributes;
 import de.ids_mannheim.korap.constant.ResourceType;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.utils.JsonUtils;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
 
 public class VirtualCorpusInfoTest extends VirtualCorpusTestBase {
 
@@ -38,7 +35,7 @@
     }
 
     @Test
-    public void testRetrieveSystemVCGuest ()
+    public void testRetrieveSystemVC_guest ()
             throws ProcessingException, KustvaktException {
         Response response = target().path(API_VERSION).path("vc")
                 .path("~system").path("system-vc").request().get();
@@ -49,7 +46,7 @@
     }
 
     @Test
-    public void testRetrieveOwnerPrivateVC ()
+    public void testRetrievePrivateVC ()
             throws ProcessingException, KustvaktException {
         JsonNode node = retrieveVCInfo("dory", "dory", "dory-vc");
         assertEquals(node.at("/name").asText(), "dory-vc");
@@ -58,7 +55,7 @@
     }
 
     @Test
-    public void testRetrievePrivateVCUnauthorized ()
+    public void testRetrievePrivateVC_unauthorized ()
             throws ProcessingException, KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("dory-vc").request()
@@ -69,16 +66,32 @@
     }
 
     @Test
-    public void testRetrieveProjectVC ()
+    public void testRetrieveProjectVC_member ()
             throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        inviteMember(doryGroupName, "dory", "nemo");
+        subscribe(doryGroupName, "nemo");
+        
+        createAccess("dory", "group-vc", doryGroupName, "dory");
+        
         JsonNode node = retrieveVCInfo("nemo", "dory", "group-vc");
         assertEquals(node.at("/name").asText(), "group-vc");
         assertEquals(ResourceType.PROJECT.displayName(),
                 node.at("/type").asText());
+        
+        inviteMember(doryGroupName, "dory", "pearl");
+        subscribe(doryGroupName, "pearl");
+        
+        node = retrieveVCInfo("pearl", "dory", "group-vc");
+        assertEquals(node.at("/name").asText(), "group-vc");
+        assertEquals(ResourceType.PROJECT.displayName(),
+                node.at("/type").asText());
+        
+        deleteGroupByName(doryGroupName, "dory");
     }
 
     @Test
-    public void testRetrieveProjectVCUnauthorized ()
+    public void testRetrieveProjectVC_unauthorized ()
             throws ProcessingException, KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("group-vc").request()
@@ -89,7 +102,7 @@
     }
 
     @Test
-    public void testRetrieveProjectVCbyNonActiveMember ()
+    public void testRetrieveProjectVC_nonActiveMember ()
             throws ProcessingException, KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("group-vc").request()
@@ -100,32 +113,7 @@
     }
 
     @Test
-    public void testRetrievePublishedVC ()
-            throws ProcessingException, KustvaktException {
-        JsonNode node = retrieveVCInfo("gill", "marlin", "published-vc");
-        assertEquals(node.at("/name").asText(), "published-vc");
-        assertEquals(ResourceType.PUBLISHED.displayName(),
-                node.at("/type").asText());
-        Form f = new Form();
-        f.param("status", "HIDDEN");
-        // check gill in the hidden group of the vc
-        Response response = target().path(API_VERSION).path("admin")
-                .path("group").path("list").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("admin", "pass"))
-                .header(HttpHeaders.CONTENT_TYPE,
-                        MediaType.APPLICATION_FORM_URLENCODED)
-                .post(Entity.form(f));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        node = JsonUtils.readTree(entity);
-        assertEquals(3, node.at("/0/id").asInt());
-        String members = node.at("/0/members").toString();
-        assertTrue(members.contains("\"userId\":\"gill\""));
-    }
-
-    @Test
-    public void testAdminRetrievePrivateVC ()
+    public void testRetrievePrivateVC_admin ()
             throws ProcessingException, KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("dory-vc").request()
@@ -140,7 +128,7 @@
     }
 
     @Test
-    public void testAdminRetrieveProjectVC ()
+    public void testRetrieveProjectVC_admin ()
             throws ProcessingException, KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("group-vc").request()
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusListTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusListTest.java
similarity index 90%
rename from src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusListTest.java
rename to src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusListTest.java
index f1e1d17..8db247d 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusListTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusListTest.java
@@ -1,4 +1,4 @@
-package de.ids_mannheim.korap.web.controller;
+package de.ids_mannheim.korap.web.controller.vc;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
@@ -26,7 +26,7 @@
         node = listSystemVC("nemo");
         assertEquals(1, node.size());
         node = listVC("nemo");
-        assertEquals(3, node.size());
+        assertEquals(2, node.size());
     }
 
     @Test
@@ -35,16 +35,26 @@
         JsonNode node = testListOwnerVC("pearl");
         assertEquals(0, node.size());
         node = listVC("pearl");
-        assertEquals(2, node.size());
+        assertEquals(1, node.size());
     }
 
     @Test
+    public void testListVCMarlin ()
+            throws ProcessingException, KustvaktException {
+        JsonNode node = testListOwnerVC("marlin");
+        assertEquals(2, node.size());
+        node = listVC("marlin");
+        assertEquals(3, node.size());
+    }
+
+    
+    @Test
     public void testListVCDory ()
             throws ProcessingException, KustvaktException {
         JsonNode node = testListOwnerVC("dory");
         assertEquals(2, node.size());
         node = listVC("dory");
-        assertEquals(4, node.size());
+        assertEquals(3, node.size());
     }
 
     @Test
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusPublishedTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusPublishedTest.java
new file mode 100644
index 0000000..acfc3bb
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusPublishedTest.java
@@ -0,0 +1,218 @@
+package de.ids_mannheim.korap.web.controller.vc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.PrivilegeType;
+import de.ids_mannheim.korap.constant.ResourceType;
+import de.ids_mannheim.korap.constant.UserGroupStatus;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+public class VirtualCorpusPublishedTest extends VirtualCorpusTestBase{
+    
+    private String testUser = "vcPublishedTest";
+
+    @Test
+    public void testCreatePublishedVC () throws KustvaktException {
+        String vcName = "new-published-vc";
+        createPublishedVC(testUser, vcName);
+        
+        // test list owner vc
+        JsonNode node = retrieveVCInfo(testUser, testUser, vcName);
+        assertEquals(vcName, node.get("name").asText());
+        
+        node = getHiddenGroup(vcName);
+        assertEquals("system", node.at("/owner").asText());
+        assertEquals(UserGroupStatus.HIDDEN.name(), 
+                node.at("/status").asText());
+        
+        testRetrievePublishedVC("gill", testUser, vcName);
+        
+        String groupName = node.at("/name").asText();
+        testDeletePublishedVCUnauthorized(testUser, vcName, "gill");
+        testDeletePublishedVC(testUser, vcName, testUser, groupName);
+    }
+    
+    private void testRetrievePublishedVC (String username, String vcCreator,
+            String vcName) throws ProcessingException, KustvaktException {
+        retrieveVCInfo(username, vcCreator, vcName);
+        
+        JsonNode node = getHiddenGroup(vcName);
+        assertEquals("system", node.at("/owner").asText());
+        assertEquals(UserGroupStatus.HIDDEN.name(), 
+                node.at("/status").asText());
+        assertEquals(username, node.at("/members/0/userId").asText());
+        assertEquals(GroupMemberStatus.ACTIVE.name(), 
+                node.at("/members/0/status").asText());
+        assertEquals(1, node.at("/members/0/privileges").size());
+        assertEquals(PrivilegeType.READ_QUERY.name(), 
+                node.at("/members/0/privileges/0").asText());
+        String groupName = node.at("/name").asText();
+
+        node = listRolesByGroup("admin", groupName);
+        assertEquals(1, node.size());
+        assertEquals(vcName, node.at("/0/queryName").asText());
+        assertEquals(groupName, node.at("/0/userGroupName").asText());
+        assertEquals(1, node.at("/0/members").size());
+    }
+    
+    private void testDeletePublishedVC (String vcCreator, String vcName,
+            String deletedBy, String hiddenGroupName) throws KustvaktException {
+        deleteVC(vcName, vcCreator, deletedBy);
+
+        // EM: check if the hidden groups are deleted as well
+        JsonNode node = getHiddenGroup(vcName);
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals("No hidden group for query " + vcName + " is found",
+                node.at("/errors/0/1").asText());
+        
+        testHiddenGroupNotFound(hiddenGroupName);
+    }
+    
+    private void testHiddenGroupNotFound (String hiddenGroupName)
+            throws KustvaktException {
+        JsonNode node = listRolesByGroup("admin", hiddenGroupName);
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Group " + hiddenGroupName + " is not found",
+                node.at("/errors/0/1").asText());
+
+    }
+
+    private void testDeletePublishedVCUnauthorized (String vcCreator,
+            String vcName, String deletedBy)
+            throws KustvaktException {
+        Response response = deleteVC(vcName, vcCreator, deletedBy);
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        testResponseUnauthorized(response, deletedBy);
+    }
+    
+    @Test
+    public void testMarlinPublishedVC () throws KustvaktException {
+        
+        JsonNode node = testListOwnerVC("marlin");
+        assertEquals(2, node.size());
+        node = listVC("marlin");
+        assertEquals(3, node.size());
+        
+        String vcName = "marlin-published-vc";
+        createPublishedVC("marlin", vcName);
+        
+        node = testListOwnerVC("marlin");
+        assertEquals(3, node.size());
+        node = listVC("marlin");
+        assertEquals(4, node.size());
+        
+        String groupName = testSharePublishedVC(vcName);
+        
+        // dory is a member
+        testDeletePublishedVCUnauthorized("marlin", vcName, "dory");
+        // add dory as group admin
+        addAdminRole(marlinGroupName, "dory", "marlin");
+        testDeletePublishedVCUnauthorized("marlin", vcName, "dory");
+        
+        testDeletePublishedVC("marlin",vcName,"marlin", groupName);
+        
+        node = listRolesByGroup("admin", marlinGroupName);
+        assertEquals(0, node.size());
+        
+        deleteGroupByName(marlinGroupName, "marlin");
+    }
+    
+    private String testSharePublishedVC (String vcName) throws KustvaktException {
+        createMarlinGroup();
+        inviteMember(marlinGroupName, "marlin", "dory");
+        subscribe(marlinGroupName, "dory");
+
+        JsonNode node = listVC("dory");
+        assertEquals(3, node.size());
+
+        shareVC("marlin", vcName, marlinGroupName, "marlin");
+        
+        node = listVC("dory");
+        assertEquals(4, node.size());
+        node = listVC("marlin");
+        assertEquals(4, node.size());
+        
+        // check marlin-group access
+        node = listRolesByGroup("admin", marlinGroupName);
+        assertEquals(1, node.size());
+        assertEquals(vcName, node.at("/0/queryName").asText());
+        assertEquals(marlinGroupName, node.at("/0/userGroupName").asText());
+        assertEquals(2, node.at("/0/members").size());
+
+        // check hidden group access
+        node = getHiddenGroup(vcName);
+        String hiddenGroupName = node.at("/name").asText();
+        node = listRolesByGroup("admin", hiddenGroupName);
+        assertEquals(0, node.at("/0/members").size());
+        
+        testAddMemberAfterSharingPublishedVC(hiddenGroupName);
+        testRetrievePublishedVC("dory", "marlin", vcName);
+        return hiddenGroupName;
+    }
+    
+    private void testAddMemberAfterSharingPublishedVC (String hiddenGroupName)
+            throws KustvaktException {
+        JsonNode node = listVC("nemo");
+        assertEquals(2, node.size());
+
+        inviteMember(marlinGroupName, "marlin", "nemo");
+        subscribe(marlinGroupName, "nemo");
+
+        node = listVC("nemo");
+        assertEquals(3, node.size());
+
+        node = listRolesByGroup("admin", marlinGroupName);
+        assertEquals(3, node.at("/0/members").size());
+
+        node = listRolesByGroup("admin", hiddenGroupName);
+        assertEquals(0, node.at("/0/members").size());
+    }
+    
+    @Test
+    public void testPublishProjectVC () throws KustvaktException {
+        String vcName = "group-vc";
+        JsonNode node = retrieveVCInfo("dory", "dory", vcName);
+        assertEquals(ResourceType.PROJECT.displayName(),
+                node.get("type").asText());
+        
+        // edit PROJECT to PUBLISHED vc
+        String json = "{\"type\": \"PUBLISHED\"}";
+        editVC("dory", "dory", vcName, json);
+        
+        // check VC type
+        node = testListOwnerVC("dory");
+        JsonNode n = node.get(1);
+        assertEquals(ResourceType.PUBLISHED.displayName(),
+                n.get("type").asText());
+        
+        // check hidden group and roles
+        node = getHiddenGroup(vcName);
+        String hiddenGroupName = node.at("/name").asText();
+        node = listRolesByGroup("admin", hiddenGroupName);
+        assertEquals(1, node.size());
+        node = node.get(0);
+        assertEquals(vcName, node.at("/queryName").asText());
+        assertEquals(hiddenGroupName, node.at("/userGroupName").asText());
+        
+        // change PUBLISHED to PROJECT
+        json = "{\"type\": \"PROJECT\"}";
+        editVC("dory", "dory", vcName, json);
+        node = testListOwnerVC("dory");
+        assertEquals(ResourceType.PROJECT.displayName(),
+                node.get(1).get("type").asText());
+        
+        testHiddenGroupNotFound(hiddenGroupName);
+    }
+}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusReferenceTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusReferenceTest.java
similarity index 87%
rename from src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusReferenceTest.java
rename to src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusReferenceTest.java
index db1df98..638a041 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusReferenceTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusReferenceTest.java
@@ -1,4 +1,4 @@
-package de.ids_mannheim.korap.web.controller;
+package de.ids_mannheim.korap.web.controller.vc;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -11,12 +11,12 @@
 import org.springframework.beans.factory.annotation.Autowired;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import com.google.common.net.HttpHeaders;
 
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
 import de.ids_mannheim.korap.cache.VirtualCorpusCache;
 import de.ids_mannheim.korap.config.Attributes;
-import de.ids_mannheim.korap.config.SpringJerseyTest;
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.UserGroupStatus;
 import de.ids_mannheim.korap.dao.QueryDao;
 import de.ids_mannheim.korap.entity.QueryDO;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
@@ -24,13 +24,10 @@
 import de.ids_mannheim.korap.init.NamedVCLoader;
 import de.ids_mannheim.korap.util.QueryException;
 import de.ids_mannheim.korap.utils.JsonUtils;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.Form;
-import jakarta.ws.rs.core.MediaType;
 import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.core.Response.Status;
 
-public class VirtualCorpusReferenceTest extends SpringJerseyTest {
+public class VirtualCorpusReferenceTest extends VirtualCorpusTestBase {
 
     @Autowired
     private NamedVCLoader vcLoader;
@@ -202,30 +199,27 @@
 
     @Test
     public void testSearchWithRefPublishedVc () throws KustvaktException {
+        String vcName = "marlin-published-vc";
+        createPublishedVC("marlin", vcName);
+
         Response response = target().path(API_VERSION).path("search")
                 .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
-                .queryParam("cq", "referTo \"marlin/published-vc\"").request()
+                .queryParam("cq", "referTo \"marlin/" + vcName + "\"").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("squirt", "pass"))
                 .get();
         String ent = response.readEntity(String.class);
         JsonNode node = JsonUtils.readTree(ent);
         assertTrue(node.at("/matches").size() > 0);
-        Form f = new Form();
-        f.param("status", "HIDDEN");
-        // check dory in the hidden group of the vc
-        response = target().path(API_VERSION).path("admin").path("group")
-                .path("list").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("admin", "pass"))
-                .header(HttpHeaders.CONTENT_TYPE,
-                        MediaType.APPLICATION_FORM_URLENCODED)
-                .post(Entity.form(f));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        node = JsonUtils.readTree(entity);
-        assertEquals(3, node.at("/0/id").asInt());
-        String members = node.at("/0/members").toString();
-        assertTrue(members.contains("\"userId\":\"squirt\""));
+
+        node = getHiddenGroup(vcName);
+        assertEquals("system", node.at("/owner").asText());
+        assertEquals(UserGroupStatus.HIDDEN.name(),
+                node.at("/status").asText());
+        node = node.at("/members");
+        assertEquals("squirt", node.at("/0/userId").asText());
+        assertEquals(GroupMemberStatus.ACTIVE.name(),
+                node.at("/0/status").asText());
+        deleteVC(vcName, "marlin", "marlin");
     }
 }
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusSharingTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusSharingTest.java
new file mode 100644
index 0000000..1ffbdb8
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusSharingTest.java
@@ -0,0 +1,326 @@
+package de.ids_mannheim.korap.web.controller.vc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.http.HttpStatus;
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.PrivilegeType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+public class VirtualCorpusSharingTest extends VirtualCorpusTestBase {
+
+    private String testUser = "VirtualCorpusSharingTest";
+
+    @Test
+    public void testShareUnknownVC () throws KustvaktException {
+        Response response = shareVCByCreator("marlin", "non-existing-vc",
+                "marlin group");
+        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
+        assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatus());
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+    }
+
+    @Test
+    public void testShareUnknownGroup () throws KustvaktException {
+        Response response = shareVCByCreator("marlin", "marlin-vc",
+                "non-existing-group");
+        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
+        assertEquals(HttpStatus.SC_NOT_FOUND, response.getStatus());
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+    }
+
+    @Test
+    public void testShareVC_Unauthorized () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~marlin").path("marlin-vc").path("share")
+                .path("@marlin group").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("dory", "pass"))
+                .post(Entity.form(new Form()));
+        testResponseUnauthorized(response, "dory");
+    }
+
+    @Test
+    public void testShareVC_ByGroupAdmin () throws KustvaktException {
+        createMarlinGroup();
+        inviteMember(marlinGroupName, "marlin", "nemo");
+        subscribe(marlinGroupName, "nemo");
+
+        JsonNode node = listRolesByGroup("marlin", marlinGroupName);
+        assertEquals(0, node.size());
+
+        // share by member unauthorized
+        Response response = shareVCByCreator("nemo", "nemo-vc",
+                marlinGroupName);
+        testResponseUnauthorized(response, "nemo");
+
+        addAdminRole(marlinGroupName, "nemo", "marlin");
+        
+        response = shareVCByCreator("nemo", "nemo-vc", marlinGroupName);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        testShareVC_redundant("nemo", "nemo-vc", marlinGroupName);;
+
+        node = listRolesByGroup("marlin", marlinGroupName);
+        assertEquals(1, node.size());
+        deleteGroupByName(marlinGroupName, "marlin");
+    }
+
+    private void testShareVC_redundant (String vcCreator, String vcName,
+            String groupName) throws KustvaktException {
+        Response response = shareVCByCreator(vcCreator, vcName, groupName);
+        assertEquals(Status.CONFLICT.getStatusCode(), response.getStatus());
+        //        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
+        //        System.out.println(node.toPrettyString());
+    }
+
+    @Test
+    public void testSharePrivateVC () throws KustvaktException {
+        String vcName = "new_private_vc";
+        createPrivateVC(testUser, vcName);
+
+        String groupName = "DNB-group";
+        Response response = createUserGroup(groupName, "DNB users", testUser);
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+        JsonNode roleNodes = listRolesByGroup(testUser, groupName, false);
+        assertEquals(5, roleNodes.size());
+
+        String memberName = "darla";
+        testInviteMember(groupName, testUser, memberName);
+        subscribe(groupName, memberName);
+
+        roleNodes = listRolesByGroup(testUser, groupName, false);
+        assertEquals(6, roleNodes.size());
+
+        // share vc to group
+        shareVCByCreator(testUser, vcName, groupName);
+
+        // check member roles
+        JsonNode queryRoleNodes = listRolesByGroup(testUser, groupName);
+        assertEquals(1, queryRoleNodes.size());
+
+        testDeleteQueryAccessUnauthorized(testUser, vcName, groupName,
+                memberName);
+        testDeleteQueryAccessToGroup(testUser, groupName, vcName);
+
+        deleteVC(vcName, testUser, testUser);
+        deleteGroupByName(groupName, testUser);
+
+        roleNodes = listRolesByGroup(testUser, groupName, false);
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                roleNodes.at("/errors/0/0").asInt());
+    }
+
+    @Test
+    public void testShareProjectVC () throws KustvaktException {
+        String vcName = "new_project_vc";
+        createProjectVC(testUser, vcName);
+
+        // retrieve vc info
+        JsonNode vcInfo = retrieveVCInfo(testUser, testUser, vcName);
+        assertEquals(vcName, vcInfo.get("name").asText());
+
+        // list user VC
+        JsonNode node = listVC(testUser);
+        assertEquals(2, node.size());
+        assertEquals(vcName, node.get(1).get("name").asText());
+
+        // search by non member
+        Response response = searchWithVCRef("dory", testUser, vcName);
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+
+        // create user group
+        String groupName = "owidGroup";
+        String memberName = "darla";
+        response = createUserGroup(groupName, "Owid users", testUser);
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+        testInviteMember(groupName, testUser, memberName);
+        subscribe(groupName, memberName);
+        checkMemberInGroup(memberName, testUser, groupName);
+
+        // share vc to group
+        shareVCByCreator(testUser, vcName, groupName);
+
+        // check member roles
+        node = listRolesByGroup(testUser, groupName);
+        assertEquals(1, node.size());
+
+        // search by member
+        response = searchWithVCRef(memberName, testUser, vcName);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        node = JsonUtils.readTree(response.readEntity(String.class));
+        assertTrue(node.at("/matches").size() > 0);
+        // delete project VC
+        testDeleteSharedVC(vcName, testUser, testUser, groupName);
+        // list VC
+        node = listVC(testUser);
+        assertEquals(1, node.size());
+        // search by member
+        response = searchWithVCRef(memberName, testUser, vcName);
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+        node = JsonUtils.readTree(response.readEntity(String.class));
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+
+        deleteGroupByName(groupName, testUser);
+    }
+
+    @Test
+    public void testShareMultipleVC () throws KustvaktException {
+        String vc1 = "new_private_vc";
+        String vc2 = "new_project_vc";
+        createPrivateVC(testUser, vc1);
+        createProjectVC(testUser, vc2);
+
+        String groupName = "DNB-group";
+        Response response = createUserGroup(groupName, "DNB users", testUser);
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+
+        String memberName = "darla";
+        testInviteMember(groupName, testUser, memberName);
+        subscribe(groupName, memberName);
+
+        shareVC(testUser, vc1, groupName, testUser);
+        shareVC(testUser, vc2, groupName, testUser);
+
+        // list user VC
+        JsonNode node = listVC(testUser);
+        assertEquals(3, node.size());
+
+        node = listVC(memberName);
+        assertEquals(3, node.size());
+
+        testDeleteQueryAccessBySystemAdmin(testUser, vc1, groupName, "admin");
+
+        node = listVC(memberName);
+        assertEquals(2, node.size());
+
+        node = listVC(testUser);
+        assertEquals(3, node.size());
+
+        testDeleteQueryAccessByGroupAdmin(testUser, vc2, groupName, memberName);
+
+        node = listVC(memberName);
+        assertEquals(1, node.size());
+
+        deleteVC(vc1, testUser, testUser);
+        deleteVC(vc2, testUser, testUser);
+
+        node = listVC(testUser);
+        assertEquals(1, node.size());
+
+        deleteGroupByName(groupName, testUser);
+    }
+
+    private void testDeleteQueryAccessToGroup (String username,
+            String groupName, String vcName) throws KustvaktException {
+        JsonNode roleNodes = listRolesByGroup(username, groupName, false);
+        assertEquals(7, roleNodes.size());
+
+        // delete group role
+        Response response = deleteRoleByGroupAndQuery(username, vcName,
+                groupName, username);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        JsonNode queryRoleNodes = listRolesByGroup(username, groupName);
+        assertEquals(0, queryRoleNodes.size());
+
+        roleNodes = listRolesByGroup(username, groupName, false);
+        assertEquals(6, roleNodes.size());
+
+    }
+
+    private void testDeleteQueryAccessUnauthorized (String vcCreator,
+            String vcName, String groupName, String username)
+            throws KustvaktException {
+        Response response = deleteRoleByGroupAndQuery(vcCreator, vcName,
+                groupName, username);
+
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+    }
+
+    private void testDeleteQueryAccessBySystemAdmin (String vcCreator,
+            String vcName, String groupName, String username)
+            throws KustvaktException {
+        Response response = deleteRoleByGroupAndQuery(vcCreator, vcName,
+                groupName, username);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+
+    private void testDeleteQueryAccessByGroupAdmin (String vcCreator,
+            String vcName, String groupName, String memberName)
+            throws KustvaktException {
+
+        addAdminRole(groupName, memberName, vcCreator);
+        Response response = deleteRoleByGroupAndQuery(vcCreator, vcName,
+                groupName, memberName);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+
+    private void testDeleteSharedVC (String vcName, String vcCreator,
+            String username, String groupName) throws KustvaktException {
+        JsonNode node = listRolesByGroup(username, groupName);
+        assertEquals(1, node.size());
+
+        Response response = deleteVC(vcName, vcCreator, username);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        node = listRolesByGroup(username, groupName);
+        assertEquals(0, node.size());
+    }
+
+    //    private JsonNode listUserGroup (String username, String groupName)
+    //            throws KustvaktException {
+    //        Response response = target().path(API_VERSION).path("group").request()
+    //                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+    //                        .createBasicAuthorizationHeaderValue(username, "pass"))
+    //                .get();
+    //        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    //        String entity = response.readEntity(String.class);
+    //        JsonNode node = JsonUtils.readTree(entity);
+    //        return node;
+    //    }
+
+    private void checkMemberInGroup (String memberName, String testUser,
+            String groupName) throws KustvaktException {
+        JsonNode node = listUserGroups(testUser).get(0);
+        assertEquals(2, node.get("members").size());
+        assertEquals(memberName, node.at("/members/1/userId").asText());
+        assertEquals(GroupMemberStatus.ACTIVE.name(),
+                node.at("/members/1/status").asText());
+        assertEquals(PrivilegeType.DELETE_MEMBER.name(),
+                node.at("/members/1/privileges/0").asText());
+    }
+
+    @Test
+    public void testlistRolesUnauthorized () throws KustvaktException {
+        createDoryGroup();
+        JsonNode node = listRolesByGroup("nemo", "dory-group");
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "Unauthorized operation for user: nemo");
+        deleteGroupByName(doryGroupName, "dory");
+    }
+}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusTestBase.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusTestBase.java
new file mode 100644
index 0000000..d9604fb
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusTestBase.java
@@ -0,0 +1,282 @@
+package de.ids_mannheim.korap.web.controller.vc;
+
+import static org.hamcrest.CoreMatchers.hasItem;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.Set;
+
+import org.apache.http.entity.ContentType;
+import org.glassfish.jersey.server.ContainerRequest;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.net.HttpHeaders;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.ResourceType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import de.ids_mannheim.korap.web.controller.usergroup.UserGroupTestBase;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+public abstract class VirtualCorpusTestBase extends UserGroupTestBase {
+
+    protected JsonNode retrieveVCInfo (String username, String vcCreator,
+            String vcName) throws ProcessingException, KustvaktException {
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~" + vcCreator).path(vcName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .get();
+        String entity = response.readEntity(String.class);
+        // System.out.println(entity);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        return JsonUtils.readTree(entity);
+    }
+
+    protected void createVC (String authHeader, String username, String vcName,
+            String vcJson) throws KustvaktException {
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~" + username).path(vcName).request()
+                .header(Attributes.AUTHORIZATION, authHeader)
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .put(Entity.json(vcJson));
+
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+    }
+    
+    protected void createVC (String username, String vcName,
+            ResourceType vcType) throws KustvaktException {
+        String vcJson = "{\"type\": \""+vcType+"\""
+                + ",\"queryType\": \"VIRTUAL_CORPUS\""
+                + ",\"corpusQuery\": \"corpusSigle=GOE\"}";
+
+        String authHeader = HttpAuthorizationHandler
+                .createBasicAuthorizationHeaderValue(username, "pass");
+        
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~" + username).path(vcName).request()
+                .header(Attributes.AUTHORIZATION, authHeader)
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .put(Entity.json(vcJson));
+
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+    }
+    
+    protected void createPrivateVC (String username, String vcName)
+            throws KustvaktException {
+        createVC(username, vcName, ResourceType.PRIVATE);
+    }
+    
+    protected void createProjectVC (String username, String vcName)
+            throws KustvaktException {
+        createVC(username, vcName, ResourceType.PROJECT);
+    }
+
+    protected void createPublishedVC (String username, String vcName)
+            throws KustvaktException {
+        createVC(username, vcName, ResourceType.PUBLISHED);
+    }
+
+
+    protected void editVC (String username, String vcCreator, String vcName,
+            String vcJson) throws KustvaktException {
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~" + vcCreator).path(vcName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .put(Entity.json(vcJson));
+
+        assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
+    }
+
+    protected JsonNode listVC (String username)
+            throws ProcessingException, KustvaktException {
+        Response response = target().path(API_VERSION).path("vc").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .get();
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        // System.out.println(entity);
+        return JsonUtils.readTree(entity);
+    }
+
+    protected JsonNode listVCWithAuthHeader (String authHeader)
+            throws ProcessingException, KustvaktException {
+        Response response = target().path(API_VERSION).path("vc").request()
+                .header(Attributes.AUTHORIZATION, authHeader).get();
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        return JsonUtils.readTree(entity);
+    }
+
+    protected JsonNode testListOwnerVC (String username)
+            throws ProcessingException, KustvaktException {
+        Response response = target().path(API_VERSION).path("vc")
+                .queryParam("filter-by", "own").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .get();
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        String entity = response.readEntity(String.class);
+        return JsonUtils.readTree(entity);
+    }
+
+    protected JsonNode listSystemVC (String username) throws KustvaktException {
+        Response response = target().path(API_VERSION).path("vc")
+                .queryParam("filter-by", "system").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("pearl", "pass"))
+                .get();
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        return node;
+    }
+
+    protected Response shareVCByCreator (String vcCreator, String vcName,
+            String groupName) throws KustvaktException {
+
+        return target().path(API_VERSION).path("vc").path("~" + vcCreator)
+                .path(vcName).path("share").path("@" + groupName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(vcCreator, "pass"))
+                .post(Entity.form(new Form()));
+    }
+    
+    protected Response shareVC (String vcCreator, String vcName,
+            String groupName, String username) throws ProcessingException, KustvaktException {
+
+        return target().path(API_VERSION).path("vc").path("~" + vcCreator)
+                .path(vcName).path("share").path("@" + groupName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .post(Entity.form(new Form()));
+    }
+
+    protected JsonNode listRolesByGroup (String username, String groupName)
+            throws KustvaktException {
+        return listRolesByGroup(username, groupName, true);
+    }
+    
+    protected JsonNode listRolesByGroup (String username, String groupName,
+            boolean hasQuery)
+            throws KustvaktException {
+        Response response = target().path(API_VERSION).path("vc").path("access")
+                .queryParam("groupName", groupName)
+                .queryParam("hasQuery", hasQuery)
+                .request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .get();
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        return node;
+    }
+
+    protected Response deleteVC (String vcName, String vcCreator, String username)
+            throws KustvaktException {
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~" + vcCreator).path(vcName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .delete();
+
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        return response;
+    }
+
+    protected void testResponseUnauthorized (Response response, String username)
+            throws KustvaktException {
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Unauthorized operation for user: " + username,
+                node.at("/errors/0/1").asText());
+
+        checkWWWAuthenticateHeader(response);
+    }
+
+    protected void checkWWWAuthenticateHeader (Response response) {
+        Set<Entry<String, List<Object>>> headers = response.getHeaders()
+                .entrySet();
+
+        for (Entry<String, List<Object>> header : headers) {
+            if (header.getKey().equals(ContainerRequest.WWW_AUTHENTICATE)) {
+                assertThat(header.getValue(),
+                        not(hasItem("Api realm=\"Kustvakt\"")));
+                assertThat(header.getValue(),
+                        hasItem("Bearer realm=\"Kustvakt\""));
+                assertThat(header.getValue(),
+                        hasItem("Basic realm=\"Kustvakt\""));
+            }
+        }
+    }
+    
+    protected void createAccess (String vcCreator, String vcName,
+            String groupName, String username)
+            throws ProcessingException, KustvaktException {
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~" + vcCreator).path(vcName).path("share")
+                .path("@" + groupName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .post(Entity.form(new Form()));
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+    
+    @Deprecated
+    protected Response deleteAccess (String username, String accessId)
+            throws ProcessingException, KustvaktException {
+        Response response = target().path(API_VERSION).path("vc").path("access")
+                .path(accessId).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .delete();
+        return response;
+    }
+    
+    protected Response deleteRoleByGroupAndQuery (String vcCreator, String vcName,
+            String groupName, String deleteBy)
+            throws ProcessingException, KustvaktException {
+        Response response = target().path(API_VERSION).path("vc")
+                .path("~" + vcCreator).path(vcName).path("delete")
+                .path("@" + groupName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(deleteBy, "pass"))
+                .delete();
+        return response;
+    }
+    
+    protected Response searchWithVCRef (String username, String vcCreator,
+            String vcName) throws KustvaktException {
+        Response response = target().path(API_VERSION).path("search")
+                .queryParam("q", "[orth=der]").queryParam("ql", "poliqarp")
+                .queryParam("cq",
+                        "referTo \"" + vcCreator + "/" + vcName + "\"")
+                .request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .get();
+        return response;
+    }
+}
diff --git a/src/test/resources/kustvakt-test.conf b/src/test/resources/kustvakt-test.conf
index f10d3e8..317b720 100644
--- a/src/test/resources/kustvakt-test.conf
+++ b/src/test/resources/kustvakt-test.conf
@@ -52,7 +52,7 @@
 # Delete configuration (default hard)
 #
 # delete.auto.group = hard
-delete.group = soft
+#delete.group = soft
 delete.group.member = soft
 
 # Virtual corpus and queries