Merge branch 'v.74-hot-fix'

Change-Id: I0b3a2e4b76222ba16ebc8eac67929cd9998b3d05
diff --git a/.gitignore b/.gitignore
index dbb295d..68f78b2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,6 +2,7 @@
 !/.gitignore
 !/.github
 !/.dockerignore
+!/.gitlab-ci.yml
 target
 tmp
 logs
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000..1aa1f85
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,31 @@
+# gitlab ci pipeline to build kustvakt docker container
+# automatically triggered on tag pushs or run manually
+#
+# Download latest container from artifacts and import it:
+#
+# curl -Ls 'https://gitlab.ids-mannheim.de/KorAP/kustvakt/-/jobs/artifacts/master/raw/kustvakt.tar.xz?job=build-docker' | docker load
+
+image: docker:latest
+
+services:
+  - docker:dind
+
+build-docker:
+  rules:
+    - if: $CI_COMMIT_TAG =~ /.+/
+      variables:
+        VID: $CI_COMMIT_TAG
+    - when: manual
+      variables:
+        VID: $CI_COMMIT_BRANCH-$CI_COMMIT_SHORT_SHA
+  stage: build
+  before_script:
+    - apk update
+    - apk add --no-cache git
+  script:
+    - docker build -f Dockerfile -t korap/kustvakt:$VID -t korap/kustvakt:latest  -t korap/kustvakt:$VID-full -t korap/kustvakt:latest-full --target kustvakt-full .
+    - docker save korap/kustvakt:$VID-full | xz -T0 -M16G -9 > kustvakt.tar.xz
+  artifacts:
+    paths:
+      - kustvakt.tar.xz
+
diff --git a/Changes b/Changes
index 391066a..050dfc7 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,32 @@
+# version 0.75-SNAPSHOT
+
+- Alter role and remove privilege database tables (#763)
+- Update user-group and user-group member web-services (#763)
+- Remove edit member role web-service (#763)
+- Remove query access table (#763)
+- Remove query access admin, merged with user-group admin (#763)
+- Update share-query and query-access web-services (#763)
+- Add new web-service: delete role by query and group (#763)
+- Remove soft delete group and group status deleted (#765)
+- Remove soft delete group member and member status deleted (#765)
+- Removed SearchResourceFilters and UserGroupJson
+- Removed deleted_by from user_group and user_group_member tables (#764)
+- Removed created_by, status and status_date from user_group_member table (#764)
+- Removed GroupMemberStatus (#764)
+- Replace invite and subscribe to add member (#764)
+- Remove unsubscribe member (#764)
+- Added deprecation messages to deprecated services
+- Removed mail configuration (#764)
+- Deprecate VC access deletion.
+- Change default port to 8089.
+- Disallow scope all for non super clients.
+
+# version 0.74.1-SNAPSHOT
+
+- Switch Docker image to temurin (diewald).
+- - Introduce filter_by and deprecate authorized_only in OAuth2
+  client list (close #579)
+
 # version 0.74 hot-fix
 
 - Removed admin & owner restriction on client info access.
diff --git a/Dockerfile b/Dockerfile
index 2b138d1..ae5beb1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # Use alpine linux as base image
-FROM openjdk:19-alpine AS builder
+FROM eclipse-temurin:22-jdk-alpine AS builder
 
 # Copy repository respecting .dockerignore
 COPY . /kustvakt
@@ -94,7 +94,7 @@
 
 CMD ["sh"]
 
-FROM openjdk:19-alpine AS kustvakt-lite
+FROM eclipse-temurin:22-jre-alpine AS kustvakt-lite
 
 RUN addgroup -S korap && \
     adduser -S kustvakt -G korap && \
@@ -115,7 +115,7 @@
 
 CMD [ "Kustvakt-lite.jar" ]
 
-FROM openjdk:19-alpine AS kustvakt-full
+FROM eclipse-temurin:22-jre-alpine AS kustvakt-full
 
 RUN addgroup -S korap && \
     adduser -S kustvakt -G korap && \
diff --git a/pom.xml b/pom.xml
index e9cc568..fc504d0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -4,14 +4,14 @@
 	<modelVersion>4.0.0</modelVersion>
 	<groupId>de.ids-mannheim.korap.kustvakt</groupId>
 	<artifactId>Kustvakt</artifactId>
-	<version>0.74</version>
+	<version>0.75-SNAPSHOT</version>
 	<properties>
 		<java.version>17</java.version>
 		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
 		<jersey.version>3.1.5</jersey.version>
 		<hibernate.ehcache.version>6.0.0.Alpha7</hibernate.ehcache.version>
 		<hibernate.version>6.1.7.Final</hibernate.version>
-		<spring.version>6.1.6</spring.version>
+		<spring.version>6.1.12</spring.version>
 		<!-- spring6.version is used in jersey and defined here 
 		to make sure that jersey uses the correct spring version-->
 		<spring6.version>${spring.version}</spring6.version>
@@ -139,8 +139,6 @@
 												<exclude>com.novell.ldap</exclude>
 												<exclude>com.unboundid</exclude>
 												<exclude>org.glassfish.jersey.test-framework*</exclude>
-												<exclude>org.apache.velocity*</exclude>
-												<exclude>com.sun.mail</exclude>
 												<exclude>META-INF/*.SF</exclude>
 								                <exclude>META-INF/*.DSA</exclude>
 								                <exclude>META-INF/*.RSA</exclude>
@@ -419,11 +417,6 @@
 		</dependency>
 		<dependency>
 			<groupId>org.springframework</groupId>
-			<artifactId>spring-context-support</artifactId>
-			<version>${spring.version}</version>
-		</dependency>
-		<dependency>
-			<groupId>org.springframework</groupId>
 			<artifactId>spring-test</artifactId>
 			<version>${spring.version}</version>
 			<scope>compile</scope>
@@ -620,37 +613,6 @@
 			<version>6.0.11</version>
 		</dependency>
 
-		<!-- velocity -->
-		<dependency>
-			<groupId>org.apache.velocity</groupId>
-			<artifactId>velocity-engine-core</artifactId>
-			<version>2.3</version>
-		</dependency>
-		<dependency>
-			<groupId>org.apache.velocity.tools</groupId>
-			<artifactId>velocity-tools-generic</artifactId>
-			<version>3.1</version>
-			<exclusions>
-				<exclusion>
-					<groupId>commons-logging</groupId>
-					<artifactId>commons-logging</artifactId>
-				</exclusion>
-			</exclusions>
-		</dependency>
-
-		<!-- Mail -->
-		<dependency>
-			<groupId>com.sun.mail</groupId>
-			<artifactId>jakarta.mail</artifactId>
-			<version>2.0.1</version>
-		</dependency>
-
-		<dependency>
-			<groupId>jakarta.activation</groupId>
-			<artifactId>jakarta.activation-api</artifactId>
-			<version>2.1.2</version>
-		</dependency>
-
 		<!-- OAuth -->
 		<dependency>
 			<groupId>com.nimbusds</groupId>
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/AuthenticationManager.java b/src/main/java/de/ids_mannheim/korap/authentication/AuthenticationManager.java
index 6ee6dc6..1ebbce9 100644
--- a/src/main/java/de/ids_mannheim/korap/authentication/AuthenticationManager.java
+++ b/src/main/java/de/ids_mannheim/korap/authentication/AuthenticationManager.java
@@ -72,7 +72,4 @@
         return "provider list: " + this.providers.toString();
     }
 
-    public abstract User getUser (String username, String method)
-            throws KustvaktException;
-
 }
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/DummyAuthenticationManager.java b/src/main/java/de/ids_mannheim/korap/authentication/DummyAuthenticationManager.java
index 83b979c..21379b2 100644
--- a/src/main/java/de/ids_mannheim/korap/authentication/DummyAuthenticationManager.java
+++ b/src/main/java/de/ids_mannheim/korap/authentication/DummyAuthenticationManager.java
@@ -62,12 +62,4 @@
         // TODO Auto-generated method stub
 
     }
-
-    @Override
-    public User getUser (String username, String method)
-            throws KustvaktException {
-        // TODO Auto-generated method stub
-        return null;
-    }
-
 }
diff --git a/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java b/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java
index e5f5a54..031c3c1 100644
--- a/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java
+++ b/src/main/java/de/ids_mannheim/korap/authentication/KustvaktAuthenticationManager.java
@@ -131,24 +131,6 @@
         //		return entHandler.getAccount(username);
     }
 
-    @Override
-    public User getUser (String username, String method)
-            throws KustvaktException {
-        KorAPUser user = new KorAPUser();
-        user.setUsername(username);
-        String email = null;
-        switch (method.toLowerCase()) {
-            case "ldap":
-                email = config.getTestEmail();
-                break;
-            default:
-                email = config.getTestEmail();
-                break;
-        }
-        user.setEmail(email);
-        return user;
-    }
-
     public TokenContext refresh (TokenContext context)
             throws KustvaktException {
         AuthenticationIface provider = getProvider(context.getTokenType(),
@@ -315,6 +297,7 @@
         return user;
     }
 
+    @Deprecated
     // todo: what if attributes null?
     private User authenticate (String username, String password,
             Map<String, Object> attr) throws KustvaktException {
diff --git a/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java b/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
index 0a904d3..75c6162 100644
--- a/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
+++ b/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
@@ -27,13 +27,6 @@
 
 public class FullConfiguration extends KustvaktConfiguration {
     public static Logger jlog = LogManager.getLogger(FullConfiguration.class);
-    // mail configuration
-    private boolean isMailEnabled;
-    private String testEmail;
-    private String noReply;
-    private String emailAddressRetrieval;
-
-    private String groupInvitationTemplate;
 
     private String ldapConfig;
 
@@ -47,10 +40,6 @@
 
     private String authenticationScheme;
 
-    private boolean isSoftDeleteAutoGroup;
-    private boolean isSoftDeleteGroup;
-    private boolean isSoftDeleteGroupMember;
-
     private EncryptionIface.Encryption secureHashAlgorithm;
 
     private AuthenticationMethod OAuth2passwordAuthentication;
@@ -89,8 +78,6 @@
 
         // EM: pattern for matching availability in Krill matches
         setLicensePatterns(properties);
-        setDeleteConfiguration(properties);
-        setMailConfiguration(properties);
         ldapConfig = properties.getProperty("ldap.config");
 
         setSecurityConfiguration(properties);
@@ -154,34 +141,6 @@
                 .getProperty("oauth2.refresh.token.long.expiry", "365D")));
     }
 
-    private void setMailConfiguration (Properties properties) {
-        setMailEnabled(Boolean
-                .valueOf(properties.getProperty("mail.enabled", "false")));
-        if (isMailEnabled) {
-            // other properties must be set in the kustvakt.conf
-            setTestEmail(
-                    properties.getProperty("mail.receiver", "test@localhost"));
-            setNoReply(properties.getProperty("mail.sender"));
-            setGroupInvitationTemplate(
-                    properties.getProperty("template.group.invitation"));
-            setEmailAddressRetrieval(
-                    properties.getProperty("mail.address.retrieval", "test"));
-        }
-    }
-
-    private void setDeleteConfiguration (Properties properties) {
-        setSoftDeleteGroup(
-                parseDeleteConfig(properties.getProperty("delete.group", "")));
-        setSoftDeleteAutoGroup(parseDeleteConfig(
-                properties.getProperty("delete.auto.group", "")));
-        setSoftDeleteGroupMember(parseDeleteConfig(
-                properties.getProperty("delete.group.member", "")));
-    }
-
-    private boolean parseDeleteConfig (String deleteConfig) {
-        return deleteConfig.equals("soft") ? true : false;
-    }
-
     private void setLicensePatterns (Properties properties) {
         setFreeLicensePattern(compilePattern(getFreeOnlyRegex()));
         setPublicLicensePattern(compilePattern(
@@ -311,70 +270,6 @@
         this.allOnlyRegex = allOnlyRegex;
     }
 
-    public boolean isSoftDeleteGroup () {
-        return isSoftDeleteGroup;
-    }
-
-    public void setSoftDeleteGroup (boolean isSoftDeleteGroup) {
-        this.isSoftDeleteGroup = isSoftDeleteGroup;
-    }
-
-    public boolean isSoftDeleteGroupMember () {
-        return isSoftDeleteGroupMember;
-    }
-
-    public void setSoftDeleteGroupMember (boolean isSoftDeleteGroupMember) {
-        this.isSoftDeleteGroupMember = isSoftDeleteGroupMember;
-    }
-
-    public boolean isSoftDeleteAutoGroup () {
-        return isSoftDeleteAutoGroup;
-    }
-
-    public void setSoftDeleteAutoGroup (boolean isSoftDeleteAutoGroup) {
-        this.isSoftDeleteAutoGroup = isSoftDeleteAutoGroup;
-    }
-
-    public String getTestEmail () {
-        return testEmail;
-    }
-
-    public void setTestEmail (String testEmail) {
-        this.testEmail = testEmail;
-    }
-
-    public boolean isMailEnabled () {
-        return isMailEnabled;
-    }
-
-    public void setMailEnabled (boolean isMailEnabled) {
-        this.isMailEnabled = isMailEnabled;
-    }
-
-    public String getNoReply () {
-        return noReply;
-    }
-
-    public void setNoReply (String noReply) {
-        this.noReply = noReply;
-    }
-
-    public String getGroupInvitationTemplate () {
-        return groupInvitationTemplate;
-    }
-
-    public void setGroupInvitationTemplate (String groupInvitationTemplate) {
-        this.groupInvitationTemplate = groupInvitationTemplate;
-    }
-
-    public String getEmailAddressRetrieval () {
-        return emailAddressRetrieval;
-    }
-
-    public void setEmailAddressRetrieval (String emailAddressRetrieval) {
-        this.emailAddressRetrieval = emailAddressRetrieval;
-    }
-
     public EncryptionIface.Encryption getSecureHashAlgorithm () {
         return secureHashAlgorithm;
     }
diff --git a/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java b/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
index a1ccd11..1647e41 100644
--- a/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
+++ b/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
@@ -62,7 +62,6 @@
     private int returnhits;
     private String keystoreLocation;
     private String keystorePassword;
-    private Properties mailProperties;
     private String host;
     private String shibUserMapping;
     private String userConfig;
@@ -71,10 +70,6 @@
     private long loginAttemptNum;
     private boolean allowMultiLogIn;
     private int loadFactor;
-    @Deprecated
-    private int validationStringLength;
-    @Deprecated
-    private int validationEmaillength;
     
     // EM: determine if search and match info services restricted 
     // to logged in users. This replaces @SearchResourceFilters
@@ -119,6 +114,7 @@
     // EM: metadata restriction
     // another variable might be needed to define which metadata fields are restricted 
     private boolean isMetadataRestricted = false;
+    private boolean totalResultCacheEnabled;
 
     // EM: Maybe needed when we support pipe registration
     @Deprecated
@@ -134,7 +130,7 @@
     public KustvaktConfiguration () {}
 
     public void loadBasicProperties (Properties properties) {
-        port = Integer.valueOf(properties.getProperty("server.port", "8095"));
+        port = Integer.valueOf(properties.getProperty("server.port", "8089"));
         baseURL = properties.getProperty("kustvakt.base.url", "/api/*");
         setSecureRandomAlgorithm(
                 properties.getProperty("security.secure.random.algorithm", ""));
@@ -212,10 +208,6 @@
 
         loadFactor = Integer.valueOf(
                 properties.getProperty("security.encryption.loadFactor", "15"));
-        validationStringLength = Integer.valueOf(properties
-                .getProperty("security.validation.stringLength", "150"));
-        validationEmaillength = Integer.valueOf(properties
-                .getProperty("security.validation.emailLength", "40"));
 
         sharedSecret = properties.getProperty("security.sharedSecret", "")
                 .getBytes();
@@ -229,6 +221,9 @@
 
         // network endpoint
         networkEndpointURL = properties.getProperty("network.endpoint.url", "");
+        // cache
+        totalResultCacheEnabled = Boolean.valueOf(properties.getProperty(
+                "cache.total.results.enabled","true"));
     }
 
     @Deprecated
diff --git a/src/main/java/de/ids_mannheim/korap/constant/GroupMemberStatus.java b/src/main/java/de/ids_mannheim/korap/constant/GroupMemberStatus.java
deleted file mode 100644
index 445e31e..0000000
--- a/src/main/java/de/ids_mannheim/korap/constant/GroupMemberStatus.java
+++ /dev/null
@@ -1,17 +0,0 @@
-package de.ids_mannheim.korap.constant;
-
-/**
- * Defines possible statuses of a user-group member
- * 
- * @author margaretha
- *
- */
-public enum GroupMemberStatus {
-    ACTIVE,
-    // membership invitation was sent and has not been accepted 
-    // or rejected yet
-    PENDING,
-    // either membership invitation was rejected or the member was 
-    // deleted by a user-group admin
-    DELETED;
-}
diff --git a/src/main/java/de/ids_mannheim/korap/constant/OAuth2Scope.java b/src/main/java/de/ids_mannheim/korap/constant/OAuth2Scope.java
index f24956b..c37a471 100644
--- a/src/main/java/de/ids_mannheim/korap/constant/OAuth2Scope.java
+++ b/src/main/java/de/ids_mannheim/korap/constant/OAuth2Scope.java
@@ -25,7 +25,7 @@
 
     DELETE_USER_GROUP_MEMBER, ADD_USER_GROUP_MEMBER,
 
-    EDIT_USER_GROUP_MEMBER_ROLE, ADD_USER_GROUP_MEMBER_ROLE, DELETE_USER_GROUP_MEMBER_ROLE,
+    ADD_USER_GROUP_MEMBER_ROLE, DELETE_USER_GROUP_MEMBER_ROLE,
 
     CREATE_VC, VC_INFO, EDIT_VC, DELETE_VC,
 
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..e3454c6 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,19 @@
 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,
+    DELETE_SELF,
+    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/constant/UserGroupStatus.java b/src/main/java/de/ids_mannheim/korap/constant/UserGroupStatus.java
index 03eedcb..bec7b8e 100644
--- a/src/main/java/de/ids_mannheim/korap/constant/UserGroupStatus.java
+++ b/src/main/java/de/ids_mannheim/korap/constant/UserGroupStatus.java
@@ -9,7 +9,7 @@
  *
  */
 public enum UserGroupStatus {
-    ACTIVE, DELETED,
+    ACTIVE, 
     // group members cannot see the group
     HIDDEN;
 }
diff --git a/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java b/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
index e616295..5972577 100644
--- a/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
+++ b/src/main/java/de/ids_mannheim/korap/core/service/SearchService.java
@@ -211,7 +211,7 @@
 
         int hashedKoralQuery = createTotalResultCacheKey(query);
         boolean hasCutOff = hasCutOff(query);
-        if (!hasCutOff) {
+        if (config.isTotalResultCacheEnabled() && !hasCutOff) {
             query = precheckTotalResultCache(hashedKoralQuery, query);
         }
 
@@ -229,7 +229,10 @@
         }
         // jlog.debug("Query result: " + result);
 
-        result = afterCheckTotalResultCache(hashedKoralQuery, result);
+        if (config.isTotalResultCacheEnabled()) {
+            result = afterCheckTotalResultCache(hashedKoralQuery, result);
+        }
+        
         if (!hasCutOff) {
             result = removeCutOff(result);
         }
@@ -248,7 +251,7 @@
             throws KustvaktException {
         ObjectNode queryNode = (ObjectNode) JsonUtils.readTree(query);
         queryNode.remove("meta");
-        return queryNode.hashCode();
+        return queryNode.toString().hashCode();
     }
 
     private String afterCheckTotalResultCache (int hashedKoralQuery,
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..9f2f70a 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,22 @@
 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.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 +34,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 +268,17 @@
         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, user));
 
         Query q = entityManager.createQuery(cq);
         return q.getResultList();
@@ -352,14 +329,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..6c7914a 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,27 @@
 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.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 +33,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 +49,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,27 +60,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.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 {
+    public void deleteGroup (int groupId, String deletedBy) throws KustvaktException {
         ParameterChecker.checkIntegerValue(groupId, "groupId");
         ParameterChecker.checkStringValue(deletedBy, "deletedBy");
 
@@ -100,17 +111,11 @@
                     "groupId: " + groupId);
         }
 
-        if (isSoftDelete) {
-            group.setStatus(UserGroupStatus.DELETED);
-            group.setDeletedBy(deletedBy);
-            entityManager.merge(group);
+        // EM: this seems weird
+        if (!entityManager.contains(group)) {
+            group = entityManager.merge(group);
         }
-        else {
-            if (!entityManager.contains(group)) {
-                group = entityManager.merge(group);
-            }
-            entityManager.remove(group);
-        }
+        entityManager.remove(group);
     }
 
     public void updateGroup (UserGroup group) throws KustvaktException {
@@ -187,9 +192,7 @@
                 criteriaBuilder.equal(root.get(UserGroup_.status),
                         UserGroupStatus.ACTIVE),
                 criteriaBuilder.equal(members.get(UserGroupMember_.userId),
-                        userId),
-                criteriaBuilder.notEqual(members.get(UserGroupMember_.status),
-                        GroupMemberStatus.DELETED));
+                        userId));
         // criteriaBuilder.equal(members.get(UserGroupMember_.status),
         // GroupMemberStatus.ACTIVE));
 
@@ -232,23 +235,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 +262,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 +359,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..86ab9dd 100644
--- a/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
+++ b/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
@@ -3,6 +3,17 @@
 import java.util.HashSet;
 import java.util.List;
 
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+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;
+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.ParameterChecker;
 import jakarta.persistence.EntityManager;
 import jakarta.persistence.NoResultException;
 import jakarta.persistence.PersistenceContext;
@@ -13,18 +24,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.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.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.utils.ParameterChecker;
-
 /**
  * Manages database queries and transactions regarding
  * {@link UserGroupMember} entity and
@@ -44,6 +43,7 @@
     public void addMember (UserGroupMember member) throws KustvaktException {
         ParameterChecker.checkObjectValue(member, "userGroupMember");
         entityManager.persist(member);
+        entityManager.flush();
     }
 
     public void updateMember (UserGroupMember member) throws KustvaktException {
@@ -51,8 +51,8 @@
         entityManager.merge(member);
     }
 
-    public void deleteMember (UserGroupMember member, String deletedBy,
-            boolean isSoftDelete) throws KustvaktException {
+    public void deleteMember (UserGroupMember member, String deletedBy)
+            throws KustvaktException {
         ParameterChecker.checkObjectValue(member, "UserGroupMember");
         ParameterChecker.checkStringValue(deletedBy, "deletedBy");
 
@@ -60,16 +60,8 @@
             member = entityManager.merge(member);
         }
 
-        if (isSoftDelete) {
-            member.setStatus(GroupMemberStatus.DELETED);
-            member.setDeletedBy(deletedBy);
-            member.setRoles(new HashSet<>());
-            entityManager.persist(member);
-        }
-        else {
-            member.setRoles(new HashSet<>());
-            entityManager.remove(member);
-        }
+        member.setRoles(new HashSet<>());
+        entityManager.remove(member);
     }
 
     public UserGroupMember retrieveMemberById (String userId, int groupId)
@@ -104,9 +96,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();
@@ -119,9 +110,7 @@
         Predicate predicate = criteriaBuilder.and(
                 criteriaBuilder.equal(root.get(UserGroupMember_.group),
                         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,20 +120,15 @@
         }
         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());
         }
     }
 
+    @SuppressWarnings("unchecked")
     public List<UserGroupMember> retrieveMemberByGroupId (int groupId)
             throws KustvaktException {
-        return retrieveMemberByGroupId(groupId, false);
-    }
-
-    @SuppressWarnings("unchecked")
-    public List<UserGroupMember> retrieveMemberByGroupId (int groupId,
-            boolean isAdmin) throws KustvaktException {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<UserGroupMember> query = criteriaBuilder
                 .createQuery(UserGroupMember.class);
@@ -154,12 +138,6 @@
         Predicate predicate = criteriaBuilder.and(criteriaBuilder
                 .equal(root.get(UserGroupMember_.group), groupId));
 
-        if (!isAdmin) {
-            predicate = criteriaBuilder.and(predicate,
-                    criteriaBuilder.notEqual(root.get(UserGroupMember_.status),
-                            GroupMemberStatus.DELETED));
-        }
-
         query.select(root);
         query.where(predicate);
         Query q = entityManager.createQuery(query);
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/QueryDto.java b/src/main/java/de/ids_mannheim/korap/dto/QueryDto.java
index 6d31fd6..6918b6d 100644
--- a/src/main/java/de/ids_mannheim/korap/dto/QueryDto.java
+++ b/src/main/java/de/ids_mannheim/korap/dto/QueryDto.java
@@ -17,7 +17,7 @@
  */
 @Getter
 @Setter
-@JsonInclude(Include.NON_DEFAULT)
+@JsonInclude(Include.NON_NULL)
 public class QueryDto {
 
     private int id;
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..b5501d4 100644
--- a/src/main/java/de/ids_mannheim/korap/dto/UserGroupDto.java
+++ b/src/main/java/de/ids_mannheim/korap/dto/UserGroupDto.java
@@ -4,7 +4,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 +29,6 @@
     @JsonInclude(JsonInclude.Include.NON_EMPTY)
     private List<UserGroupMemberDto> members;
 
-    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..40ea6d4 100644
--- a/src/main/java/de/ids_mannheim/korap/dto/UserGroupMemberDto.java
+++ b/src/main/java/de/ids_mannheim/korap/dto/UserGroupMemberDto.java
@@ -2,7 +2,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;
 
@@ -17,6 +17,5 @@
 @Getter
 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..3ffa646 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
@@ -7,7 +7,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;
@@ -27,8 +27,7 @@
 public class UserGroupConverter {
 
     public UserGroupDto createUserGroupDto (UserGroup group,
-            List<UserGroupMember> members, GroupMemberStatus userMemberStatus,
-            Set<Role> roleSet) {
+            List<UserGroupMember> members, Set<Role> roleSet) {
 
         UserGroupDto dto = new UserGroupDto();
         dto.setId(group.getId());
@@ -36,10 +35,9 @@
         dto.setDescription(group.getDescription());
         dto.setStatus(group.getStatus());
         dto.setOwner(group.getCreatedBy());
-        dto.setUserMemberStatus(userMemberStatus);
 
         if (roleSet != null) {
-            dto.setUserRoles(convertRoleSetToStringList(roleSet));
+            dto.setUserPrivileges(createPrivilegeList(roleSet));
         }
 
         if (members != null) {
@@ -49,9 +47,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 +59,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..f5522c2 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;
@@ -44,17 +45,19 @@
     private String description;
     @Column(name = "created_by")
     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/entity/UserGroupMember.java b/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
index e1cc3e5..d5d54ae 100644
--- a/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
+++ b/src/main/java/de/ids_mannheim/korap/entity/UserGroupMember.java
@@ -1,12 +1,10 @@
 package de.ids_mannheim.korap.entity;
 
-import java.time.ZonedDateTime;
 import java.util.Set;
 
+import de.ids_mannheim.korap.constant.PredefinedRole;
 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;
@@ -18,9 +16,6 @@
 import jakarta.persistence.ManyToOne;
 import jakarta.persistence.Table;
 import jakarta.persistence.UniqueConstraint;
-
-import de.ids_mannheim.korap.constant.GroupMemberStatus;
-import de.ids_mannheim.korap.constant.PredefinedRole;
 import lombok.Getter;
 import lombok.Setter;
 
@@ -45,17 +40,6 @@
     private int id;
     @Column(name = "user_id")
     private String userId;
-    @Column(name = "created_by")
-    private String createdBy;
-    @Column(name = "deleted_by")
-    private String deletedBy;
-
-    // auto update in the database
-    @Column(name = "status_date")
-    private ZonedDateTime statusDate;
-
-    @Enumerated(EnumType.STRING)
-    private GroupMemberStatus status;
 
     @ManyToOne(fetch = FetchType.EAGER)
     @JoinColumn(name = "group_id")
@@ -74,7 +58,6 @@
     @Override
     public String toString () {
         return "id=" + id + ", group= " + group + ", userId= " + userId
-                + ", createdBy= " + createdBy + ", deletedBy= " + deletedBy
                 + ", roles=" + roles;
     }
 }
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/oauth2/dto/OAuth2ClientInfoDto.java b/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientInfoDto.java
index 0fca60a..74143d2 100644
--- a/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientInfoDto.java
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/dto/OAuth2ClientInfoDto.java
@@ -19,7 +19,7 @@
  *
  */
 @JsonInclude(Include.NON_EMPTY)
-public class OAuth2ClientInfoDto {
+public class OAuth2ClientInfoDto implements Comparable<OAuth2ClientInfoDto>{
     @JsonProperty("super")
     private boolean isSuper;
 
@@ -80,6 +80,12 @@
             }
         } 
     }
+    
+    @Override
+    public int compareTo (OAuth2ClientInfoDto o) {
+        return this.getClientName().compareTo(o.getClientName());
+    }
+
 
     public boolean isSuper () {
         return isSuper;
diff --git a/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
index 36b027f..2d09877 100644
--- a/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
+++ b/src/main/java/de/ids_mannheim/korap/oauth2/service/OAuth2AuthorizationService.java
@@ -25,6 +25,7 @@
 
 import de.ids_mannheim.korap.config.Attributes;
 import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.constant.OAuth2Scope;
 import de.ids_mannheim.korap.encryption.RandomCodeGenerator;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
@@ -88,6 +89,18 @@
             OAuth2Client client = clientService.authenticateClientId(clientId);
             redirectURI = verifyRedirectUri(client, redirectUri);
             //checkResponseType(authzRequest.getResponseType(), redirectURI);
+            
+            if (scope == null || scope.isEmpty()) {
+                throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
+                        "scope is required", OAuth2Error.INVALID_SCOPE);
+            }
+            else if (!client.isSuper()
+                    && scope.contains(OAuth2Scope.ALL.toString())) {
+                throw new KustvaktException(StatusCodes.NOT_ALLOWED,
+                        "Requested scope all is not allowed.", 
+                        OAuth2Error.INVALID_SCOPE);
+            }
+            
             code = codeGenerator.createRandomCode();
             URI responseURI = createAuthorizationResponse(requestURI,
                     redirectURI, code, state);
@@ -102,7 +115,7 @@
             throw e;
         }
     }
-
+    
     private URI createAuthorizationResponse (URI requestURI, URI redirectURI,
             String code, String state) throws KustvaktException {
         AuthorizationRequest authRequest = null;
@@ -171,10 +184,6 @@
             ZonedDateTime authenticationTime, String nonce)
             throws KustvaktException {
 
-        if (scope == null || scope.isEmpty()) {
-            throw new KustvaktException(StatusCodes.MISSING_PARAMETER,
-                    "scope is required", OAuth2Error.INVALID_SCOPE);
-        }
         Set<AccessScope> accessScopes = scopeService
                 .convertToAccessScope(scope);
 
diff --git a/src/main/java/de/ids_mannheim/korap/server/KustvaktBaseServer.java b/src/main/java/de/ids_mannheim/korap/server/KustvaktBaseServer.java
index eb09557..269adb6 100644
--- a/src/main/java/de/ids_mannheim/korap/server/KustvaktBaseServer.java
+++ b/src/main/java/de/ids_mannheim/korap/server/KustvaktBaseServer.java
@@ -170,6 +170,7 @@
 
         server.setConnectors(new Connector[] { connector });
         try {
+            log.info("Starting server on port" + kargs.port);
             server.start();
             server.join();
         }
diff --git a/src/main/java/de/ids_mannheim/korap/service/MailAuthenticator.java b/src/main/java/de/ids_mannheim/korap/service/MailAuthenticator.java
deleted file mode 100644
index 21f2202..0000000
--- a/src/main/java/de/ids_mannheim/korap/service/MailAuthenticator.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package de.ids_mannheim.korap.service;
-
-import jakarta.mail.Authenticator;
-import jakarta.mail.PasswordAuthentication;
-
-/**
- * Defines Authenticator for creating javax.mail.Session.
- * 
- * @see src/main/resources/default-config.xml
- * 
- * @author margaretha
- *
- */
-public class MailAuthenticator extends Authenticator {
-
-    private PasswordAuthentication passwordAuthentication;
-
-    public MailAuthenticator (String username, String password) {
-        passwordAuthentication = new PasswordAuthentication(username, password);
-
-    }
-
-    @Override
-    protected PasswordAuthentication getPasswordAuthentication () {
-        return passwordAuthentication;
-    }
-
-}
diff --git a/src/main/java/de/ids_mannheim/korap/service/MailService.java b/src/main/java/de/ids_mannheim/korap/service/MailService.java
deleted file mode 100644
index 7619a25..0000000
--- a/src/main/java/de/ids_mannheim/korap/service/MailService.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package de.ids_mannheim.korap.service;
-
-import java.io.StringWriter;
-import java.nio.charset.StandardCharsets;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.velocity.VelocityContext;
-import org.apache.velocity.app.VelocityEngine;
-import org.apache.velocity.context.Context;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.mail.javamail.JavaMailSender;
-import org.springframework.mail.javamail.MimeMessageHelper;
-import org.springframework.mail.javamail.MimeMessagePreparator;
-import org.springframework.stereotype.Service;
-
-import de.ids_mannheim.korap.authentication.AuthenticationManager;
-import de.ids_mannheim.korap.config.FullConfiguration;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.user.User;
-import de.ids_mannheim.korap.utils.ParameterChecker;
-import jakarta.mail.internet.InternetAddress;
-import jakarta.mail.internet.MimeMessage;
-
-/**
- * Manages mail related services, such as sending group member
- * invitations per email.
- * 
- * @author margaretha
- *
- */
-@Service
-public class MailService {
-
-    public static Logger jlog = LogManager.getLogger(MailService.class);
-    public static boolean DEBUG = false;
-
-    @Autowired
-    private AuthenticationManager authenticationManager;
-    @Autowired
-    private JavaMailSender mailSender;
-    @Autowired
-    private VelocityEngine velocityEngine;
-    @Autowired
-    private FullConfiguration config;
-
-    public void sendMemberInvitationNotification (String inviteeName,
-            String groupName, String inviter) throws KustvaktException {
-
-        ParameterChecker.checkStringValue(inviteeName, "inviteeName");
-        ParameterChecker.checkStringValue(groupName, "groupName");
-        ParameterChecker.checkStringValue(inviter, "inviter");
-
-        MimeMessagePreparator preparator = new MimeMessagePreparator() {
-
-            public void prepare (MimeMessage mimeMessage) throws Exception {
-
-                User invitee = authenticationManager.getUser(inviteeName,
-                        config.getEmailAddressRetrieval());
-
-                MimeMessageHelper message = new MimeMessageHelper(mimeMessage);
-                message.setTo(new InternetAddress(invitee.getEmail()));
-                message.setFrom(config.getNoReply());
-                message.setSubject("Invitation to join " + groupName);
-                message.setText(prepareGroupInvitationText(inviteeName,
-                        groupName, inviter), true);
-            }
-
-        };
-        mailSender.send(preparator);
-    }
-
-    private String prepareGroupInvitationText (String username,
-            String groupName, String inviter) {
-        Context context = new VelocityContext();
-        context.put("username", username);
-        context.put("group", groupName);
-        context.put("inviter", inviter);
-
-        StringWriter stringWriter = new StringWriter();
-
-        velocityEngine.mergeTemplate(
-                "templates/" + config.getGroupInvitationTemplate(),
-                StandardCharsets.UTF_8.name(), context, stringWriter);
-
-        String message = stringWriter.toString();
-        if (DEBUG)
-            jlog.debug(message);
-        return message;
-    }
-}
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..1962167 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;
@@ -17,19 +19,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.QueryDto;
-import de.ids_mannheim.korap.dto.converter.QueryAccessConverter;
+import de.ids_mannheim.korap.dto.RoleDto;
 import de.ids_mannheim.korap.dto.converter.QueryConverter;
-import de.ids_mannheim.korap.entity.QueryAccess;
+import de.ids_mannheim.korap.dto.converter.RoleConverter;
 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 +47,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 +79,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 +96,7 @@
     @Autowired
     private QueryConverter converter;
     @Autowired
-    private QueryAccessConverter accessConverter;
+    private RoleConverter roleConverter;
 
     private void verifyUsername (String contextUsername, String pathUsername)
             throws KustvaktException {
@@ -145,7 +155,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 +165,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);
             }
             if (type.equals(QueryType.VIRTUAL_CORPUS)
                     && VirtualCorpusCache.contains(queryName)) {
@@ -173,7 +181,7 @@
         }
         else {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
-                    "Unauthorized operation for user: " + username, username);
+                    "Unauthorized operation for user: " + deletedBy, deletedBy);
         }
     }
 
@@ -222,11 +230,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);
                     // EM: should the users within the hidden group
                     // receive
                     // notifications?
@@ -234,7 +241,7 @@
                 // else remains the same
             }
             else if (type.equals(ResourceType.PUBLISHED)) {
-                publishQuery(existingQuery.getId());
+                publishQuery(existingQuery.getId(), username, queryName);
             }
         }
 
@@ -244,24 +251,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 +393,7 @@
                     cause.getMessage());
         }
         if (type.equals(ResourceType.PUBLISHED)) {
-            publishQuery(queryId);
+            publishQuery(queryId, queryCreator, queryName);
         }
     }
 
@@ -476,153 +485,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 +611,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 {
@@ -681,14 +679,14 @@
             String createdBy, QueryType queryType) throws KustvaktException {
         QueryDO query = searchQueryByName(username, queryName, createdBy,
                 queryType);
-        // String json = query.getKoralQuery();
+
         String statistics = null;
         // long start,end;
         // start = System.currentTimeMillis();
-        // if (query.getQueryType().equals(QueryType.VIRTUAL_CORPUS))
-        // {
-        // statistics = krill.getStatistics(json);
-        // }
+         if (query.getQueryType().equals(QueryType.VIRTUAL_CORPUS)) {
+              String json = query.getKoralQuery();
+              statistics = krill.getStatistics(json);
+         }
         // end = System.currentTimeMillis();
         // jlog.debug("{} statistics duration: {}", queryName, (end -
         // start));
@@ -713,7 +711,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 +721,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", 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", memberRoles);                
+                }
                 catch (KustvaktException e) {
                     // member exists
                     // skip adding user to hidden group
@@ -738,16 +753,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..11e9c12 100644
--- a/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
+++ b/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
@@ -1,7 +1,6 @@
 package de.ids_mannheim.korap.service;
 
 import java.sql.SQLException;
-import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
@@ -15,9 +14,8 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
-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;
@@ -63,16 +61,10 @@
     @Autowired
     private UserGroupConverter converter;
     @Autowired
-    private FullConfiguration config;
-    @Autowired
-    private MailService mailService;
-    @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
@@ -103,8 +95,8 @@
             members = retrieveMembers(group.getId(), username);
             userAsMember = groupMemberDao.retrieveMemberById(username,
                     group.getId());
-            groupDto = converter.createUserGroupDto(group, members,
-                    userAsMember.getStatus(), userAsMember.getRoles());
+            groupDto = converter.createUserGroupDto(group, members, 
+                    userAsMember.getRoles());
             dtos.add(groupDto);
         }
 
@@ -115,7 +107,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 +130,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);
     }
 
     public List<UserGroupDto> retrieveUserGroupByStatus (String username,
@@ -154,40 +155,37 @@
         List<UserGroupMember> members;
         UserGroupDto groupDto;
         for (UserGroup group : userGroups) {
-            members = groupMemberDao.retrieveMemberByGroupId(group.getId(),
-                    true);
-            groupDto = converter.createUserGroupDto(group, members, null, null);
+            members = groupMemberDao.retrieveMemberByGroupId(group.getId());
+            groupDto = converter.createUserGroupDto(group, members, null);
             dtos.add(groupDto);
         }
         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_SELF, 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 +216,7 @@
         UserGroup userGroup = null;
         boolean groupExists = false;
         try {
-            userGroup = userGroupDao.retrieveGroupByName(groupName, false);
+            userGroup = retrieveUserGroupByName(groupName);
             groupExists = true;
         }
         catch (KustvaktException e) {
@@ -231,7 +229,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,19 +255,10 @@
 
     public void deleteGroup (String groupName, String username)
             throws KustvaktException {
-        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName,
-                false);
-        if (userGroup.getStatus() == UserGroupStatus.DELETED) {
-            // EM: should this be "not found" instead?
-            throw new KustvaktException(StatusCodes.GROUP_DELETED,
-                    "Group " + userGroup.getName() + " has been deleted.",
-                    userGroup.getName());
-        }
-        else if (userGroup.getCreatedBy().equals(username)
+        UserGroup userGroup = retrieveUserGroupByName(groupName);
+        if (userGroup.getCreatedBy().equals(username)
                 || adminDao.isAdmin(username)) {
-            // soft delete
-            userGroupDao.deleteGroup(userGroup.getId(), username,
-                    config.isSoftDeleteGroup());
+            userGroupDao.deleteGroup(userGroup.getId(), username);
         }
         else {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
@@ -277,137 +266,66 @@
         }
     }
 
-    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
-     * group,
-     * the entry will be deleted first, and a new entry will be added.
-     * 
-     * If a username with other statuses exists, a KustvaktException
-     * will
-     * be thrown.
-     * 
-     * @see GroupMemberStatus
-     * 
-     * @param username
-     *            a username
-     * @param userGroup
-     *            a user group
-     * @param createdBy
-     *            the user (query-access admin/system) adding the user
-     *            the user-group
-     * @param status
-     *            the status of the membership
-     * @throws KustvaktException
-     */
-    public void inviteGroupMember (String username, UserGroup userGroup,
-            String createdBy, GroupMemberStatus status)
-            throws KustvaktException {
-
-        addGroupMember(username, userGroup, createdBy, status);
-
-        if (config.isMailEnabled()
-                && userGroup.getStatus() != UserGroupStatus.HIDDEN) {
-            mailService.sendMemberInvitationNotification(username,
-                    userGroup.getName(), createdBy);
-        }
-    }
-
     public void addGroupMember (String username, UserGroup userGroup,
-            String createdBy, GroupMemberStatus status)
+            String createdBy, Set<Role> roles)
             throws KustvaktException {
-        int groupId = userGroup.getId();
-        ParameterChecker.checkIntegerValue(groupId, "userGroupId");
-
-        GroupMemberStatus existingStatus = memberExists(username, groupId,
-                status);
-        if (existingStatus != null) {
+        
+        if (!isMember(username, userGroup)) {
+            int groupId = userGroup.getId();
+            ParameterChecker.checkIntegerValue(groupId, "userGroupId");
+    
+            UserGroupMember member = new UserGroupMember();
+            member.setGroup(userGroup);
+            member.setUserId(username);
+            if (roles !=null) {
+                member.setRoles(roles);
+            }
+            groupMemberDao.addMember(member);
+        }
+        else {
             throw new KustvaktException(StatusCodes.GROUP_MEMBER_EXISTS,
-                    "Username " + username + " with status " + existingStatus
-                            + " exists in the user-group "
-                            + userGroup.getName(),
-                    username, existingStatus.name(), userGroup.getName());
+                    "Username: "+username+" exists in the user-group: "+
+                    userGroup.getName(), username, userGroup.getName());
         }
-
-        UserGroupMember member = new UserGroupMember();
-        member.setCreatedBy(createdBy);
-        member.setGroup(userGroup);
-        member.setStatus(status);
-        member.setUserId(username);
-        groupMemberDao.addMember(member);
     }
 
-    private GroupMemberStatus memberExists (String username, int groupId,
-            GroupMemberStatus status) throws KustvaktException {
-        UserGroupMember existingMember;
-        try {
-            existingMember = groupMemberDao.retrieveMemberById(username,
-                    groupId);
-        }
-        catch (KustvaktException e) {
-            return null;
-        }
-
-        GroupMemberStatus existingStatus = existingMember.getStatus();
-        if (existingStatus.equals(GroupMemberStatus.ACTIVE)
-                || existingStatus.equals(status)) {
-            return existingStatus;
-        }
-        else if (existingStatus.equals(GroupMemberStatus.DELETED)) {
-            // hard delete, not customizable
-            doDeleteMember(username, groupId, "system", false);
-        }
-
-        return null;
-    }
-
-    public void inviteGroupMembers (String groupName, String groupMembers,
-            String inviter) throws KustvaktException {
+    public void addGroupMembers (String groupName, String groupMembers,
+            String username) throws KustvaktException {
         String[] members = groupMembers.split(",");
         ParameterChecker.checkStringValue(groupName, "group name");
         ParameterChecker.checkStringValue(groupMembers, "members");
 
         UserGroup userGroup = retrieveUserGroupByName(groupName);
-        if (userGroup.getStatus() == UserGroupStatus.DELETED) {
-            throw new KustvaktException(StatusCodes.GROUP_DELETED,
-                    "Group " + userGroup.getName() + " has been deleted.",
-                    userGroup.getName());
-        }
-
-        if (isUserGroupAdmin(inviter, userGroup) || adminDao.isAdmin(inviter)) {
+        if (isUserGroupAdmin(username, userGroup)
+                || adminDao.isAdmin(username)) {
+            Set<Role> memberRoles = prepareMemberRoles(userGroup);
             for (String memberName : members) {
-                inviteGroupMember(memberName, userGroup, inviter,
-                        GroupMemberStatus.PENDING);
+                addGroupMember(memberName, userGroup, username,memberRoles);
             }
         }
         else {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
-                    "Unauthorized operation for user: " + inviter, inviter);
+                    "Unauthorized operation for user: " + username, username);
         }
     }
 
-    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,77 +335,12 @@
         return false;
     }
 
-    /**
-     * Updates the {@link GroupMemberStatus} of a pending member
-     * to {@link GroupMemberStatus#ACTIVE} and add default member
-     * roles.
-     * 
-     * @param groupId
-     *            groupId
-     * @param username
-     *            the username of the group member
-     * @throws KustvaktException
-     */
-    public void acceptInvitation (String groupName, String username)
-            throws KustvaktException {
-
-        ParameterChecker.checkStringValue(username, "userId");
-        ParameterChecker.checkStringValue(groupName, "groupId");
-
-        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName,
-                false);
-        if (userGroup.getStatus() == UserGroupStatus.DELETED) {
-            throw new KustvaktException(StatusCodes.GROUP_DELETED,
-                    "Group " + userGroup.getName() + " has been deleted.",
-                    userGroup.getName());
-        }
-
-        UserGroupMember member = groupMemberDao.retrieveMemberById(username,
-                userGroup.getId());
-        GroupMemberStatus status = member.getStatus();
-        if (status.equals(GroupMemberStatus.DELETED)) {
-            throw new KustvaktException(StatusCodes.GROUP_MEMBER_DELETED,
-                    username + " has already been deleted from the group "
-                            + userGroup.getName(),
-                    username, userGroup.getName());
-        }
-        else if (member.getStatus().equals(GroupMemberStatus.ACTIVE)) {
-            throw new KustvaktException(StatusCodes.GROUP_MEMBER_EXISTS,
-                    "Username " + username + " with status " + status
-                            + " exists in the user-group "
-                            + userGroup.getName(),
-                    username, status.name(), userGroup.getName());
-        }
-        // status pending
-        else {
-            if (DEBUG) {
-                jlog.debug("status: " + member.getStatusDate());
-            }
-            ZonedDateTime expiration = member.getStatusDate().plusMinutes(30);
-            ZonedDateTime now = ZonedDateTime.now();
-            if (DEBUG) {
-                jlog.debug("expiration: " + expiration + ", now: " + now);
-            }
-
-            if (expiration.isAfter(now)) {
-                member.setStatus(GroupMemberStatus.ACTIVE);
-                setMemberRoles();
-                member.setRoles(memberRoles);
-                groupMemberDao.updateMember(member);
-            }
-            else {
-                throw new KustvaktException(StatusCodes.INVITATION_EXPIRED);
-            }
-        }
-    }
-
     public boolean isMember (String username, UserGroup userGroup)
             throws KustvaktException {
         List<UserGroupMember> members = groupMemberDao
                 .retrieveMemberByGroupId(userGroup.getId());
         for (UserGroupMember member : members) {
-            if (member.getUserId().equals(username)
-                    && member.getStatus().equals(GroupMemberStatus.ACTIVE)) {
+            if (member.getUserId().equals(username)) {
                 return true;
             }
         }
@@ -497,14 +350,8 @@
     public void deleteGroupMember (String memberId, String groupName,
             String deletedBy) throws KustvaktException {
 
-        UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName,
-                false);
-        if (userGroup.getStatus() == UserGroupStatus.DELETED) {
-            throw new KustvaktException(StatusCodes.GROUP_DELETED,
-                    "Group " + userGroup.getName() + " has been deleted.",
-                    userGroup.getName());
-        }
-        else if (memberId.equals(userGroup.getCreatedBy())) {
+        UserGroup userGroup = retrieveUserGroupByName(groupName);
+        if (memberId.equals(userGroup.getCreatedBy())) {
             throw new KustvaktException(StatusCodes.NOT_ALLOWED,
                     "Operation " + "'delete group owner'" + "is not allowed.",
                     "delete group owner");
@@ -512,9 +359,9 @@
         else if (memberId.equals(deletedBy)
                 || isUserGroupAdmin(deletedBy, userGroup)
                 || adminDao.isAdmin(deletedBy)) {
-            // soft delete
-            doDeleteMember(memberId, userGroup.getId(), deletedBy,
-                    config.isSoftDeleteGroupMember());
+            UserGroupMember member = groupMemberDao.retrieveMemberById(memberId,
+                    userGroup.getId());
+            groupMemberDao.deleteMember(member, deletedBy);
         }
         else {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
@@ -522,79 +369,61 @@
         }
     }
 
-    /**
-     * Updates the {@link GroupMemberStatus} of a member to
-     * {@link GroupMemberStatus#DELETED}
-     * 
-     * @param userId
-     *            user to be deleted
-     * @param groupId
-     *            user-group id
-     * @param deletedBy
-     *            user that issue the delete
-     * @param isSoftDelete
-     *            true if database entry is to be deleted
-     *            permanently, false otherwise
-     * @throws KustvaktException
-     */
-    private void doDeleteMember (String username, int groupId, String deletedBy,
-            boolean isSoftDelete) throws KustvaktException {
-
-        UserGroup group = userGroupDao.retrieveGroupById(groupId);
-
-        UserGroupMember member = groupMemberDao.retrieveMemberById(username,
-                groupId);
-        GroupMemberStatus status = member.getStatus();
-        if (isSoftDelete && status.equals(GroupMemberStatus.DELETED)) {
-            throw new KustvaktException(StatusCodes.GROUP_MEMBER_DELETED,
-                    username + " has already been deleted from the group "
-                            + group.getName(),
-                    username, group.getName());
-        }
-
-        groupMemberDao.deleteMember(member, deletedBy, isSoftDelete);
-    }
-
     public UserGroupDto searchByName (String groupName)
             throws KustvaktException {
         UserGroup userGroup = userGroupDao.retrieveGroupByName(groupName, true);
         UserGroupDto groupDto = converter.createUserGroupDto(userGroup,
-                userGroup.getMembers(), null, null);
+                userGroup.getMembers(), null);
         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");
 
         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)
+
+        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());
-            }
+            if (!isUserGroupAdmin(memberUsername, userGroup)) {
+                Set<Role> existingRoles = member.getRoles();
+                PredefinedRole role = PredefinedRole.GROUP_ADMIN;
 
-            Set<Role> roles = new HashSet<>();
-            for (int i = 0; i < roleIds.size(); i++) {
-                roles.add(roleDao.retrieveRoleById(roleIds.get(i)));
+                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 +431,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 +451,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 +465,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/OAuthClientController.java b/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
index ee2d72f..1c41998 100644
--- a/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
+++ b/src/main/java/de/ids_mannheim/korap/web/controller/OAuthClientController.java
@@ -7,6 +7,7 @@
 
 import de.ids_mannheim.korap.constant.OAuth2Scope;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientDto;
 import de.ids_mannheim.korap.oauth2.dto.OAuth2ClientInfoDto;
 import de.ids_mannheim.korap.oauth2.service.OAuth2ClientService;
@@ -16,7 +17,6 @@
 import de.ids_mannheim.korap.web.filter.APIVersionFilter;
 import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
 import de.ids_mannheim.korap.web.filter.BlockingFilter;
-import de.ids_mannheim.korap.web.filter.DemoFilter;
 import de.ids_mannheim.korap.web.filter.DemoUserFilter;
 import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
 import de.ids_mannheim.korap.web.utils.ResourceFilters;
@@ -220,7 +220,8 @@
             @Context SecurityContext context,
             @FormParam("super_client_id") String superClientId,
             @FormParam("super_client_secret") String superClientSecret,
-            @FormParam("authorized_only") boolean authorizedOnly) {
+            @FormParam("authorized_only") boolean authorizedOnly, // deprecated
+            @FormParam("filter_by") String filterBy) {
 
         TokenContext tokenContext = (TokenContext) context.getUserPrincipal();
         String username = tokenContext.getUsername();
@@ -230,12 +231,34 @@
                     OAuth2Scope.LIST_USER_CLIENT);
 
             clientService.verifySuperClient(superClientId, superClientSecret);
+            
+            List<OAuth2ClientInfoDto> clients = null; 
+            
             if (authorizedOnly) {
-                return clientService.listUserAuthorizedClients(username);
+                clients = clientService.listUserAuthorizedClients(username);
             }
             else {
-                return clientService.listUserRegisteredClients(username);
+                if (filterBy !=null && !filterBy.isEmpty()) {
+                    if (filterBy.equals("authorized_only")) {
+                        clients = clientService.listUserAuthorizedClients(username);
+                    }
+                    else if (filterBy.equals("owned_only")) {
+                        clients = clientService.listUserRegisteredClients(username); 
+                    }
+                    else {
+                        throw new KustvaktException(
+                                StatusCodes.UNSUPPORTED_VALUE, "filter_by");
+                    }
+                }
+                else {               
+//                    clients = clientService.listUserAuthorizedClients(username);
+//                    clients.addAll(clientService.listUserRegisteredClients(username));
+                
+                    clients = clientService.listUserRegisteredClients(username);
+                }
             }
+            
+            return clients;
         }
         catch (KustvaktException e) {
             throw responseHandler.throwit(e);
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..a968946 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,8 +7,10 @@
 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.exceptions.StatusCodes;
 import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
 import de.ids_mannheim.korap.security.context.TokenContext;
 import de.ids_mannheim.korap.service.UserGroupService;
@@ -200,6 +202,7 @@
      *            usernames separated by comma
      * @return if successful, HTTP response status OK
      */
+    @Deprecated
     @POST
     @Path("@{groupName}/invite")
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@@ -207,54 +210,32 @@
             @Context SecurityContext securityContext,
             @PathParam("groupName") String groupName,
             @FormParam("members") String members) {
+        throw kustvaktResponseHandler.throwit(new KustvaktException(
+                StatusCodes.DEPRECATED,
+                "This web-service is deprecated and will be completely removed "
+                + "in API v1.1."));
+    }
+    
+    @PUT
+    @Path("@{groupName}/member")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response addGroupMembers (
+            @Context SecurityContext securityContext,
+            @PathParam("groupName") String groupName,
+            @FormParam("members") String members) {
         TokenContext context = (TokenContext) securityContext
                 .getUserPrincipal();
         try {
             scopeService.verifyScope(context,
                     OAuth2Scope.ADD_USER_GROUP_MEMBER);
-            service.inviteGroupMembers(groupName, members,
-                    context.getUsername());
-            return Response.ok("SUCCESS").build();
+            service.addGroupMembers(groupName, members, context.getUsername());
+            return Response.ok().build();
         }
         catch (KustvaktException e) {
             throw kustvaktResponseHandler.throwit(e);
         }
     }
-
-    /**
-     * 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.
@@ -268,6 +249,7 @@
      *            a role id or multiple role ids
      * @return if successful, HTTP response status OK
      */
+    @Deprecated
     @POST
     @Path("@{groupName}/role/add")
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
@@ -275,14 +257,35 @@
             @PathParam("groupName") String groupName,
             @FormParam("memberUsername") String memberUsername,
             @FormParam("roleId") List<Integer> roleIds) {
+        throw kustvaktResponseHandler.throwit(new KustvaktException(
+                StatusCodes.DEPRECATED,
+                "This web-service is deprecated and will be completely removed "
+                + "in API v1.1."));
+    }
+
+    /**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
+     * @return HTTP status 200, if successful 
+     */
+    @POST
+    @Path("@{groupName}/role/add/admin")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response addAdminRole (@Context SecurityContext securityContext,
+            @PathParam("groupName") String groupName,
+            @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);
-            return Response.ok("SUCCESS").build();
+            service.addAdminRole(context.getUsername(), groupName,
+                    memberUsername);
+            return Response.ok().build();
         }
         catch (KustvaktException e) {
             throw kustvaktResponseHandler.throwit(e);
@@ -309,15 +312,24 @@
     public Response deleteMemberRoles (@Context SecurityContext securityContext,
             @PathParam("groupName") String groupName,
             @FormParam("memberUsername") String memberUsername,
-            @FormParam("roleId") List<Integer> roleIds) {
+            @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);
-            return Response.ok("SUCCESS").build();
+            if (roleIds != null && !roleIds.isEmpty()){
+                throw kustvaktResponseHandler.throwit(new KustvaktException(
+                        StatusCodes.DEPRECATED,
+                        "Parameter roleIds is deprecated and will be completely"
+                        + " removed in API v1.1."));
+            }
+            else {
+                service.deleteMemberRoles(context.getUsername(), groupName,
+                        memberUsername, roles);
+            }
+            return Response.ok().build();
         }
         catch (KustvaktException e) {
             throw kustvaktResponseHandler.throwit(e);
@@ -333,21 +345,15 @@
      *            a group name
      * @return if successful, HTTP response status OK
      */
+    @Deprecated
     @POST
     @Path("@{groupName}/subscribe")
     public Response subscribeToGroup (@Context SecurityContext securityContext,
             @PathParam("groupName") String groupName) {
-        TokenContext context = (TokenContext) securityContext
-                .getUserPrincipal();
-        try {
-            scopeService.verifyScope(context,
-                    OAuth2Scope.ADD_USER_GROUP_MEMBER);
-            service.acceptInvitation(groupName, context.getUsername());
-            return Response.ok("SUCCESS").build();
-        }
-        catch (KustvaktException e) {
-            throw kustvaktResponseHandler.throwit(e);
-        }
+        throw kustvaktResponseHandler.throwit(new KustvaktException(
+                StatusCodes.DEPRECATED,
+                "This web-service is deprecated and will be completely removed "
+                + "in API v1.1."));
     }
 
     /**
@@ -361,22 +367,15 @@
      * @param groupName
      * @return if successful, HTTP response status OK
      */
+    @Deprecated
     @DELETE
     @Path("@{groupName}/unsubscribe")
     public Response unsubscribeFromGroup (
             @Context SecurityContext securityContext,
             @PathParam("groupName") String groupName) {
-        TokenContext context = (TokenContext) securityContext
-                .getUserPrincipal();
-        try {
-            scopeService.verifyScope(context,
-                    OAuth2Scope.DELETE_USER_GROUP_MEMBER);
-            service.deleteGroupMember(context.getUsername(), groupName,
-                    context.getUsername());
-            return Response.ok("SUCCESS").build();
-        }
-        catch (KustvaktException e) {
-            throw kustvaktResponseHandler.throwit(e);
-        }
+        throw kustvaktResponseHandler.throwit(new KustvaktException(
+                StatusCodes.DEPRECATED,
+                "This web-service is deprecated and will be complete removed "
+                + "in API v1.1."));
     }
 }
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..27aff66 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,40 @@
      * @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());
-        }
-        catch (KustvaktException e) {
-            throw kustvaktResponseHandler.throwit(e);
-        }
-        return Response.ok().build();
+        throw kustvaktResponseHandler.throwit(new KustvaktException(
+                StatusCodes.DEPRECATED,
+                "This web-service is deprecated and will be completely removed "
+                + "in API v1.1."));
     }
-
+    
     /**
-     * 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/java/de/ids_mannheim/korap/web/input/UserGroupJson.java b/src/main/java/de/ids_mannheim/korap/web/input/UserGroupJson.java
deleted file mode 100644
index 0cc4922..0000000
--- a/src/main/java/de/ids_mannheim/korap/web/input/UserGroupJson.java
+++ /dev/null
@@ -1,22 +0,0 @@
-package de.ids_mannheim.korap.web.input;
-
-import de.ids_mannheim.korap.web.controller.UserGroupController;
-import lombok.Getter;
-import lombok.Setter;
-
-/**
- * Java POJO of JSON input used in the user group controller for
- * creating user group and managing group members.
- * 
- * @author margaretha
- * @see UserGroupController
- */
-@Deprecated
-@Getter
-@Setter
-public class UserGroupJson {
-
-    private int id;
-    private String name;
-    private String[] members;
-}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFilters.java b/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFilters.java
deleted file mode 100644
index 249793b..0000000
--- a/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFilters.java
+++ /dev/null
@@ -1,27 +0,0 @@
-package de.ids_mannheim.korap.web.utils;
-
-import java.lang.annotation.ElementType;
-import java.lang.annotation.Retention;
-import java.lang.annotation.RetentionPolicy;
-import java.lang.annotation.Target;
-
-/**
- * Defines the list of
- * {@link jakarta.ws.rs.container.ContainerRequestFilter}
- * and {@link jakarta.ws.rs.container.ContainerResponseFilter}
- * classes associated with a resource method.
- * <p>
- * This annotation can be specified on a class or on method(s).
- * Specifying it
- * at a class level means that it applies to all the methods in the
- * class.
- * Specifying it on a method means that it is applicable to that
- * method only.
- * If applied at both the class and methods level , the method value
- * overrides
- * the class value.
- */
-@Deprecated
-@Target({ ElementType.TYPE, ElementType.METHOD })
-@Retention(RetentionPolicy.RUNTIME)
-public @interface SearchResourceFilters {}
diff --git a/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFiltersFeature.java b/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFiltersFeature.java
deleted file mode 100644
index 341817e..0000000
--- a/src/main/java/de/ids_mannheim/korap/web/utils/SearchResourceFiltersFeature.java
+++ /dev/null
@@ -1,60 +0,0 @@
-package de.ids_mannheim.korap.web.utils;
-
-import java.util.Arrays;
-import java.util.List;
-
-import org.glassfish.jersey.model.internal.CommonConfig;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Component;
-
-import de.ids_mannheim.korap.web.filter.APIVersionFilter;
-import jakarta.ws.rs.container.DynamicFeature;
-import jakarta.ws.rs.container.ResourceInfo;
-import jakarta.ws.rs.core.FeatureContext;
-import jakarta.ws.rs.ext.Provider;
-
-/**
- * Registers {@link jakarta.ws.rs.container.ContainerRequestFilter}
- * and {@link jakarta.ws.rs.container.ContainerResponseFilter}
- * classes for a resource method annotated with
- * {@link ResourceFilters}.
- */
-@Deprecated
-@Provider
-@Component
-public class SearchResourceFiltersFeature implements DynamicFeature {
-
-    @Value("${search.resource.filters:AuthenticationFilter,DemoUserFilter}")
-    private String[] resourceFilters;
-
-    @Override
-    public void configure (ResourceInfo resourceInfo, FeatureContext context) {
-        SearchResourceFilters filters = resourceInfo.getResourceMethod()
-                .getAnnotation(SearchResourceFilters.class);
-        if (filters != null) {
-            CommonConfig con = (CommonConfig) context.getConfiguration();
-            con.getComponentBag().clear();
-        }
-        else {
-            filters = resourceInfo.getResourceClass()
-                    .getAnnotation(SearchResourceFilters.class);
-        }
-
-        if (filters != null) {
-            List<?> list = Arrays.asList(resourceFilters);
-            if (!list.contains("APIVersionFilter")) {
-                context.register(APIVersionFilter.class);
-            }
-
-            for (String c : resourceFilters) {
-                try {
-                    context.register(Class
-                            .forName("de.ids_mannheim.korap.web.filter." + c));
-                }
-                catch (ClassNotFoundException e) {
-                    e.printStackTrace();
-                }
-            }
-        }
-    }
-}
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__role_alteration.sql b/src/main/resources/db/sqlite/V1.13__role_alteration.sql
new file mode 100644
index 0000000..8def1df
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.13__role_alteration.sql
@@ -0,0 +1,45 @@
+DROP INDEX IF EXISTS role_index;
+
+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);
+  
+DELETE FROM oauth2_access_scope where id="EDIT_USER_GROUP_MEMBER_ROLE";  
+  
\ No newline at end of file
diff --git a/src/main/resources/db/sqlite/V1.14__user_group_alteration.sql b/src/main/resources/db/sqlite/V1.14__user_group_alteration.sql
new file mode 100644
index 0000000..1be030e
--- /dev/null
+++ b/src/main/resources/db/sqlite/V1.14__user_group_alteration.sql
@@ -0,0 +1,27 @@
+-- please commented out the indexes in V1.1 later
+DROP INDEX IF EXISTS group_member_role_index;
+DROP INDEX IF EXISTS user_group_member_status_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
+ADD COLUMN created_date TIMESTAMP;
+
+ALTER TABLE user_group
+DROP COLUMN deleted_by;
+
+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;
+  
\ 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/main/resources/default-config.xml b/src/main/resources/default-config.xml
index 0e12214..f76785f 100644
--- a/src/main/resources/default-config.xml
+++ b/src/main/resources/default-config.xml
@@ -42,8 +42,6 @@
 				<value>classpath:properties/jdbc.properties</value>
 				<value>file:./jdbc.properties</value>
 				<value>file:./data/jdbc.properties</value>
-				<value>classpath:properties/mail.properties</value>
-				<value>file:./mail.properties</value>
 				<value>classpath:properties/hibernate.properties</value>
 
 				<value>classpath:kustvakt.conf</value>
@@ -289,40 +287,4 @@
 		<!-- <property name="dataSource" ref="c3p0DataSource" /> -->
 	</bean>
 
-	<!-- mail -->
-	<bean id="authenticator"
-		class="de.ids_mannheim.korap.service.MailAuthenticator">
-		<constructor-arg index="0" value="${mail.username}" />
-		<constructor-arg index="1" value="${mail.password}" />
-	</bean>
-	<bean id="smtpSession" class="jakarta.mail.Session"
-		factory-method="getInstance">
-		<constructor-arg index="0">
-			<props>
-				<prop key="mail.smtp.submitter">${mail.username}</prop>
-				<prop key="mail.smtp.auth">${mail.auth}</prop>
-				<prop key="mail.smtp.host">${mail.host}</prop>
-				<prop key="mail.smtp.port">${mail.port}</prop>
-				<prop key="mail.smtp.starttls.enable">${mail.starttls.enable}</prop>
-				<prop key="mail.smtp.connectiontimeout">${mail.connectiontimeout}</prop>
-			</props>
-		</constructor-arg>
-		<constructor-arg index="1" ref="authenticator" />
-	</bean>
-	<bean id="mailSender"
-		class="org.springframework.mail.javamail.JavaMailSenderImpl">
-		<property name="username" value="${mail.username}" />
-		<property name="password" value="${mail.password}" />
-		<property name="session" ref="smtpSession" />
-	</bean>
-	<bean id="velocityEngine"
-		class="org.apache.velocity.app.VelocityEngine">
-		<constructor-arg index="0">
-			<props>
-				<prop key="resource.loader">class</prop>
-				<prop key="class.resource.loader.class">org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
-				</prop>
-			</props>
-		</constructor-arg>
-	</bean>
 </beans>
diff --git a/src/main/resources/kustvakt.conf b/src/main/resources/kustvakt.conf
index e90c513..96a5823 100644
--- a/src/main/resources/kustvakt.conf
+++ b/src/main/resources/kustvakt.conf
@@ -21,15 +21,6 @@
 server.port=8089
 server.host=localhost
 
-# mail settings
-mail.enabled = false
-mail.receiver = test@localhost
-mail.sender = noreply@ids-mannheim.de
-mail.address.retrieval = test
-
-# mail.templates
-template.group.invitation = notification.vm
-
 # default foundries for specific layers
 default.foundry.partOfSpeech = tt
 default.foundry.lemma = tt
diff --git a/src/main/resources/properties/mail.properties b/src/main/resources/properties/mail.properties
deleted file mode 100644
index 29d1ca1..0000000
--- a/src/main/resources/properties/mail.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-mail.host = localhost
-mail.port = 25
-mail.connectiontimeout = 3000
-mail.auth = false
-mail.starttls.enable = false
-mail.username = username
-mail.password = password
\ No newline at end of file
diff --git a/src/test/java/de/ids_mannheim/korap/cache/TotalResultTest.java b/src/test/java/de/ids_mannheim/korap/cache/TotalResultTest.java
index 07f4485..7886ddc 100644
--- a/src/test/java/de/ids_mannheim/korap/cache/TotalResultTest.java
+++ b/src/test/java/de/ids_mannheim/korap/cache/TotalResultTest.java
@@ -1,11 +1,15 @@
 package de.ids_mannheim.korap.cache;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+
+import de.ids_mannheim.korap.config.KustvaktConfiguration;
 import de.ids_mannheim.korap.config.SpringJerseyTest;
 import de.ids_mannheim.korap.core.service.SearchService;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
@@ -17,6 +21,9 @@
 
     @Autowired
     private SearchService searchService;
+    
+    @Autowired
+    private KustvaktConfiguration config;
 
     @Test
     public void testClearCache () {
@@ -54,8 +61,7 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         entity = response.readEntity(String.class);
         node = JsonUtils.readTree(entity);
-        assertTrue(node.at("/meta/totalResults").isNumber(),
-                "totalResults should be a number");
+        assertTrue(node.at("/meta/totalResults").isNumber());
         assertEquals(totalResults, node.at("/meta/totalResults").asInt());
         assertEquals(1, searchService.getTotalResultCache()
                 .getAllCacheElements().size());
@@ -108,4 +114,79 @@
         node = JsonUtils.readTree(entity);
         assertTrue(node.at("/meta/cutOff").asBoolean());
     }
+    
+    @Test
+    public void testCacheDisabled () throws KustvaktException {
+        searchService.getTotalResultCache().clearCache();
+        assertEquals(0, searchService.getTotalResultCache()
+                .getAllCacheElements().size());
+
+        config.setTotalResultCacheEnabled(false);
+        
+        Response response = target().path(API_VERSION).path("search")
+                .queryParam("q", "[orth=zu]").queryParam("ql", "poliqarp")
+                .queryParam("page", "1").request().get();
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertTrue(node.at("/meta/totalResults").isNumber(),
+                "totalResults should be a number");
+        assertEquals(0, searchService.getTotalResultCache()
+                .getAllCacheElements().size());
+        
+        config.setTotalResultCacheEnabled(true);
+        
+        response = target().path(API_VERSION).path("search")
+                .queryParam("q", "[orth=zu]").queryParam("ql", "poliqarp")
+                .queryParam("page", "1").request().get();
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        assertEquals(1, searchService.getTotalResultCache()
+                .getAllCacheElements().size());
+        
+        searchService.getTotalResultCache().clearCache();
+    }
+    
+    @Test
+    public void testCacheKey () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("search")
+                .queryParam("q", "[orth=populistischer]")
+                .queryParam("ql", "poliqarp")
+                .queryParam("cq", "availability!=QAO-NC-LOC:ids & corpusSigle = "
+                        + "/SOL|[UTSZ][0-9][0-9]/ & pubDate in 1976")
+                //.queryParam("fields", "corpusSigle,textSigle,pubDate,pubPlace,"
+                //        + "availability,textClass")
+                .queryParam("access-rewrite-disabled", "true")
+                .queryParam("page", "1").request().get();
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        
+        ObjectNode queryNode = (ObjectNode) JsonUtils.readTree(entity);
+        queryNode.remove("meta");
+        queryNode.remove("matches");
+        int queryHashCode1 = queryNode.hashCode();
+        int queryStringHashCode1 = queryNode.toString().hashCode();
+        
+        response = target().path(API_VERSION).path("search")
+                .queryParam("q", "[orth=populistisches]")
+                .queryParam("ql", "poliqarp")
+                .queryParam("cq", "availability!=QAO-NC-LOC:ids & corpusSigle = "
+                        + "/SOL|[UTSZ][0-9][0-9]/ & pubDate in 1975")
+                //.queryParam("fields", "corpusSigle,textSigle,pubDate,pubPlace,"
+                //        + "availability,textClass")
+                .queryParam("access-rewrite-disabled", "true")
+                .queryParam("page", "1").request().get();
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        entity = response.readEntity(String.class);
+        
+        queryNode = (ObjectNode) JsonUtils.readTree(entity);
+        queryNode.remove("meta");
+        queryNode.remove("matches");
+        int queryHashCode2 = queryNode.hashCode();
+        int queryStringHashCode2 = queryNode.toString().hashCode();
+        
+        assertEquals(queryHashCode1, queryHashCode2);
+        assertNotEquals(queryStringHashCode1, queryStringHashCode2);
+    }
 }
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..d282c17
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/dao/DaoTestBase.java
@@ -0,0 +1,53 @@
+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.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",null);
+        userGroupService.addGroupMember("marlin", group, "dory",null);
+        return group;
+    }
+    
+    protected void deleteUserGroup (int groupId, String username)
+            throws KustvaktException {
+        userGroupDao.deleteGroup(groupId, username);
+        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..f747a89 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();
-        assertEquals(4, members.size());
+        List<UserGroupMember> members = userGroupDao
+                .retrieveGroupById(group.getId(), true).getMembers();
+        assertEquals(3, 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/misc/ScopesTest.java b/src/test/java/de/ids_mannheim/korap/misc/ScopesTest.java
deleted file mode 100644
index ad92c36..0000000
--- a/src/test/java/de/ids_mannheim/korap/misc/ScopesTest.java
+++ /dev/null
@@ -1,16 +0,0 @@
-package de.ids_mannheim.korap.misc;
-
-import org.junit.jupiter.api.Test;
-
-/**
- * @author hanl
- * @date 20/01/2016
- */
-public class ScopesTest {
-
-    @Test
-    public void testScopes () {}
-
-    @Test
-    public void testOpenIDScopes () {}
-}
diff --git a/src/test/java/de/ids_mannheim/korap/misc/SerializationTest.java b/src/test/java/de/ids_mannheim/korap/misc/SerializationTest.java
deleted file mode 100644
index dd4ca5b..0000000
--- a/src/test/java/de/ids_mannheim/korap/misc/SerializationTest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package de.ids_mannheim.korap.misc;
-
-import org.junit.jupiter.api.Disabled;
-import org.junit.jupiter.api.Test;
-
-/**
- * @author hanl
- * @date 21/01/2016
- */
-@Disabled
-public class SerializationTest {
-
-    @Test
-    public void testSettingsObject () {
-        // String t = "poliqarp_test";
-        // UserSettings s = new UserSettings();
-        // Map map = s.toObjectMap();
-        // map.put(Attributes.QUERY_LANGUAGE, t);
-        // s.updateObjectSettings(map);
-        // 
-        // assert s.getQueryLanguage().equals(t);
-    }
-}
diff --git a/src/test/java/de/ids_mannheim/korap/misc/ServiceSuite.java b/src/test/java/de/ids_mannheim/korap/misc/ServiceSuite.java
deleted file mode 100644
index ac2c8a1..0000000
--- a/src/test/java/de/ids_mannheim/korap/misc/ServiceSuite.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.ids_mannheim.korap.misc;
-
-/**
- * @author hanl
- * @date 29/02/2016
- */
-public class ServiceSuite {}
diff --git a/src/test/java/de/ids_mannheim/korap/misc/TestNullableSqls.java b/src/test/java/de/ids_mannheim/korap/misc/TestNullableSqls.java
deleted file mode 100644
index 8fcb1d0..0000000
--- a/src/test/java/de/ids_mannheim/korap/misc/TestNullableSqls.java
+++ /dev/null
@@ -1,7 +0,0 @@
-package de.ids_mannheim.korap.misc;
-
-/**
- * @author hanl
- * @date 30/01/2016
- */
-public class TestNullableSqls {}
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..d537584 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
@@ -6,16 +6,9 @@
 
 import java.io.IOException;
 
-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.junit.jupiter.api.Test;
+
 import com.fasterxml.jackson.databind.JsonNode;
-import com.google.common.net.HttpHeaders;
-import com.nimbusds.oauth2.sdk.GrantType;
 
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
 import de.ids_mannheim.korap.config.Attributes;
@@ -24,8 +17,11 @@
 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.core.Response;
+import jakarta.ws.rs.core.Response.Status;
 
-public class OAuth2AccessTokenTest extends OAuth2TestBase {
+public class OAuth2AccessTokenTest extends UserGroupTestBase {
 
     private String userAuthHeader;
 
@@ -38,7 +34,7 @@
                 .createBasicAuthorizationHeaderValue(confidentialClientId,
                         clientSecret);
     }
-
+    
     @Test
     public void testScopeWithSuperClient () throws KustvaktException {
         Response response = requestTokenWithDoryPassword(superClientId,
@@ -46,6 +42,11 @@
         JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
         assertEquals(node.at("/scope").asText(), "all");
         String accessToken = node.at("/access_token").asText();
+        
+        createDoryGroup();
+        createMarlinGroup();
+        addMember(marlinGroupName, "dory", "marlin");
+        
         // test list user group
         response = target().path(API_VERSION).path("group").request()
                 .header(Attributes.AUTHORIZATION, "Bearer " + accessToken)
@@ -53,6 +54,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
@@ -65,6 +69,8 @@
                 confidentialClientId, clientSecret, code);
         JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
         String token = node.at("/access_token").asText();
+        String refreshToken = node.at("/refresh_token").asText();
+        
         assertTrue(node.at("/scope").asText()
                 .contains(OAuth2Scope.VC_INFO.toString()));
         // test list vc using the token
@@ -72,7 +78,12 @@
                 .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());
+        
+        revokeToken(token, confidentialClientId, clientSecret,
+                ACCESS_TOKEN_TYPE);
+        revokeToken(refreshToken, confidentialClientId, clientSecret,
+                REFRESH_TOKEN_TYPE);
     }
 
     @Test
@@ -84,9 +95,15 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
         String accessToken = node.at("/access_token").asText();
+        String refreshToken = node.at("/refresh_token").asText();
         testScopeNotAuthorized(accessToken);
         testScopeNotAuthorize2(accessToken);
         testSearchWithOAuth2Token(accessToken);
+        
+        revokeToken(accessToken, confidentialClientId, clientSecret,
+                ACCESS_TOKEN_TYPE);
+        revokeToken(refreshToken, confidentialClientId, clientSecret,
+                REFRESH_TOKEN_TYPE);
     }
 
     private void testScopeNotAuthorized (String accessToken)
@@ -140,17 +157,14 @@
         JsonNode node = requestTokenWithAuthorizationCodeAndHeader(
                 confidentialClientId, code, clientAuthHeader);
         String accessToken = node.at("/access_token").asText();
-        Form form = new Form();
-        form.param("token", accessToken);
-        form.param("client_id", confidentialClientId);
-        form.param("client_secret", "secret");
-        Response response = target().path(API_VERSION).path("oauth2")
-                .path("revoke").request()
-                .header(HttpHeaders.CONTENT_TYPE,
-                        ContentType.APPLICATION_FORM_URLENCODED)
-                .post(Entity.form(form));
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        String refreshToken = node.at("/refresh_token").asText();
+        
+        revokeToken(accessToken, confidentialClientId, clientSecret,
+                ACCESS_TOKEN_TYPE);
         testSearchWithRevokedAccessToken(accessToken);
+        
+        revokeToken(refreshToken, confidentialClientId, clientSecret,
+                REFRESH_TOKEN_TYPE);
     }
 
     @Test
@@ -161,7 +175,7 @@
                 publicClientId, "", code);
         JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
         String accessToken = node.at("/access_token").asText();
-        testRevokeTokenViaSuperClient(accessToken, userAuthHeader);
+        revokeTokenViaSuperClient(accessToken, userAuthHeader);
         testSearchWithRevokedAccessToken(accessToken);
     }
 
@@ -174,23 +188,18 @@
                 confidentialClientId, code, clientAuthHeader);
         String accessToken = node.at("/access_token").asText();
         String refreshToken = node.at("/refresh_token").asText();
-        Form form = new Form();
-        form.param("grant_type", GrantType.REFRESH_TOKEN.toString());
-        form.param("client_id", confidentialClientId);
-        form.param("client_secret", "secret");
-        form.param("refresh_token", refreshToken);
-        Response response = target().path(API_VERSION).path("oauth2")
-                .path("token").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(HttpHeaders.CONTENT_TYPE,
-                        ContentType.APPLICATION_FORM_URLENCODED)
-                .post(Entity.form(form));
-        String entity = response.readEntity(String.class);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        node = JsonUtils.readTree(entity);
-        assertNotNull(node.at("/access_token").asText());
-        assertTrue(!refreshToken.equals(node.at("/refresh_token").asText()));
+
+        node = requestTokenWithRefreshToken(confidentialClientId, clientSecret,
+                refreshToken);
+        String newAccessToken = node.at("/access_token").asText();
+        String newRefreshToken = node.at("/refresh_token").asText();
+        assertTrue(!refreshToken.equals(newRefreshToken));
+
         testSearchWithRevokedAccessToken(accessToken);
+        revokeToken(newAccessToken, confidentialClientId, clientSecret,
+                ACCESS_TOKEN_TYPE);
+        revokeToken(newRefreshToken, confidentialClientId, clientSecret,
+                REFRESH_TOKEN_TYPE);
     }
 
     @Test
@@ -201,6 +210,7 @@
         JsonNode node = requestTokenWithAuthorizationCodeAndHeader(
                 confidentialClientId, code, clientAuthHeader);
         String userAuthToken = node.at("/access_token").asText();
+        String refreshToken = node.at("/refresh_token").asText();
         Response response = requestAuthorizationCode("code",
                 confidentialClientId, "", "search", "",
                 "Bearer " + userAuthToken);
@@ -210,6 +220,11 @@
                 node.at("/errors/0/0").asInt());
         assertEquals(node.at("/errors/0/1").asText(),
                 "Scope authorize is not authorized");
+
+        revokeToken(userAuthToken, confidentialClientId, clientSecret,
+                ACCESS_TOKEN_TYPE);
+        revokeToken(refreshToken, confidentialClientId, clientSecret,
+                REFRESH_TOKEN_TYPE);
     }
 
     @Test
@@ -227,5 +242,7 @@
         String code = requestAuthorizationCode(superClientId,
                 "Bearer " + userAuthToken);
         assertNotNull(code);
+        
+        revokeTokenViaSuperClient(userAuthToken, userAuthHeader);
     }
 }
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AdminControllerTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AdminControllerTest.java
index fabc267..f5a8a61 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AdminControllerTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AdminControllerTest.java
@@ -114,7 +114,7 @@
         String entity = response.readEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
         String accessToken = node.at("/access_token").asText();
-        testRevokeToken(accessToken, publicClientId, null, ACCESS_TOKEN_TYPE);
+        revokeToken(accessToken, publicClientId, null, ACCESS_TOKEN_TYPE);
         int accessTokensAfter = accessDao.retrieveInvalidAccessTokens().size();
         assertEquals(accessTokensAfter, accessTokensBefore + 1);
         target().path(API_VERSION).path("admin").path("oauth2").path("token")
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AuthorizationPostTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AuthorizationPostTest.java
index 8372acc..9225220 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AuthorizationPostTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AuthorizationPostTest.java
@@ -78,10 +78,15 @@
                 confidentialClientId, clientSecret, code);
         String entity = response.readEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
-        assertNotNull(node.at("/access_token").asText());
-        assertNotNull(node.at("/refresh_token").asText());
+        String token = node.at("/access_token").asText();
+        String refreshToken = node.at("/refresh_token").asText();
         assertEquals(TokenType.BEARER.displayName(),
                 node.at("/token_type").asText());
         assertNotNull(node.at("/expires_in").asText());
+        
+        revokeToken(token, confidentialClientId, clientSecret,
+                ACCESS_TOKEN_TYPE);
+        revokeToken(refreshToken, confidentialClientId, clientSecret,
+                REFRESH_TOKEN_TYPE);
     }
 }
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AuthorizationTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AuthorizationTest.java
index 27903e7..e2dd072 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AuthorizationTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2AuthorizationTest.java
@@ -277,6 +277,21 @@
     }
 
     @Test
+    public void testAuthorizeScopeAll () throws KustvaktException {
+        String scope = "all";
+        Response response = requestAuthorizationCode("code",
+                confidentialClientId, "", scope, state, userAuthHeader);
+        assertEquals(Status.TEMPORARY_REDIRECT.getStatusCode(),
+                response.getStatus());
+
+        assertEquals(
+                "https://third.party.com/confidential/redirect?"
+                        + "error=invalid_scope&error_description=Requested+scope"
+                        + "+all+is+not+allowed.&state=thisIsMyState",
+                response.getLocation().toString());
+    }
+    
+    @Test
     public void testAuthorizeUnsupportedTokenResponseType ()
             throws KustvaktException {
         Response response = requestAuthorizationCode("token",
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
index 9cc459f..8790ed1 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ClientControllerTest.java
@@ -102,6 +102,7 @@
         assertFalse(clientId.contains("a"));
         testListConfidentialClient(username, clientId);
         testConfidentialClientInfo(clientId, username);
+//        testListAllUserClients(username);
         testResetConfidentialClientSecret(clientId, clientSecret);
         deregisterClient(username, clientId);
     }
@@ -292,7 +293,7 @@
         assertNotNull(clientId);
         assertTrue(node.at("/client_secret").isMissingNode());
         
-        node = listUserClients(username);
+        node = listUserClients(username,"owned_only");
         assertFalse(node.at("/0/client_redirect_uri").isMissingNode());
         assertFalse(node.at("/0/registration_date").isMissingNode());
 //        assertEquals(username,
@@ -508,7 +509,10 @@
     private void testListAuthorizedClients (String userAuthHeader)
             throws KustvaktException {
         Form form = getSuperClientForm();
-        form.param("authorized_only", "true");
+        // deprecated, use filter_by = authorized_only instead
+//        form.param("authorized_only", "true");
+        
+        form.param("filter_by", "authorized_only");
         Response response = target().path(API_VERSION).path("oauth2")
                 .path("client").path("list").request()
                 .header(Attributes.AUTHORIZATION, userAuthHeader)
@@ -535,7 +539,7 @@
         OAuth2ClientJson json = createOAuth2ClientJson(clientName,
                 OAuth2ClientType.PUBLIC, "Dory's client.");
         registerClient("dory", json);
-        JsonNode node = listUserClients("dory");
+        JsonNode node = listUserClients("dory","owned_only");
         assertEquals(1, node.size());
         assertEquals(clientName, node.at("/0/client_name").asText());
         assertEquals(OAuth2ClientType.PUBLIC.name(),
@@ -544,12 +548,16 @@
         assertFalse(node.at("/0/registration_date").isMissingNode());
         assertTrue(node.at("/refresh_token_expiry").isMissingNode());
         String clientId = node.at("/0/client_id").asText();
+        
+//        testListAllUserClients("dory");
         testDeregisterPublicClient(clientId, "dory");
     }
 
     private void testListConfidentialClient (String username, String clientId)
             throws ProcessingException, KustvaktException {
-        JsonNode node = listUserClients(username);
+        // means authorized_only = false
+        // this is deprecated, filter_by = owned_only should be use instead.
+        JsonNode node = listUserClients(username,"");
         assertEquals(1, node.size());
         assertEquals(clientId, node.at("/0/client_id").asText());
         assertEquals(node.at("/0/client_name").asText(), "OAuth2ClientTest");
@@ -566,8 +574,27 @@
         assertTrue(node.at("/0/source").isMissingNode());
     }
 
+    // not ready until the behavior for (filterBy=null || filterBy.isEmpty) is set
+    private void testListAllUserClients (String username) throws KustvaktException {
+        // authorize
+        String userAuthHeader = HttpAuthorizationHandler
+                .createBasicAuthorizationHeaderValue(username, "password");
+        String code = requestAuthorizationCode(confidentialClientId, userAuthHeader);
+        Response response = requestTokenWithAuthorizationCodeAndForm(
+                confidentialClientId, this.clientSecret, code);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
+        String accessToken = node.at("/access_token").asText();
+        
+        node = listUserClients(username,null);
+        assertEquals(2, node.size());
+        
+        testRevokeAllTokenViaSuperClient(confidentialClientId, userAuthHeader,
+                accessToken);
+    }
+    
     @Test
-    public void testListUserClients () throws KustvaktException {
+    public void testListAuthorizedUserClients () throws KustvaktException {
         String username = "pearl";
         String password = "pwd";
         userAuthHeader = HttpAuthorizationHandler
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
index ab2eb27..dfbb987 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2ControllerTest.java
@@ -57,7 +57,7 @@
         assertEquals(TokenType.BEARER.displayName(),
                 node.at("/token_type").asText());
         assertEquals(31536000, node.at("/expires_in").asInt());
-        testRevokeToken(accessToken, publicClientId, null, ACCESS_TOKEN_TYPE);
+        revokeToken(accessToken, publicClientId, null, ACCESS_TOKEN_TYPE);
         assertTrue(node.at("/refresh_token").isMissingNode());
     }
 
@@ -487,7 +487,7 @@
         assertTrue(!newRefreshToken.equals(refreshToken));
         testRequestTokenWithRevokedRefreshToken(clientId, clientSecret,
                 refreshToken);
-        testRevokeToken(newRefreshToken, clientId, clientSecret,
+        revokeToken(newRefreshToken, clientId, clientSecret,
                 REFRESH_TOKEN_TYPE);
         testRequestTokenWithRevokedRefreshToken(clientId, clientSecret,
                 newRefreshToken);
@@ -654,24 +654,24 @@
         node = requestTokenList(userAuthHeader, REFRESH_TOKEN_TYPE,
                 confidentialClientId);
         assertEquals(2, node.size());
-        testRevokeToken(refreshToken1, superClientId, clientSecret,
+        revokeToken(refreshToken1, superClientId, clientSecret,
                 REFRESH_TOKEN_TYPE);
-        testRevokeToken(node.at("/0/token").asText(), confidentialClientId,
+        revokeToken(node.at("/0/token").asText(), confidentialClientId,
                 clientSecret, REFRESH_TOKEN_TYPE);
-        testRevokeToken(node.at("/1/token").asText(), confidentialClientId2,
+        revokeToken(node.at("/1/token").asText(), confidentialClientId2,
                 clientSecret, REFRESH_TOKEN_TYPE);
         node = requestTokenList(userAuthHeader, REFRESH_TOKEN_TYPE);
         assertEquals(1, node.size());
-        testRevokeTokenViaSuperClient(node.at("/0/token").asText(),
+        revokeTokenViaSuperClient(node.at("/0/token").asText(),
                 userAuthHeader);
         node = requestTokenList(userAuthHeader, REFRESH_TOKEN_TYPE);
         assertEquals(0, node.size());
         // try revoking a token belonging to another user
         // should not return any errors
-        testRevokeTokenViaSuperClient(refreshToken5, userAuthHeader);
+        revokeTokenViaSuperClient(refreshToken5, userAuthHeader);
         node = requestTokenList(darlaAuthHeader, REFRESH_TOKEN_TYPE);
         assertEquals(1, node.size());
-        testRevokeTokenViaSuperClient(refreshToken5, darlaAuthHeader);
+        revokeTokenViaSuperClient(refreshToken5, darlaAuthHeader);
         node = requestTokenList(darlaAuthHeader, REFRESH_TOKEN_TYPE);
         assertEquals(0, node.size());
     }
@@ -702,7 +702,7 @@
         // list refresh tokens
         node = requestTokenList(userAuthHeader, REFRESH_TOKEN_TYPE);
         assertEquals(0, node.size());
-        testRevokeTokenViaSuperClient(accessToken1, userAuthHeader);
+        revokeTokenViaSuperClient(accessToken1, userAuthHeader);
         node = requestTokenList(userAuthHeader, ACCESS_TOKEN_TYPE);
         // System.out.println(node);
         assertEquals(1, node.size());
@@ -715,7 +715,7 @@
         assertNotNull(node.at("/0/client_name").asText());
         assertNotNull(node.at("/0/client_description").asText());
         assertNotNull(node.at("/0/client_url").asText());
-        testRevokeTokenViaSuperClient(accessToken2, userAuthHeader);
+        revokeTokenViaSuperClient(accessToken2, userAuthHeader);
         node = requestTokenList(userAuthHeader, ACCESS_TOKEN_TYPE);
         assertEquals(0, node.size());
     }
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
index a8c2139..e150d5d 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2PluginTest.java
@@ -113,7 +113,7 @@
     private void testListUserRegisteredPlugins (String username,
             String clientId, String clientName, int refreshTokenExpiry)
             throws ProcessingException, KustvaktException {
-        JsonNode node = listUserClients(username);
+        JsonNode node = listUserClients(username, "owned_only");
         assertEquals(1, node.size());
         assertEquals(clientId, node.at("/0/client_id").asText());
         assertEquals(clientName, node.at("/0/client_name").asText());
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
index ba7d57d..062a04e 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/OAuth2TestBase.java
@@ -60,7 +60,7 @@
 public abstract class OAuth2TestBase extends SpringJerseyTest {
 
     @Autowired
-    private AccessTokenDao tokenDao;
+    protected AccessTokenDao tokenDao;
     @Autowired
     private OAuth2ClientDao clientDao;
     @Autowired
@@ -255,6 +255,24 @@
         return requestToken(form);
     }
 
+    protected JsonNode requestTokenWithRefreshToken (String clientId,
+            String clientSecret, String refreshToken) throws KustvaktException {
+        Form form = new Form();
+        form.param("grant_type", GrantType.REFRESH_TOKEN.toString());
+        form.param("client_id", clientId);
+        form.param("client_secret", clientSecret);
+        form.param("refresh_token", refreshToken);
+        Response response = target().path(API_VERSION).path("oauth2")
+                .path("token").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .post(Entity.form(form));
+        String entity = response.readEntity(String.class);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        return JsonUtils.readTree(entity);
+    }
+    
     protected void testRequestTokenWithRevokedRefreshToken (String clientId,
             String clientSecret, String refreshToken) throws KustvaktException {
         Form form = new Form();
@@ -280,6 +298,44 @@
         assertEquals("Refresh token has been revoked",
                 node.at("/error_description").asText());
     }
+    
+    protected void revokeTokenViaSuperClient (String token,
+            String userAuthHeader) {
+        Form form = new Form();
+        form.param("token", token);
+        form.param("super_client_id", superClientId);
+        form.param("super_client_secret", clientSecret);
+
+        Response response = target().path(API_VERSION).path("oauth2")
+                .path("revoke").path("super").request()
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .header(Attributes.AUTHORIZATION, userAuthHeader)
+                .post(Entity.form(form));
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        assertEquals("SUCCESS", response.readEntity(String.class));
+    }
+
+    protected void revokeToken (String token, String clientId,
+            String clientSecret, String tokenType) {
+        Form form = new Form();
+        form.param("token_type", tokenType);
+        form.param("token", token);
+        form.param("client_id", clientId);
+        if (clientSecret != null) {
+            form.param("client_secret", clientSecret);
+        }
+
+        Response response = target().path(API_VERSION).path("oauth2")
+                .path("revoke").request()
+                .header(HttpHeaders.CONTENT_TYPE,
+                        ContentType.APPLICATION_FORM_URLENCODED)
+                .post(Entity.form(form));
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        assertEquals("SUCCESS", response.readEntity(String.class));
+    }
 
     protected Response registerClient (String username, OAuth2ClientJson json)
             throws ProcessingException, KustvaktException {
@@ -392,55 +448,21 @@
                 node.at("/errors/0/1").asText());
     }
 
-    protected void testRevokeTokenViaSuperClient (String token,
-            String userAuthHeader) {
-        Form form = new Form();
-        form.param("token", token);
-        form.param("super_client_id", superClientId);
-        form.param("super_client_secret", clientSecret);
-
-        Response response = target().path(API_VERSION).path("oauth2")
-                .path("revoke").path("super").request()
-                .header(HttpHeaders.CONTENT_TYPE,
-                        ContentType.APPLICATION_FORM_URLENCODED)
-                .header(Attributes.AUTHORIZATION, userAuthHeader)
-                .post(Entity.form(form));
-
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        assertEquals("SUCCESS", response.readEntity(String.class));
-    }
-
-    protected void testRevokeToken (String token, String clientId,
-            String clientSecret, String tokenType) {
-        Form form = new Form();
-        form.param("token_type", tokenType);
-        form.param("token", token);
-        form.param("client_id", clientId);
-        if (clientSecret != null) {
-            form.param("client_secret", clientSecret);
-        }
-
-        Response response = target().path(API_VERSION).path("oauth2")
-                .path("revoke").request()
-                .header(HttpHeaders.CONTENT_TYPE,
-                        ContentType.APPLICATION_FORM_URLENCODED)
-                .post(Entity.form(form));
-
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        assertEquals("SUCCESS", response.readEntity(String.class));
-    }
-
-    protected JsonNode listUserClients (String username)
+    protected JsonNode listUserClients (String username, String filterBy)
             throws ProcessingException, KustvaktException {
         Form form = getSuperClientForm();
+        
+        if (filterBy != null) {
+            form.param("filter_by", filterBy);
+        }
         Response response = target().path(API_VERSION).path("oauth2")
-                .path("client").path("list").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pwd"))
-                .header(HttpHeaders.CONTENT_TYPE,
-                        ContentType.APPLICATION_FORM_URLENCODED)
-                .post(Entity.form(form));
-
+            .path("client").path("list").request()
+            .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                    .createBasicAuthorizationHeaderValue(username, "pwd"))
+            .header(HttpHeaders.CONTENT_TYPE,
+                    ContentType.APPLICATION_FORM_URLENCODED)
+            .post(Entity.form(form));
+        
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         String entity = response.readEntity(String.class);
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/UserControllerTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/UserControllerTest.java
index 27c8512..3a0c7c4 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/UserControllerTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/UserControllerTest.java
@@ -2,22 +2,18 @@
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
-import java.net.URI;
-
-import jakarta.ws.rs.ProcessingException;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.core.Response.Status;
-
 import org.junit.jupiter.api.Test;
-import org.springframework.util.MultiValueMap;
-import org.springframework.web.util.UriComponentsBuilder;
+
 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.exceptions.KustvaktException;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2ClientType;
 import de.ids_mannheim.korap.utils.JsonUtils;
 import de.ids_mannheim.korap.web.input.OAuth2ClientJson;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
 
 public class UserControllerTest extends OAuth2TestBase {
 
@@ -40,7 +36,7 @@
     }
 
     private String registerClient ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         OAuth2ClientJson clientJson = createOAuth2Client();
         Response response = registerClient(username, clientJson);
         JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
@@ -63,7 +59,7 @@
     }
 
     @Test
-    public void getUsername () throws ProcessingException, KustvaktException {
+    public void getUsername () throws KustvaktException {
         String clientId = registerClient();
         String accessToken = requestOAuth2AccessToken(clientId);
         Response response = target().path(API_VERSION).path("user").path("info")
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/VirtualCorpusListTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusListTest.java
deleted file mode 100644
index f1e1d17..0000000
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusListTest.java
+++ /dev/null
@@ -1,93 +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.junit.jupiter.api.Disabled;
-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.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
-import de.ids_mannheim.korap.utils.JsonUtils;
-
-public class VirtualCorpusListTest extends VirtualCorpusTestBase {
-
-    @Test
-    public void testListVCNemo ()
-            throws ProcessingException, KustvaktException {
-        JsonNode node = testListOwnerVC("nemo");
-        assertEquals(1, node.size());
-        node = listSystemVC("nemo");
-        assertEquals(1, node.size());
-        node = listVC("nemo");
-        assertEquals(3, node.size());
-    }
-
-    @Test
-    public void testListVCPearl ()
-            throws ProcessingException, KustvaktException {
-        JsonNode node = testListOwnerVC("pearl");
-        assertEquals(0, node.size());
-        node = listVC("pearl");
-        assertEquals(2, node.size());
-    }
-
-    @Test
-    public void testListVCDory ()
-            throws ProcessingException, KustvaktException {
-        JsonNode node = testListOwnerVC("dory");
-        assertEquals(2, node.size());
-        node = listVC("dory");
-        assertEquals(4, node.size());
-    }
-
-    @Test
-    public void testListAvailableVCGuest ()
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("vc").request()
-                .get();
-        testResponseUnauthorized(response, "guest");
-    }
-
-    @Disabled
-    @Deprecated
-    @Test
-    public void testListAvailableVCByOtherUser ()
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("vc").path("~dory")
-                .request().header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("pearl", "pass"))
-                .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: pearl");
-        checkWWWAuthenticateHeader(response);
-    }
-
-    @Disabled
-    @Deprecated
-    @Test
-    public void testListUserVC ()
-            throws ProcessingException, KustvaktException {
-        Response response = target().path(API_VERSION).path("vc")
-                .queryParam("username", "dory").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("admin", "pass"))
-                .get();
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(4, node.size());
-    }
-}
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/UserGroupControllerAdminTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerAdminTest.java
similarity index 61%
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..13fa68a 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,39 +1,34 @@
-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.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";
 
     private JsonNode listGroup (String username)
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("group").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(testUser, "pass"))
@@ -46,6 +41,17 @@
 
     @Test
     public void testListUserGroupsUsingAdminToken () throws KustvaktException {
+        createDoryGroup();
+        
+        createMarlinGroup();
+        addMember(marlinGroupName, "dory", "marlin");
+        
+        String testGroup = "test-group"; 
+        createUserGroup("test-group", "Test group to be deleted.", "marlin");
+        addMember(testGroup, "dory", "marlin");
+        deleteGroupByName("test-group", "marlin");
+
+        
         Form f = new Form();
         f.param("username", "dory");
         f.param("token", "secret");
@@ -57,7 +63,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 +111,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());
     }
@@ -125,12 +135,12 @@
     // same as list user-groups of the admin
     @Test
     public void testListWithoutUsername ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("group").request()
                 .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);
@@ -139,13 +149,13 @@
 
     @Test
     public void testListByStatusAll ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("admin")
                 .path("group").path("list").request()
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "pass"))
+                                        admin, "pass"))
                 .header(HttpHeaders.CONTENT_TYPE,
                         MediaType.APPLICATION_FORM_URLENCODED)
                 .post(null);
@@ -162,111 +172,64 @@
     }
 
     @Test
-    public void testListByStatusHidden ()
-            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);
+    public void testListHiddenGroups ()
+            throws KustvaktException {
+        JsonNode node = listHiddenGroup();
         assertEquals(1, node.size());
-        assertEquals(3, node.at("/0/id").asInt());
     }
 
     @Test
     public void testUserGroupAdmin ()
-            throws ProcessingException, KustvaktException {
+            throws 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);
         assertEquals(1, node.size());
         node = node.get(0);
         assertEquals(groupName, node.get("name").asText());
-        testInviteMember(groupName);
-        testMemberRole("marlin", groupName);
+        testAddMember(groupName);
+        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)
-            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));
+    private void testAddAdminRole (String groupName, String memberUsername)
+            throws KustvaktException {
+        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;
             }
         }
     }
 
     private void testDeleteMemberRoles (String groupName, String memberUsername)
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         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,20 +237,20 @@
         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;
             }
         }
     }
 
     private JsonNode retrieveGroup (String groupName)
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("admin")
                 .path("group").path("@" + groupName).request()
                 .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,31 +258,15 @@
         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 {
+            throws KustvaktException {
         // delete marlin from group
         Response response = target().path(API_VERSION).path("group")
                 .path("@" + groupName).path("~marlin").request()
                 .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
@@ -327,30 +274,25 @@
         node = node.get(0);
         assertEquals(3, node.get("members").size());
         assertEquals(node.at("/members/1/userId").asText(), "nemo");
-        assertEquals(GroupMemberStatus.PENDING.name(),
-                node.at("/members/1/status").asText());
     }
 
-    private void testInviteMember (String groupName)
-            throws ProcessingException, KustvaktException {
+    private void testAddMember (String groupName)
+            throws KustvaktException {
         Form form = new Form();
         form.param("members", "marlin,nemo,darla");
         Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("invite").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .path("@" + groupName).path("member").request()
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
-                                        sysAdminUser, "pass"))
-                .post(Entity.form(form));
+                                        admin, "pass"))
+                .put(Entity.form(form));
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         // list group
         JsonNode node = listGroup(testUser);
         node = node.get(0);
         assertEquals(4, node.get("members").size());
         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(1, 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..292c8e0
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerTest.java
@@ -0,0 +1,182 @@
+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.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;
+
+/**
+ * @author margaretha
+ */
+public class UserGroupControllerTest extends UserGroupTestBase {
+
+    private String username = "UserGroupControllerTest";
+
+    @Test
+    public void testCreateGroupEmptyDescription ()
+            throws KustvaktException {
+        String groupName = "empty_group";
+        Response response = createUserGroup(groupName, "", username);
+        assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
+        deleteGroupByName(groupName,username);
+    }
+
+    @Test
+    public void testCreateGroupMissingDescription ()
+            throws 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 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 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 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 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(5,  node.at("/members/0/privileges").size());
+
+        testUpdateUserGroup(groupName);
+        testAddMember(groupName, username, "darla");
+        testDeleteGroup(groupName,username);
+    }
+    
+    private void testUpdateUserGroup (String groupName)
+            throws 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 testDeleteGroup (String groupName, String username)
+            throws KustvaktException {
+        deleteGroupByName(groupName, username);
+        JsonNode node = listUserGroups(username);
+        assertEquals(0, node.size());
+    }
+
+    @Test
+    public void testDeleteGroupUnauthorized ()
+            throws KustvaktException {
+        createMarlinGroup();
+        addMember(marlinGroupName, "dory", "marlin");
+        
+        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 KustvaktException {
+        createMarlinGroup();
+        deleteGroupByName(marlinGroupName, "marlin");
+        Response response = deleteGroupByName(marlinGroupName, "marlin");
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+    }
+
+    @Test
+    public void testDeleteGroupOwner ()
+            throws 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..b96467a
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupListTest.java
@@ -0,0 +1,66 @@
+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();
+        addMember(doryGroupName, "marlin", "dory");
+        addMember(doryGroupName, "nemo", "dory");
+        
+        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 {
+        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();
+        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..10ce9ba
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupMemberTest.java
@@ -0,0 +1,368 @@
+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.Disabled;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+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.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.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 testAddMultipleMembers ()
+            throws KustvaktException {
+        createDoryGroup();
+        addMember(doryGroupName, "nemo,marlin,pearl", "dory");
+        
+        JsonNode node = listUserGroups("dory");
+        node = node.get(0);
+        assertEquals(4, node.get("members").size());
+        
+        testAddExistingMember();
+        
+        deleteGroupByName(doryGroupName, "dory");
+        
+        testAddMemberToDeletedGroup();
+    }
+    
+    private void testAddExistingMember () throws KustvaktException {
+        Response response = addMember(doryGroupName, "nemo", "dory");
+        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 exists in the user-group: "
+                        + "dory-group",
+                node.at("/errors/0/1").asText());
+        assertEquals(node.at("/errors/0/2").asText(),
+                "[nemo, dory-group]");
+    }
+    
+    private void testAddMemberToDeletedGroup () throws KustvaktException {
+        Response response = addMember(doryGroupName, "pearl", "dory");
+        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("Group dory-group is not found",
+                node.at("/errors/0/1").asText());
+    }
+    
+    @Test
+    public void testAddMemberMissingGroupName () throws KustvaktException {
+        Response response = addMember("", "pearl","dory");
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+    }
+
+
+    @Test
+    public void testAddMemberNonExistentGroup () throws KustvaktException {
+        Response response = addMember("non-existent", "pearl","dory");
+        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("Group non-existent is not found",
+                node.at("/errors/0/1").asText());
+    }
+    
+ // if username is not found in LDAP
+    @Disabled
+    @Test
+    public void testMemberAddNonExistent () throws KustvaktException {
+        createDoryGroup();
+        
+        Response response = addMember(doryGroupName, "bruce", "dory");
+        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("bruce is not found in the group",
+                node.at("/errors/0/1").asText());
+        
+        testAddDeletedMember();
+        deleteGroupByName(doryGroupName, "dory");
+    }
+    
+    @Test
+    public void testAddDeletedMember () throws KustvaktException {
+        createDoryGroup();
+        addMember(doryGroupName, "pearl", "dory");
+        deleteMember(doryGroupName, "pearl", "dory");
+        
+        Response response = addMember(doryGroupName, "pearl", "dory");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = listUserGroups("pearl");
+        assertEquals(1, node.size());
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+
+    @Test
+    public void testDeleteMemberByGroupOwner ()
+            throws KustvaktException {
+        createDoryGroup();
+        addMember(doryGroupName, "pearl", "dory");
+        addMember(doryGroupName, "marlin", "dory");
+
+        testDeleteMemberUnauthorizedByNonMember(doryGroupName, "pearl", "nemo");
+        testDeleteMemberUnauthorizedByMember(doryGroupName, "pearl", "marlin");
+        deleteMember(doryGroupName, "pearl", "dory");
+
+        // check group member
+        JsonNode node = listUserGroups("dory");
+        node = node.get(0);
+        assertEquals(2, node.get("members").size());
+
+        deleteGroupByName(doryGroupName, "dory");
+    }
+    
+    private void testDeleteMemberUnauthorizedByNonMember (String groupName,
+            String memberName, String deletedBy)
+            throws KustvaktException {
+        Response response = deleteMember(groupName, memberName, deletedBy);
+        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: "+deletedBy);
+    }
+    
+    private void testDeleteMemberUnauthorizedByMember (String groupName,
+            String memberName, String deletedBy) 
+                    throws KustvaktException {
+        Response response = deleteMember(groupName, memberName, deletedBy);
+        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: "+deletedBy);
+    }
+
+    @Test
+    public void testDeleteMemberByGroupAdmin ()
+            throws KustvaktException {
+        createDoryGroup();
+        addMember(doryGroupName, "pearl", "dory");
+        addMember(doryGroupName, "nemo", "dory");
+        addAdminRole(doryGroupName, "nemo", "dory");
+
+        // check group member
+        JsonNode node = listUserGroups("dory");
+        node = node.get(0);
+        assertEquals(3, node.get("members").size());
+        
+        Response response = deleteMember(doryGroupName, "pearl", "nemo");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        // check group member
+        node = listUserGroups("dory");
+        node = node.get(0);
+        assertEquals(2, node.get("members").size());
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+    
+    @Test
+    public void testDeleteMemberBySelf ()
+            throws KustvaktException {
+        createDoryGroup();
+        addMember(doryGroupName, "pearl", "dory");
+
+        // check group member
+        JsonNode node = listUserGroups("dory");
+        node = node.get(0);
+        assertEquals(2, node.get("members").size());
+        
+        Response response = deleteMember(doryGroupName, "pearl", "pearl");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        // check group member
+        node = listUserGroups("dory");
+        node = node.get(0);
+        assertEquals(1, node.get("members").size());
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+    
+    @Test
+    public void testDeleteMemberDeletedGroup ()
+            throws KustvaktException {
+        createDoryGroup();
+        addMember(doryGroupName, "pearl", "dory");
+        deleteGroupByName(doryGroupName, "dory");
+        
+        Response response = deleteMember(doryGroupName, "pearl", "dory");
+        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("Group "+doryGroupName+" is not found",
+                node.at("/errors/0/1").asText());
+    }
+
+    @Test
+    public void testDeleteMemberAlreadyDeleted ()
+            throws KustvaktException {
+        createDoryGroup();
+        addMember(doryGroupName, "pearl", "dory");
+        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_NOT_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals("pearl is not found in the group",
+                node.at("/errors/0/1").asText());
+        assertEquals("pearl",node.at("/errors/0/2").asText());
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+    
+    @Test
+    public void testDeleteMemberMissingGroupName () throws KustvaktException {
+        Response response = deleteMember("", "pearl","dory");
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+    }
+    
+    @Test
+    public void testDeleteMemberNonExistent () throws KustvaktException {
+        createDoryGroup();
+        Response response = deleteMember(doryGroupName, "pearl", "dory");
+        String entity = response.readEntity(String.class);
+        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("pearl is not found in the group",
+                node.at("/errors/0/1").asText());
+        assertEquals("pearl",node.at("/errors/0/2").asText());
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+    
+    @Test
+    public void testDeleteMemberNonExistentGroup () throws KustvaktException {
+        Response response = deleteMember("non-existent", "pearl","dory");
+        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("Group non-existent is not found",
+                node.at("/errors/0/1").asText());
+    }
+    
+//    @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();
+        addMember(marlinGroupName, "dory", "marlin");
+        
+        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 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 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/UserGroupTestBase.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupTestBase.java
new file mode 100644
index 0000000..630ee66
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupTestBase.java
@@ -0,0 +1,171 @@
+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.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.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 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 addMember (String groupName, String memberUsername,
+            String username) throws KustvaktException {
+        Form form = new Form();
+        form.param("members", memberUsername);
+        Response response = target().path(API_VERSION).path("group")
+                .path("@" + groupName).path("member").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .put(Entity.form(form));
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        return response;
+    }
+    
+    protected void testAddMember (String groupName, String username,
+            String memberUsername)
+            throws KustvaktException {
+        Response response = addMember(groupName, memberUsername, username);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        // list group
+        JsonNode node = listUserGroups(username);
+        node = node.get(0);
+        assertEquals(2, node.get("members").size());
+        assertEquals(node.at("/members/1/userId").asText(), memberUsername);
+        assertEquals(1, node.at("/members/1/privileges").size());
+    }
+
+    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 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/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 79%
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..6678ca1 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,7 +1,18 @@
-package de.ids_mannheim.korap.web.controller;
+package de.ids_mannheim.korap.web.controller.vc;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
 
+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.ResourceType;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.utils.JsonUtils;
 import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Entity;
 import jakarta.ws.rs.core.Form;
@@ -9,16 +20,6 @@
 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.ResourceType;
-import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.utils.JsonUtils;
-
 /**
  * @author margaretha
  */
@@ -161,34 +162,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());
+        
         node2 = testAdminListVC_UsingAdminToken(vcCreator,
                 ResourceType.PROJECT);
         assertEquals(1, node2.size());
+        
+        // delete role
+        deleteRoleByGroupAndQuery(vcCreator, vcName, groupName, "admin");
+        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 +195,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..b4c44d8 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\"}";
@@ -409,6 +402,20 @@
         assertEquals(node.at("/collection/@type").asText(), "koral:doc");
         assertEquals(node.at("/collection/key").asText(), "corpusSigle");
         assertEquals(node.at("/collection/value").asText(), "WPD17");
+        
+        json = "{\"corpusQuery\": \"corpusSigle=GOE AND creationDate since "
+                + "1820\"}";
+        editVC("dory", "dory", "dory-vc", json);
+        node = testRetrieveKoralQuery("dory", "dory-vc");
+        assertEquals(node.at("/collection/@type").asText(), "koral:docGroup");
+        assertEquals(node.at("/collection/operation").asText(),
+                "operation:and");
+        assertEquals(node.at("/collection/operands/0/key").asText(),
+                "corpusSigle");
+        assertEquals(node.at("/collection/operands/0/value").asText(), "GOE");
+        assertEquals(node.at("/collection/operands/1/key").asText(),
+                "creationDate");
+        assertEquals(node.at("/collection/operands/1/value").asText(), "1820");
     }
 
     private JsonNode testRetrieveKoralQuery (String username, String vcName)
@@ -443,37 +450,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 65%
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..552026f 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 org.mozilla.javascript.Node;
+
 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.core.Response;
+import jakarta.ws.rs.core.Response.Status;
 
 public class VirtualCorpusInfoTest extends VirtualCorpusTestBase {
 
@@ -27,7 +24,7 @@
 
     @Test
     public void testRetrieveSystemVC ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         JsonNode node = retrieveVCInfo(testUser, "system", "system-vc");
         assertEquals(node.at("/name").asText(), "system-vc");
         assertEquals(ResourceType.SYSTEM.displayName(),
@@ -35,11 +32,20 @@
         // assertEquals("koral:doc", node.at("/koralQuery/collection/@type").asText());
         assertTrue(node.at("/query").isMissingNode());
         assertTrue(node.at("/queryLanguage").isMissingNode());
+        
+        testStatistics(node);
     }
 
+    private void testStatistics (JsonNode node) {
+        assertTrue(node.at("/numberOfDoc").asInt()>0);
+        assertTrue(node.at("/numberOfParagraphs").asInt()>0);
+        assertTrue(node.at("/numberOfSentences").asInt()>0);
+        assertTrue(node.at("/numberOfTokens").asInt()>0);
+    }
+    
     @Test
-    public void testRetrieveSystemVCGuest ()
-            throws ProcessingException, KustvaktException {
+    public void testRetrieveSystemVC_guest ()
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc")
                 .path("~system").path("system-vc").request().get();
         JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
@@ -49,17 +55,19 @@
     }
 
     @Test
-    public void testRetrieveOwnerPrivateVC ()
-            throws ProcessingException, KustvaktException {
+    public void testRetrievePrivateVC ()
+            throws KustvaktException {
         JsonNode node = retrieveVCInfo("dory", "dory", "dory-vc");
         assertEquals(node.at("/name").asText(), "dory-vc");
         assertEquals(ResourceType.PRIVATE.displayName(),
                 node.at("/type").asText());
+        
+        testStatistics(node);
     }
 
     @Test
-    public void testRetrievePrivateVCUnauthorized ()
-            throws ProcessingException, KustvaktException {
+    public void testRetrievePrivateVC_unauthorized ()
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("dory-vc").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
@@ -69,17 +77,31 @@
     }
 
     @Test
-    public void testRetrieveProjectVC ()
-            throws ProcessingException, KustvaktException {
+    public void testRetrieveProjectVC_member ()
+            throws KustvaktException {
+        createDoryGroup();
+        addMember(doryGroupName, "nemo", "dory");
+        
+        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());
+        
+        addMember(doryGroupName, "pearl", "dory");
+        
+        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 ()
-            throws ProcessingException, KustvaktException {
+    public void testRetrieveProjectVC_unauthorized ()
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("group-vc").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
@@ -89,8 +111,8 @@
     }
 
     @Test
-    public void testRetrieveProjectVCbyNonActiveMember ()
-            throws ProcessingException, KustvaktException {
+    public void testRetrieveProjectVC_nonActiveMember ()
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("group-vc").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
@@ -100,33 +122,8 @@
     }
 
     @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 ()
-            throws ProcessingException, KustvaktException {
+    public void testRetrievePrivateVC_admin ()
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("dory-vc").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
@@ -140,8 +137,8 @@
     }
 
     @Test
-    public void testAdminRetrieveProjectVC ()
-            throws ProcessingException, KustvaktException {
+    public void testRetrieveProjectVC_admin ()
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("group-vc").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusListTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusListTest.java
new file mode 100644
index 0000000..e33da75
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusListTest.java
@@ -0,0 +1,67 @@
+package de.ids_mannheim.korap.web.controller.vc;
+
+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.junit.jupiter.api.Disabled;
+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.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+
+public class VirtualCorpusListTest extends VirtualCorpusTestBase {
+
+    @Test
+    public void testListVCNemo ()
+            throws ProcessingException, KustvaktException {
+        JsonNode node = testListOwnerVC("nemo");
+        assertEquals(1, node.size());
+        node = listSystemVC("nemo");
+        assertEquals(1, node.size());
+        node = listVC("nemo");
+        assertEquals(2, node.size());
+    }
+
+    @Test
+    public void testListVCPearl ()
+            throws ProcessingException, KustvaktException {
+        JsonNode node = testListOwnerVC("pearl");
+        assertEquals(0, node.size());
+        node = listVC("pearl");
+        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(3, node.size());
+    }
+
+    @Test
+    public void testListAvailableVCGuest ()
+            throws ProcessingException, KustvaktException {
+        Response response = target().path(API_VERSION).path("vc").request()
+                .get();
+        testResponseUnauthorized(response, "guest");
+    }
+}
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..6fad6d7
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusPublishedTest.java
@@ -0,0 +1,212 @@
+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.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 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(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();
+        addMember(marlinGroupName, "dory", "marlin");
+
+        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());
+
+        addMember(marlinGroupName, "nemo", "marlin");
+
+        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..6b908e9 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,11 @@
 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.UserGroupStatus;
 import de.ids_mannheim.korap.dao.QueryDao;
 import de.ids_mannheim.korap.entity.QueryDO;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
@@ -24,13 +23,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 +198,25 @@
 
     @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());
+        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..5a1b900
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusSharingTest.java
@@ -0,0 +1,318 @@
+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.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();
+        addMember(marlinGroupName, "nemo", "marlin");
+        
+        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";
+        testAddMember(groupName, testUser, 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());
+
+        testAddMember(groupName, testUser, 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";
+        testAddMember(groupName, testUser, 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(PrivilegeType.DELETE_SELF.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/VirtualCorpusTestBase.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusTestBase.java
similarity index 61%
rename from src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusTestBase.java
rename to src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusTestBase.java
index 04538e9..4babd96 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusTestBase.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusTestBase.java
@@ -1,4 +1,4 @@
-package de.ids_mannheim.korap.web.controller;
+package de.ids_mannheim.korap.web.controller.vc;
 
 import static org.hamcrest.CoreMatchers.hasItem;
 import static org.hamcrest.CoreMatchers.not;
@@ -9,12 +9,6 @@
 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;
 
@@ -23,11 +17,18 @@
 
 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 OAuth2TestBase {
+public abstract class VirtualCorpusTestBase extends UserGroupTestBase {
 
     protected JsonNode retrieveVCInfo (String username, String vcCreator,
             String vcName) throws ProcessingException, KustvaktException {
@@ -53,6 +54,40 @@
 
         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 {
@@ -114,8 +149,8 @@
         return node;
     }
 
-    protected Response testShareVCByCreator (String vcCreator, String vcName,
-            String groupName) throws ProcessingException, KustvaktException {
+    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()
@@ -123,11 +158,29 @@
                         .createBasicAuthorizationHeaderValue(vcCreator, "pass"))
                 .post(Entity.form(new Form()));
     }
+    
+    protected Response shareVC (String vcCreator, String vcName,
+            String groupName, String username) throws ProcessingException, KustvaktException {
 
-    protected JsonNode listAccessByGroup (String username, String groupName)
+        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).request()
+                .queryParam("groupName", groupName)
+                .queryParam("hasQuery", hasQuery)
+                .request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .get();
@@ -136,7 +189,7 @@
         return node;
     }
 
-    protected void deleteVC (String vcName, String vcCreator, String username)
+    protected Response deleteVC (String vcName, String vcCreator, String username)
             throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc")
                 .path("~" + vcCreator).path(vcName).request()
@@ -144,7 +197,8 @@
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .delete();
 
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        return response;
     }
 
     protected void testResponseUnauthorized (Response response, String username)
@@ -177,4 +231,41 @@
             }
         }
     }
+    
+    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());
+    }
+    
+    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-dnb.conf b/src/test/resources/kustvakt-dnb.conf
index 54e0edf..c940eb8 100644
--- a/src/test/resources/kustvakt-dnb.conf
+++ b/src/test/resources/kustvakt-dnb.conf
@@ -30,17 +30,6 @@
 server.port=8089
 server.host=localhost
 
-# Mail settings
-#
-mail.enabled = false
-mail.receiver = test@localhost
-mail.sender = noreply@ids-mannheim.de
-mail.address.retrieval = test
-
-# Mail.templates
-#
-template.group.invitation = notification.vm
-
 # Default foundries for specific layers (optional)
 #
 default.foundry.partOfSpeech = tt
diff --git a/src/test/resources/kustvakt-icc.conf b/src/test/resources/kustvakt-icc.conf
index 354022e..9714955 100644
--- a/src/test/resources/kustvakt-icc.conf
+++ b/src/test/resources/kustvakt-icc.conf
@@ -21,15 +21,6 @@
 server.port=8089
 server.host=localhost
 
-# mail settings
-mail.enabled = false
-mail.receiver = test@localhost
-mail.sender = noreply@ids-mannheim.de
-mail.address.retrieval = test
-
-# mail.templates
-template.group.invitation = notification.vm
-
 # default foundries for specific layers
 default.foundry.partOfSpeech = tt
 default.foundry.lemma = tt
diff --git a/src/test/resources/kustvakt-test.conf b/src/test/resources/kustvakt-test.conf
index f10d3e8..7818711 100644
--- a/src/test/resources/kustvakt-test.conf
+++ b/src/test/resources/kustvakt-test.conf
@@ -28,16 +28,8 @@
 server.port=8089
 server.host=localhost
 
-# Mail settings
-#
-mail.enabled = false
-mail.receiver = test@localhost
-mail.sender = noreply@ids-mannheim.de
-mail.address.retrieval = test
-
-# Mail.templates
-#
-template.group.invitation = notification.vm
+## Cache
+cache.total.results.enabled = true
 
 # Default foundries for specific layers (optional)
 #
@@ -52,7 +44,7 @@
 # Delete configuration (default hard)
 #
 # delete.auto.group = hard
-delete.group = soft
+#delete.group = soft
 delete.group.member = soft
 
 # Virtual corpus and queries
diff --git a/src/test/resources/test-config-dnb.xml b/src/test/resources/test-config-dnb.xml
index 53c8b8a..ac4d373 100644
--- a/src/test/resources/test-config-dnb.xml
+++ b/src/test/resources/test-config-dnb.xml
@@ -40,8 +40,6 @@
 			<array>
 				<value>classpath:test-jdbc.properties</value>
 				<value>file:./test-jdbc.properties</value>
-				<value>classpath:properties/mail.properties</value>
-				<value>file:./mail.properties</value>
 				<value>classpath:test-hibernate.properties</value>
 				<value>file:./kustvakt-dnb.conf</value>
 				<value>classpath:kustvakt-dnb.conf</value>
@@ -321,38 +319,4 @@
 		<property name="dataSource" ref="dataSource" />
 	</bean>
 
-	<!-- mail -->
-	<bean id="authenticator"
-		class="de.ids_mannheim.korap.service.MailAuthenticator">
-		<constructor-arg index="0" value="${mail.username}" />
-		<constructor-arg index="1" value="${mail.password}" />
-	</bean>
-	<bean id="smtpSession" class="jakarta.mail.Session"
-		factory-method="getInstance">
-		<constructor-arg index="0">
-			<props>
-				<prop key="mail.smtp.submitter">${mail.username}</prop>
-				<prop key="mail.smtp.auth">${mail.auth}</prop>
-				<prop key="mail.smtp.host">${mail.host}</prop>
-				<prop key="mail.smtp.port">${mail.port}</prop>
-				<prop key="mail.smtp.starttls.enable">${mail.starttls.enable}</prop>
-				<prop key="mail.smtp.connectiontimeout">${mail.connectiontimeout}</prop>
-			</props>
-		</constructor-arg>
-		<constructor-arg index="1" ref="authenticator" />
-	</bean>
-	<bean id="mailSender"
-		class="org.springframework.mail.javamail.JavaMailSenderImpl">
-		<property name="session" ref="smtpSession" />
-	</bean>
-	<bean id="velocityEngine"
-		class="org.apache.velocity.app.VelocityEngine">
-		<constructor-arg index="0">
-			<props>
-				<prop key="resource.loader">class</prop>
-				<prop key="class.resource.loader.class">org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
-				</prop>
-			</props>
-		</constructor-arg>
-	</bean>
 </beans>
diff --git a/src/test/resources/test-config-icc.xml b/src/test/resources/test-config-icc.xml
index d418f05..4924c3c 100644
--- a/src/test/resources/test-config-icc.xml
+++ b/src/test/resources/test-config-icc.xml
@@ -40,8 +40,6 @@
 			<array>
 				<value>classpath:test-jdbc.properties</value>
 				<value>file:./jdbc.properties</value>
-				<value>classpath:properties/mail.properties</value>
-				<value>file:./mail.properties</value>
 				<value>classpath:properties/hibernate.properties</value>
 				<value>file:./kustvakt-icc.conf</value>
 				<value>classpath:kustvakt-icc.conf</value>
@@ -314,38 +312,4 @@
 		<property name="dataSource" ref="dataSource" />
 	</bean>
 
-	<!-- mail -->
-	<bean id="authenticator"
-		class="de.ids_mannheim.korap.service.MailAuthenticator">
-		<constructor-arg index="0" value="${mail.username}" />
-		<constructor-arg index="1" value="${mail.password}" />
-	</bean>
-	<bean id="smtpSession" class="jakarta.mail.Session"
-		factory-method="getInstance">
-		<constructor-arg index="0">
-			<props>
-				<prop key="mail.smtp.submitter">${mail.username}</prop>
-				<prop key="mail.smtp.auth">${mail.auth}</prop>
-				<prop key="mail.smtp.host">${mail.host}</prop>
-				<prop key="mail.smtp.port">${mail.port}</prop>
-				<prop key="mail.smtp.starttls.enable">${mail.starttls.enable}</prop>
-				<prop key="mail.smtp.connectiontimeout">${mail.connectiontimeout}</prop>
-			</props>
-		</constructor-arg>
-		<constructor-arg index="1" ref="authenticator" />
-	</bean>
-	<bean id="mailSender"
-		class="org.springframework.mail.javamail.JavaMailSenderImpl">
-		<property name="session" ref="smtpSession" />
-	</bean>
-	<bean id="velocityEngine"
-		class="org.apache.velocity.app.VelocityEngine">
-		<constructor-arg index="0">
-			<props>
-				<prop key="resource.loader">class</prop>
-				<prop key="class.resource.loader.class">org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
-				</prop>
-			</props>
-		</constructor-arg>
-	</bean>
 </beans>
diff --git a/src/test/resources/test-config.xml b/src/test/resources/test-config.xml
index f79f967..5811a33 100644
--- a/src/test/resources/test-config.xml
+++ b/src/test/resources/test-config.xml
@@ -39,12 +39,12 @@
 		<property name="locations">
 			<array>
 				<value>classpath:test-jdbc.properties</value>
-				<value>file:./test-jdbc.properties</value>
-				<value>classpath:properties/mail.properties</value>
-				<value>file:./mail.properties</value>
+				<value>file:./data/test-jdbc.properties</value>
 				<value>classpath:test-hibernate.properties</value>
+				<value>file:./data/test-hibernate.properties</value>
 				<value>file:./kustvakt-test.conf</value>
 				<value>classpath:kustvakt-test.conf</value>
+				<value>file:./data/kustvakt-test.conf</value>
 			</array>
 		</property>
 	</bean>
@@ -289,41 +289,4 @@
 		class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
 		<property name="dataSource" ref="dataSource" />
 	</bean>
-
-	<!-- mail -->
-	<bean id="authenticator"
-		class="de.ids_mannheim.korap.service.MailAuthenticator">
-		<constructor-arg index="0" value="${mail.username}" />
-		<constructor-arg index="1" value="${mail.password}" />
-	</bean>
-	<bean id="smtpSession" class="jakarta.mail.Session"
-		factory-method="getInstance">
-		<constructor-arg index="0">
-			<props>
-				<prop key="mail.smtp.submitter">${mail.username}</prop>
-				<prop key="mail.smtp.auth">${mail.auth}</prop>
-				<prop key="mail.smtp.host">${mail.host}</prop>
-				<prop key="mail.smtp.port">${mail.port}</prop>
-				<prop key="mail.smtp.starttls.enable">${mail.starttls.enable}</prop>
-				<prop key="mail.smtp.connectiontimeout">${mail.connectiontimeout}</prop>
-			</props>
-		</constructor-arg>
-		<constructor-arg index="1" ref="authenticator" />
-	</bean>
-	<bean id="mailSender"
-		class="org.springframework.mail.javamail.JavaMailSenderImpl">
-		<property name="username" value="${mail.username}" />
-		<property name="password" value="${mail.password}" />
-		<property name="session" ref="smtpSession" />
-	</bean>
-	<bean id="velocityEngine"
-		class="org.apache.velocity.app.VelocityEngine">
-		<constructor-arg index="0">
-			<props>
-				<prop key="resource.loader">class</prop>
-				<prop key="class.resource.loader.class">org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader
-				</prop>
-			</props>
-		</constructor-arg>
-	</bean>
 </beans>