Replace invite and subscribe to add member, remove unsubscribe (#764).

Change-Id: I07f3ff872c19a0246d49c402f4359f8ea24b843f
diff --git a/Changes b/Changes
index 00bcacb..85d9ba2 100644
--- a/Changes
+++ b/Changes
@@ -10,6 +10,11 @@
 - 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)
 
 # version 0.74
 
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 fedd573..5026b6e 100644
--- a/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
+++ b/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
@@ -15,7 +15,6 @@
 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;
@@ -306,11 +305,12 @@
      *            the status of the membership
      * @throws KustvaktException
      */
+    @Deprecated
     public void inviteGroupMember (String username, UserGroup userGroup,
             String createdBy)
             throws KustvaktException {
 
-        addGroupMember(username, userGroup, createdBy);
+        addGroupMember(username, userGroup, createdBy,null);
 
         if (config.isMailEnabled()
                 && userGroup.getStatus() != UserGroupStatus.HIDDEN) {
@@ -320,41 +320,45 @@
     }
 
     public void addGroupMember (String username, UserGroup userGroup,
-            String createdBy)
-            throws KustvaktException {
-        addGroupMember(username, userGroup, createdBy, null);
-    }
-    
-    public void addGroupMember (String username, UserGroup userGroup,
             String createdBy, Set<Role> roles)
             throws KustvaktException {
-        int groupId = userGroup.getId();
-        ParameterChecker.checkIntegerValue(groupId, "userGroupId");
-
-        UserGroupMember member = new UserGroupMember();
-        member.setGroup(userGroup);
-        member.setUserId(username);
-        if (roles !=null) {
-            member.setRoles(roles);
+        
+        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);
         }
-        groupMemberDao.addMember(member);
+        else {
+            throw new KustvaktException(StatusCodes.GROUP_MEMBER_EXISTS,
+                    "Username: "+username+" exists in the user-group: "+
+                    userGroup.getName(), username, userGroup.getName());
+        }
     }
 
-    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 (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);
+                addGroupMember(memberName, userGroup, username,memberRoles);
             }
         }
         else {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
-                    "Unauthorized operation for user: " + inviter, inviter);
+                    "Unauthorized operation for user: " + username, username);
         }
     }
 
@@ -396,6 +400,7 @@
      *            the username of the group member
      * @throws KustvaktException
      */
+    @Deprecated
     public void acceptInvitation (String groupName, String username)
             throws KustvaktException {
 
@@ -418,9 +423,9 @@
 //
 //            if (expiration.isAfter(now)) {
 //                member.setStatus(GroupMemberStatus.ACTIVE);
-//                Set<Role> memberRoles = prepareMemberRoles(userGroup);
-//                member.setRoles(memberRoles);
-//                groupMemberDao.updateMember(member);
+                Set<Role> memberRoles = prepareMemberRoles(userGroup);
+                member.setRoles(memberRoles);
+                groupMemberDao.updateMember(member);
 //            }
 //            else {
 //                throw new KustvaktException(StatusCodes.INVITATION_EXPIRED);
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 33db5f6..c0325b8 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
@@ -201,10 +201,10 @@
      *            usernames separated by comma
      * @return if successful, HTTP response status OK
      */
-    @POST
-    @Path("@{groupName}/invite")
+    @PUT
+    @Path("@{groupName}/member")
     @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
-    public Response inviteGroupMembers (
+    public Response addGroupMembers (
             @Context SecurityContext securityContext,
             @PathParam("groupName") String groupName,
             @FormParam("members") String members) {
@@ -213,8 +213,7 @@
         try {
             scopeService.verifyScope(context,
                     OAuth2Scope.ADD_USER_GROUP_MEMBER);
-            service.inviteGroupMembers(groupName, members,
-                    context.getUsername());
+            service.addGroupMembers(groupName, members, context.getUsername());
             return Response.ok("SUCCESS").build();
         }
         catch (KustvaktException e) {
@@ -295,6 +294,7 @@
      *            a group name
      * @return if successful, HTTP response status OK
      */
+    @Deprecated
     @POST
     @Path("@{groupName}/subscribe")
     public Response subscribeToGroup (@Context SecurityContext securityContext,
@@ -323,6 +323,7 @@
      * @param groupName
      * @return if successful, HTTP response status OK
      */
+    @Deprecated
     @DELETE
     @Path("@{groupName}/unsubscribe")
     public Response unsubscribeFromGroup (
diff --git a/src/test/java/de/ids_mannheim/korap/dao/DaoTestBase.java b/src/test/java/de/ids_mannheim/korap/dao/DaoTestBase.java
index 019ecda..d282c17 100644
--- a/src/test/java/de/ids_mannheim/korap/dao/DaoTestBase.java
+++ b/src/test/java/de/ids_mannheim/korap/dao/DaoTestBase.java
@@ -34,8 +34,8 @@
     
     protected UserGroup createDoryGroup () throws KustvaktException {
         UserGroup group = createUserGroup("dory-group", "dory");
-        userGroupService.addGroupMember("nemo", group, "dory");
-        userGroupService.addGroupMember("marlin", group, "dory");
+        userGroupService.addGroupMember("nemo", group, "dory",null);
+        userGroupService.addGroupMember("marlin", group, "dory",null);
         return group;
     }
     
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 d8dff83..4f0f2cc 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
@@ -50,8 +50,7 @@
         
         createDoryGroup();
         createMarlinGroup();
-        inviteMember(marlinGroupName, "marlin", "dory");
-        subscribe(marlinGroupName, "dory");
+        addMember(marlinGroupName, "dory", "marlin");
         
         // test list user group
         response = target().path(API_VERSION).path("group").request()
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/usergroup/UserGroupControllerAdminTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerAdminTest.java
index 7cedc2b..13fa68a 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerAdminTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupControllerAdminTest.java
@@ -14,7 +14,6 @@
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.service.UserGroupService;
 import de.ids_mannheim.korap.utils.JsonUtils;
-import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Entity;
 import jakarta.ws.rs.core.Form;
 import jakarta.ws.rs.core.MediaType;
@@ -29,7 +28,7 @@
     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"))
@@ -45,13 +44,11 @@
         createDoryGroup();
         
         createMarlinGroup();
-        inviteMember(marlinGroupName, "marlin", "dory");
-        subscribe(marlinGroupName, "dory");
+        addMember(marlinGroupName, "dory", "marlin");
         
         String testGroup = "test-group"; 
         createUserGroup("test-group", "Test group to be deleted.", "marlin");
-        inviteMember(testGroup, "marlin", "dory");
-        subscribe(testGroup, "dory");
+        addMember(testGroup, "dory", "marlin");
         deleteGroupByName("test-group", "marlin");
 
         
@@ -138,7 +135,7 @@
     // 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
@@ -152,7 +149,7 @@
 
     @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,
@@ -176,14 +173,14 @@
 
     @Test
     public void testListHiddenGroups ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         JsonNode node = listHiddenGroup();
         assertEquals(1, node.size());
     }
 
     @Test
     public void testUserGroupAdmin ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         String groupName = "admin-test-group";
         Response response = createUserGroup(groupName, "test group", testUser);
         assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
@@ -192,8 +189,7 @@
         assertEquals(1, node.size());
         node = node.get(0);
         assertEquals(groupName, node.get("name").asText());
-        testInviteMember(groupName);
-        subscribe(groupName, "marlin");
+        testAddMember(groupName);
         testAddAdminRole(groupName, "marlin");
         testDeleteMemberRoles(groupName, "marlin");
         testDeleteMember(groupName);
@@ -207,7 +203,7 @@
 
 
     private void testAddAdminRole (String groupName, String memberUsername)
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = addAdminRole(groupName, memberUsername, admin);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
         
@@ -223,7 +219,7 @@
     }
 
     private void testDeleteMemberRoles (String groupName, String memberUsername)
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Form form = new Form();
         form.param("memberUsername", memberUsername);
         // USER_GROUP_ADMIN
@@ -248,7 +244,7 @@
     }
 
     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,
@@ -263,7 +259,7 @@
     }
 
     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()
@@ -280,24 +276,23 @@
         assertEquals(node.at("/members/1/userId").asText(), "nemo");
     }
 
-    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(
                                         admin, "pass"))
-                .post(Entity.form(form));
+                .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(0, node.at("/members/1/privileges").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
index 673fbfc..919c3c0 100644
--- 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
@@ -12,7 +12,6 @@
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.utils.JsonUtils;
-import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Entity;
 import jakarta.ws.rs.core.Form;
 import jakarta.ws.rs.core.Response;
@@ -27,7 +26,7 @@
 
     @Test
     public void testCreateGroupEmptyDescription ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         String groupName = "empty_group";
         Response response = createUserGroup(groupName, "", username);
         assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
@@ -36,7 +35,7 @@
 
     @Test
     public void testCreateGroupMissingDescription ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         String groupName = "missing-desc-group";
         Response response = testCreateGroupWithoutDescription(groupName);
         assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
@@ -44,7 +43,7 @@
     }
 
     private Response testCreateGroupWithoutDescription (String groupName)
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("group")
                 .path("@" + groupName).request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
@@ -56,7 +55,7 @@
 
     @Test
     public void testCreateGroupInvalidName ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         String groupName = "invalid-group-name$";
         Response response = testCreateGroupWithoutDescription(groupName);
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
@@ -70,7 +69,7 @@
 
     @Test
     public void testCreateGroupNameTooShort ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         String groupName = "a";
         Response response = testCreateGroupWithoutDescription(groupName);
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
@@ -83,7 +82,7 @@
     }
 
     @Test
-    public void testUserGroup () throws ProcessingException, KustvaktException {
+    public void testUserGroup () throws KustvaktException {
         String groupName = "new-user-group";
         String description = "This is new-user-group.";
         Response response = createUserGroup(groupName, description, username);
@@ -103,19 +102,14 @@
         assertEquals(5,  node.at("/members/0/privileges").size());
 
         testUpdateUserGroup(groupName);
-        testInviteMember(groupName, username, "darla");
-        
-        testDeleteMemberUnauthorizedByNonMember(groupName,"darla");
-        testDeleteMemberUnauthorizedByMember(groupName, "darla");
-
-        testDeleteMember(groupName, username);
+        testAddMember(groupName, username, "darla");
         testDeleteGroup(groupName,username);
 //        testSubscribeToDeletedGroup(groupName);
 //        testUnsubscribeToDeletedGroup(groupName);
     }
     
     private void testUpdateUserGroup (String groupName)
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         String description = "Description is updated.";
         Response response = createUserGroup(groupName, description, username);
         assertEquals(Status.NO_CONTENT.getStatusCode(), response.getStatus());
@@ -124,82 +118,9 @@
         assertEquals(description, node.get(0).get("description").asText());
     }
 
-    private void testDeleteMember (String groupName, String username)
-            throws ProcessingException, KustvaktException {
-        // delete darla from group
-        deleteMember(groupName, "darla", username);
-        // check group member
-        JsonNode node = listUserGroups(username);
-        node = node.get(0);
-        assertEquals(1, node.get("members").size());
-    }
-
-    private void testDeleteMemberUnauthorizedByNonMember (String groupName,
-            String memberName) throws ProcessingException, KustvaktException {
-
-        Response response = deleteMember(groupName, memberName, "nemo");
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Unauthorized operation for user: nemo");
-    }
-    
-    private void testDeleteMemberUnauthorizedByMember (String groupName,
-            String memberName) throws ProcessingException, KustvaktException {
-        inviteMember(groupName, "dory", "nemo");
-        subscribe(groupName, "nemo");
-        // nemo is a group member
-        Response response = deleteMember(groupName, memberName, "nemo");
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
-                node.at("/errors/0/0").asInt());
-        assertEquals(node.at("/errors/0/1").asText(),
-                "Unauthorized operation for user: nemo");
-    }
-
-    @Test
-    public void testDeletePendingMember ()
-            throws ProcessingException, KustvaktException {
-        createDoryGroup();
-        inviteMember(doryGroupName, "dory", "pearl");
-        // dory delete pearl
-        Response response = deleteMember(doryGroupName, "pearl", "dory");
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // check member
-        JsonNode node = listUserGroups("pearl");
-        assertEquals(0, node.size());
-        
-        deleteGroupByName(doryGroupName, "dory");
-    }
-
-    @Test
-    public void testDeleteDeletedMember ()
-            throws ProcessingException, KustvaktException {
-        createDoryGroup();
-        inviteMember(doryGroupName, "dory", "pearl");
-        subscribe(doryGroupName, "pearl");
-        deleteMember(doryGroupName, "pearl", "pearl");
-        
-        Response response = deleteMember(doryGroupName, "pearl", "pearl");
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.GROUP_MEMBER_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");
-    }
-
+   
     private void testDeleteGroup (String groupName, String username)
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         deleteGroupByName(groupName, username);
         JsonNode node = listUserGroups(username);
         assertEquals(0, node.size());
@@ -207,10 +128,9 @@
 
     @Test
     public void testDeleteGroupUnauthorized ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         createMarlinGroup();
-        inviteMember(marlinGroupName, "marlin", "dory");
-        subscribe(marlinGroupName, "dory");
+        addMember(marlinGroupName, "dory", "marlin");
         
         addAdminRole(marlinGroupName, "dory", "marlin");
         
@@ -234,7 +154,7 @@
 
     @Test
     public void testDeleteDeletedGroup ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         createMarlinGroup();
         deleteGroupByName(marlinGroupName, "marlin");
         Response response = deleteGroupByName(marlinGroupName, "marlin");
@@ -243,7 +163,7 @@
 
     @Test
     public void testDeleteGroupOwner ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         createMarlinGroup();
         // delete marlin from marlin-group
         // dory is a group admin in marlin-group
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
index ac03ff5..b96467a 100644
--- 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
@@ -19,8 +19,8 @@
     @Test
     public void testListDoryGroups () throws KustvaktException {
         createDoryGroup();
-        inviteMember(doryGroupName, "dory", "marlin");
-        inviteMember(doryGroupName, "dory", "nemo");
+        addMember(doryGroupName, "marlin", "dory");
+        addMember(doryGroupName, "nemo", "dory");
         
         JsonNode node = listUserGroups("dory");
         JsonNode group = node.get(0);
@@ -36,8 +36,6 @@
     }
     
     public void testListNemoGroups () throws KustvaktException {
-        Response response = subscribe(doryGroupName, "nemo");
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
         JsonNode node = listUserGroups("nemo");
         assertEquals(node.at("/0/name").asText(), "dory-group");
         assertEquals(node.at("/0/owner").asText(), "dory");
@@ -49,8 +47,6 @@
     // marlin has 2 groups
     public void testListMarlinGroups () throws KustvaktException {
         createMarlinGroup();
-        Response response = subscribe(doryGroupName, "marlin");
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
         JsonNode node = listUserGroups("marlin");
         assertEquals(2, node.size());
     }
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
index b12ff69..10ce9ba 100644
--- 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
@@ -4,11 +4,11 @@
 
 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 com.google.common.net.HttpHeaders;
 
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
 import de.ids_mannheim.korap.config.Attributes;
@@ -19,7 +19,6 @@
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.utils.JsonUtils;
-import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Entity;
 import jakarta.ws.rs.core.Form;
 import jakarta.ws.rs.core.Response;
@@ -31,71 +30,261 @@
     private UserGroupMemberDao memberDao;
 
     @Test
-    public void testInvitePendingMember ()
-            throws ProcessingException, KustvaktException {
+    public void testAddMultipleMembers ()
+            throws KustvaktException {
         createDoryGroup();
-        inviteMember(doryGroupName, "dory", "marlin");
+        addMember(doryGroupName, "nemo,marlin,pearl", "dory");
         
-        // marlin has status PENDING in dory-group
-        Response response = inviteMember(doryGroupName, "dory", "marlin");
-        String entity = response.readEntity(String.class);
-        // System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
+        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 marlin with status PENDING exists in the user-group "
+                "Username: nemo exists in the user-group: "
                         + "dory-group",
                 node.at("/errors/0/1").asText());
         assertEquals(node.at("/errors/0/2").asText(),
-                "[marlin, PENDING, dory-group]");
+                "[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 testInviteActiveMember ()
-            throws ProcessingException, KustvaktException {
+    public void testDeleteMemberByGroupOwner ()
+            throws KustvaktException {
         createDoryGroup();
-        inviteMember(doryGroupName, "dory", "nemo");
-        subscribe(doryGroupName, "nemo");
-        // nemo has status active in dory-group
-        Form form = new Form();
-        form.param("members", "nemo");
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("invite").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("dory", "pass"))
-                .post(Entity.form(form));
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        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(StatusCodes.GROUP_MEMBER_EXISTS,
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
                 node.at("/errors/0/0").asInt());
-        assertEquals(
-                "Username nemo with status ACTIVE exists in the user-group "
-                        + "dory-group",
-                node.at("/errors/0/1").asText());
-        assertEquals(node.at("/errors/0/2").asText(),
-                "[nemo, ACTIVE, dory-group]");
-        
-        deleteGroupByName(doryGroupName, "dory");
-        
-        testInviteMemberToDeletedGroup();
+        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);
     }
 
-    private void testInviteMemberToDeletedGroup () throws KustvaktException {
-        Response response = inviteMember(doryGroupName, "dory", "nemo");
+    @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.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");
+        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
@@ -124,8 +313,8 @@
     @Test
     public void testAddMemberRole () throws KustvaktException {
         createMarlinGroup();
-        inviteMember(marlinGroupName, "marlin", "dory");
-        subscribe(marlinGroupName, "dory");
+        addMember(marlinGroupName, "dory", "marlin");
+        
         JsonNode marlinGroup = listUserGroups("marlin");
         int groupId = marlinGroup.at("/0/id").asInt();
         
@@ -147,7 +336,7 @@
     }
 
     private void testAddSameMemberRole (int groupId)
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = addAdminRole(marlinGroupName, "dory", "marlin");
         assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
         
@@ -162,7 +351,7 @@
     }
 
     private void testDeleteMemberRole (int groupId)
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Form form = new Form();
         form.param("memberUsername", "dory");
         form.param("role", PredefinedRole.GROUP_ADMIN.name());
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupSubscriptionTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupSubscriptionTest.java
deleted file mode 100644
index b0c80a5..0000000
--- a/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupSubscriptionTest.java
+++ /dev/null
@@ -1,256 +0,0 @@
-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.ProcessingException;
-import jakarta.ws.rs.client.Entity;
-import jakarta.ws.rs.core.Form;
-import jakarta.ws.rs.core.Response;
-import jakarta.ws.rs.core.Response.Status;
-
-public class UserGroupSubscriptionTest extends UserGroupTestBase {
-    
-    @Test
-    public void testSubscribeNonExistentMember () throws KustvaktException {
-        createDoryGroup();
-        
-        Response response = subscribe(doryGroupName, "bruce");
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.GROUP_MEMBER_NOT_FOUND,
-                node.at("/errors/0/0").asInt());
-        assertEquals("bruce is not found in the group",
-                node.at("/errors/0/1").asText());
-        
-        testSubscribeDeletedMember();
-        deleteGroupByName(doryGroupName, "dory");
-    }
-    
-    // pearl has GroupMemberStatus.DELETED in dory-group
-    private void testSubscribeDeletedMember () throws KustvaktException {
-        inviteMember(doryGroupName, "dory", "pearl");
-        // delete pending member
-        deleteMember(doryGroupName, "pearl", "dory");
-        
-        Response response = subscribe(doryGroupName, "pearl");
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.GROUP_MEMBER_NOT_FOUND,
-                node.at("/errors/0/0").asInt());
-        
-        testUnsubscribeDeletedMember();
-        testInviteDeletedMember("pearl", "dory");
-    }
-    
-    // marlin has GroupMemberStatus.PENDING in dory-group
-    @Test
-    public void testSubscribePendingMember () throws KustvaktException {
-        createDoryGroup();
-        testInviteMember(doryGroupName, "dory", "marlin");
-        Response response = subscribe(doryGroupName, "marlin");
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        
-        // retrieve marlin group
-        JsonNode node = listUserGroups("marlin");
-        assertEquals(1, node.size());
-        JsonNode group = node.get(0);
-        assertEquals(group.at("/name").asText(), "dory-group");
-        assertEquals(group.at("/owner").asText(), "dory");
-        // group members are not allowed to see other members
-        assertEquals(0, group.at("/members").size());
-        
-        assertEquals(1, group.at("/userPrivileges").size());
-        
-        // unsubscribe marlin from dory-group
-        testUnsubscribeActiveMember("dory-group");
-        checkGroupMemberRole("dory-group", "marlin");
-        testInviteDeletedMember("marlin", "dory");
-        
-        deleteGroupByName(doryGroupName, "dory");
-    }
-    
-    private void testInviteDeletedMember (String invitee, String invitor)
-            throws ProcessingException, KustvaktException {
-        
-        Response response = inviteMember(doryGroupName, invitor, invitee);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        // check member
-        JsonNode node = listUserGroups(invitee);
-        assertEquals(1, node.size());
-    }
-    
-    private void checkGroupMemberRole (String groupName,
-            String deletedMemberName) throws KustvaktException {
-        Response response = target().path(API_VERSION).path("admin")
-                .path("group").path("@" + groupName).request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("admin", "pass"))
-                .post(null);
-        String entity = response.readEntity(String.class);
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        JsonNode node = JsonUtils.readTree(entity).at("/members");
-        JsonNode member;
-        for (int i = 0; i < node.size(); i++) {
-            member = node.get(i);
-            if (deletedMemberName.equals(member.at("/userId").asText())) {
-                assertEquals(0, node.at("/privileges").size());
-                break;
-            }
-        }
-    }
-    
-
-    @Test
-    public void testSubscribeMissingGroupName () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("subscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("bruce", "pass"))
-                .post(Entity.form(new Form()));
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-    }
-
-
-    @Test
-    public void testSubscribeToNonExistentGroup () throws KustvaktException {
-        Response response = subscribe("non-existent", "pearl");
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
-                node.at("/errors/0/0").asInt());
-        assertEquals("Group non-existent is not found",
-                node.at("/errors/0/1").asText());
-    }
-
-    @Test
-    public void testSubscribeToDeletedGroup ()
-            throws ProcessingException, KustvaktException {
-        createDoryGroup();
-        // hard delete
-        deleteGroupByName(doryGroupName, "dory");
-        
-        Response response = subscribe(doryGroupName, "nemo");
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-//        String entity = response.readEntity(String.class);
-//        JsonNode node = JsonUtils.readTree(entity);
-//        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
-//        assertEquals(node.at("/errors/0/1").asText(),
-//                "Group new-user-group has been deleted.");
-        testUnsubscribeToDeletedGroup(doryGroupName);
-        
-    }
-
-    private void testUnsubscribeToDeletedGroup (String groupName)
-            throws ProcessingException, KustvaktException {
-        Response response = unsubscribe(doryGroupName, "nemo");
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-//        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-//        String entity = response.readEntity(String.class);
-//        JsonNode node = JsonUtils.readTree(entity);
-//        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
-//        assertEquals(node.at("/errors/0/1").asText(),
-//                "Group new-user-group has been deleted.");
-    }
-
-    private void testUnsubscribeActiveMember (String groupName)
-            throws ProcessingException, KustvaktException {
-        Response response = unsubscribe(groupName, "marlin");
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        JsonNode node = listUserGroups("marlin");
-        assertEquals(0, node.size());
-    }
-    
-    @Test
-    public void testUnsubscribePendingMember ()
-            throws ProcessingException, KustvaktException {
-        createDoryGroup();
-        testInviteMember(doryGroupName, "dory", "marlin");
-        JsonNode node = listUserGroups("marlin");
-        assertEquals(1, node.size());
-
-        Response response = unsubscribe(doryGroupName, "marlin");
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        node = listUserGroups("marlin");
-        assertEquals(0, node.size());
-        // invite marlin to dory-group to set back the
-        // GroupMemberStatus.PENDING
-        testInviteDeletedMember("marlin","dory");
-        deleteGroupByName(doryGroupName, "dory");
-    }
-
-    private void testUnsubscribeDeletedMember ()
-            throws ProcessingException, KustvaktException {
-        // pearl unsubscribes from dory-group
-        Response response = unsubscribe(doryGroupName, "pearl");
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.GROUP_MEMBER_NOT_FOUND,
-                node.at("/errors/0/0").asInt());
-    }
-
-
-    @Test
-    public void testUnsubscribeMissingGroupName () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("unsubscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .delete();
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-    }
-
-    @Test
-    public void testUnsubscribeNonExistentMember () throws KustvaktException {
-        createDoryGroup();
-        Response response = target().path(API_VERSION).path("group")
-                .path("@dory-group").path("unsubscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("bruce", "pass"))
-                .delete();
-        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.GROUP_MEMBER_NOT_FOUND,
-                node.at("/errors/0/0").asInt());
-        assertEquals("bruce is not found in the group", 
-                node.at("/errors/0/1").asText());
-        deleteGroupByName(doryGroupName, "dory");
-    }
-
-    @Test
-    public void testUnsubscribeToNonExistentGroup () throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@tralala-group").path("unsubscribe").request()
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("pearl", "pass"))
-                .delete();
-        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
-        String entity = response.readEntity(String.class);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
-                node.at("/errors/0/0").asInt());
-        assertEquals("Group tralala-group is not found",
-                node.at("/errors/0/1").asText());
-    }
-
-}
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
index 81df9a5..b8f3bc1 100644
--- 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
@@ -11,7 +11,6 @@
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.utils.JsonUtils;
 import de.ids_mannheim.korap.web.controller.OAuth2TestBase;
-import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Entity;
 import jakarta.ws.rs.core.Form;
 import jakarta.ws.rs.core.MediaType;
@@ -25,7 +24,7 @@
     protected String admin = "admin";
 
     protected Response createUserGroup (String groupName, String description,
-            String username) throws ProcessingException, KustvaktException {
+            String username) throws KustvaktException {
         Form form = new Form();
         form.param("description", description);
         Response response = target().path(API_VERSION).path("group")
@@ -59,53 +58,46 @@
         return node;
     }
 
-    protected Response inviteMember (String groupName, String invitor,
-            String invitee) throws KustvaktException {
+    
+    protected Response addMember (String groupName, String memberUsername,
+            String username) throws KustvaktException {
         Form form = new Form();
-        form.param("members", invitee);
+        form.param("members", memberUsername);
         Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("invite").request()
+                .path("@" + groupName).path("member").request()
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(invitor, "pass"))
-                .post(Entity.form(form));
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .put(Entity.form(form));
 //        assertEquals(Status.OK.getStatusCode(), response.getStatus());
         return response;
     }
     
-    protected void testInviteMember (String groupName, String invitor,
-            String invitee)
-            throws ProcessingException, KustvaktException {
-        Response response = inviteMember(groupName, invitor, invitee);
+//    protected Response inviteMember (String groupName, String invitor,
+//            String invitee) throws KustvaktException {
+//        Form form = new Form();
+//        form.param("members", invitee);
+//        Response response = target().path(API_VERSION).path("group")
+//                .path("@" + groupName).path("invite").request()
+//                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+//                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+//                        .createBasicAuthorizationHeaderValue(invitor, "pass"))
+//                .post(Entity.form(form));
+////        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+//        return response;
+//    }
+    
+    protected void 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(invitor);
+        JsonNode node = listUserGroups(username);
         node = node.get(0);
         assertEquals(2, node.get("members").size());
-        assertEquals(node.at("/members/1/userId").asText(), invitee);
-        assertEquals(0, node.at("/members/1/privileges").size());
-    }
-
-    protected Response subscribe (String groupName, String username)
-            throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@"+groupName).path("subscribe").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .post(Entity.form(new Form()));
-//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        return response;
-    }
-    
-    protected Response unsubscribe (String groupName, String username)
-            throws KustvaktException {
-        Response response = target().path(API_VERSION).path("group")
-                .path("@" + groupName).path("unsubscribe").request()
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue(username, "pass"))
-                .delete();
-        return response;
-//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        assertEquals(node.at("/members/1/userId").asText(), memberUsername);
+        assertEquals(1, node.at("/members/1/privileges").size());
     }
 
     protected Response addAdminRole (String groupName, String memberName,
@@ -134,8 +126,7 @@
         return response;
     }
     
-    protected JsonNode createDoryGroup ()
-            throws ProcessingException, KustvaktException {
+    protected JsonNode createDoryGroup () throws KustvaktException {
         Response response = createUserGroup(doryGroupName,
                 "This is dory-group.", "dory");
         assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusControllerAdminTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusControllerAdminTest.java
index 673e1cd..08ca16e 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusControllerAdminTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusControllerAdminTest.java
@@ -2,6 +2,17 @@
 
 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
  */
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusInfoTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusInfoTest.java
index 6d98009..9495660 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusInfoTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusInfoTest.java
@@ -12,7 +12,6 @@
 import de.ids_mannheim.korap.constant.ResourceType;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.utils.JsonUtils;
-import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.core.Response;
 import jakarta.ws.rs.core.Response.Status;
 
@@ -24,7 +23,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(),
@@ -36,7 +35,7 @@
 
     @Test
     public void testRetrieveSystemVC_guest ()
-            throws ProcessingException, KustvaktException {
+            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));
@@ -47,7 +46,7 @@
 
     @Test
     public void testRetrievePrivateVC ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         JsonNode node = retrieveVCInfo("dory", "dory", "dory-vc");
         assertEquals(node.at("/name").asText(), "dory-vc");
         assertEquals(ResourceType.PRIVATE.displayName(),
@@ -56,7 +55,7 @@
 
     @Test
     public void testRetrievePrivateVC_unauthorized ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("dory-vc").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
@@ -67,10 +66,9 @@
 
     @Test
     public void testRetrieveProjectVC_member ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         createDoryGroup();
-        inviteMember(doryGroupName, "dory", "nemo");
-        subscribe(doryGroupName, "nemo");
+        addMember(doryGroupName, "nemo", "dory");
         
         createAccess("dory", "group-vc", doryGroupName, "dory");
         
@@ -79,8 +77,7 @@
         assertEquals(ResourceType.PROJECT.displayName(),
                 node.at("/type").asText());
         
-        inviteMember(doryGroupName, "dory", "pearl");
-        subscribe(doryGroupName, "pearl");
+        addMember(doryGroupName, "pearl", "dory");
         
         node = retrieveVCInfo("pearl", "dory", "group-vc");
         assertEquals(node.at("/name").asText(), "group-vc");
@@ -92,7 +89,7 @@
 
     @Test
     public void testRetrieveProjectVC_unauthorized ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("group-vc").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
@@ -103,7 +100,7 @@
 
     @Test
     public void testRetrieveProjectVC_nonActiveMember ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("group-vc").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
@@ -114,7 +111,7 @@
 
     @Test
     public void testRetrievePrivateVC_admin ()
-            throws ProcessingException, KustvaktException {
+            throws KustvaktException {
         Response response = target().path(API_VERSION).path("vc").path("~dory")
                 .path("dory-vc").request()
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
@@ -129,7 +126,7 @@
 
     @Test
     public void testRetrieveProjectVC_admin ()
-            throws ProcessingException, KustvaktException {
+            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/VirtualCorpusPublishedTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/vc/VirtualCorpusPublishedTest.java
index 9615fef..6fad6d7 100644
--- 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
@@ -41,7 +41,7 @@
     }
     
     private void testRetrievePublishedVC (String username, String vcCreator,
-            String vcName) throws ProcessingException, KustvaktException {
+            String vcName) throws KustvaktException {
         retrieveVCInfo(username, vcCreator, vcName);
         
         JsonNode node = getHiddenGroup(vcName);
@@ -127,8 +127,7 @@
     
     private String testSharePublishedVC (String vcName) throws KustvaktException {
         createMarlinGroup();
-        inviteMember(marlinGroupName, "marlin", "dory");
-        subscribe(marlinGroupName, "dory");
+        addMember(marlinGroupName, "dory", "marlin");
 
         JsonNode node = listVC("dory");
         assertEquals(3, node.size());
@@ -163,8 +162,7 @@
         JsonNode node = listVC("nemo");
         assertEquals(2, node.size());
 
-        inviteMember(marlinGroupName, "marlin", "nemo");
-        subscribe(marlinGroupName, "nemo");
+        addMember(marlinGroupName, "nemo", "marlin");
 
         node = listVC("nemo");
         assertEquals(3, node.size());
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
index 52d6b7f..3f1c8c0 100644
--- 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
@@ -57,9 +57,8 @@
     @Test
     public void testShareVC_ByGroupAdmin () throws KustvaktException {
         createMarlinGroup();
-        inviteMember(marlinGroupName, "marlin", "nemo");
-        subscribe(marlinGroupName, "nemo");
-
+        addMember(marlinGroupName, "nemo", "marlin");
+        
         JsonNode node = listRolesByGroup("marlin", marlinGroupName);
         assertEquals(0, node.size());
 
@@ -101,9 +100,8 @@
         assertEquals(5, roleNodes.size());
 
         String memberName = "darla";
-        testInviteMember(groupName, testUser, memberName);
-        subscribe(groupName, memberName);
-
+        testAddMember(groupName, testUser, memberName);
+        
         roleNodes = listRolesByGroup(testUser, groupName, false);
         assertEquals(6, roleNodes.size());
 
@@ -150,8 +148,7 @@
         response = createUserGroup(groupName, "Owid users", testUser);
         assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
 
-        testInviteMember(groupName, testUser, memberName);
-        subscribe(groupName, memberName);
+        testAddMember(groupName, testUser, memberName);
         checkMemberInGroup(memberName, testUser, groupName);
 
         // share vc to group
@@ -193,8 +190,7 @@
         assertEquals(Status.CREATED.getStatusCode(), response.getStatus());
 
         String memberName = "darla";
-        testInviteMember(groupName, testUser, memberName);
-        subscribe(groupName, memberName);
+        testAddMember(groupName, testUser, memberName);
 
         shareVC(testUser, vc1, groupName, testUser);
         shareVC(testUser, vc2, groupName, testUser);