Fixed user group member invitation and subscription tests (#763).

Disable soft delete group (see kustvakt-test.conf).

Change-Id: I7b0e8e5a3730020faa77ab9567b803e16176132e
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 4b7bd19..f3e6c34 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
@@ -121,10 +121,10 @@
         testDeleteMemberUnauthorized(groupName);
         testDeleteMember(groupName);
         testDeleteGroup(groupName);
-        testSubscribeToDeletedGroup(groupName);
-        testUnsubscribeToDeletedGroup(groupName);
+//        testSubscribeToDeletedGroup(groupName);
+//        testUnsubscribeToDeletedGroup(groupName);
     }
-
+    
     private void testUpdateUserGroup (String groupName)
             throws ProcessingException, KustvaktException {
         String description = "Description is updated.";
@@ -280,6 +280,7 @@
     @Test
     public void testDeleteGroupOwner ()
             throws ProcessingException, KustvaktException {
+        createMarlinGroup();
         // delete marlin from marlin-group
         // dory is a group admin in marlin-group
         Response response = target().path(API_VERSION).path("group")
@@ -294,375 +295,6 @@
         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");
     }
-
-    private void testInviteMember (String groupName, String invitor,
-            String invitee)
-            throws ProcessingException, KustvaktException {
-        inviteMember(groupName, invitor, invitee);
-        // list group
-        JsonNode node = listUserGroups(invitor);
-        node = node.get(0);
-        assertEquals(2, node.get("members").size());
-        assertEquals(node.at("/members/1/userId").asText(), invitee);
-        assertEquals(GroupMemberStatus.PENDING.name(),
-                node.at("/members/1/status").asText());
-        assertEquals(0, node.at("/members/1/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 = listUserGroups("marlin");
-        assertEquals(1, node.size());
-        JsonNode group = node.get(0);
-        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 = listUserGroups("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 {
-        createDoryGroup();
-        testInviteMember(doryGroupName, "dory", "marlin");
-        subscribe(doryGroupName, "marlin");
-        
-        // retrieve marlin group
-        JsonNode node = listUserGroups("marlin");
-        assertEquals(1, node.size());
-        JsonNode group = node.get(0);
-        assertEquals(group.at("/name").asText(), "dory-group");
-        assertEquals(group.at("/owner").asText(), "dory");
-        // group members are not allowed to see other members
-        assertEquals(0, group.at("/members").size());
-        assertEquals(GroupMemberStatus.ACTIVE.name(),
-                group.at("/userMemberStatus").asText());
-        
-        System.out.println(node.toPrettyString());
-        assertEquals(2, group.at("/userRoles").size());
-        // 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 = listUserGroups("marlin");
-        assertEquals(0, 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 = listUserGroups("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 = listUserGroups("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.");
-    }
-
-
 }
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 0b30126..41acbb9 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
@@ -35,8 +35,8 @@
     }
     
     public void testListNemoGroups () throws KustvaktException {
-        subscribe(doryGroupName, "nemo");
-
+        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");
@@ -47,8 +47,8 @@
     // marlin has 2 groups
     public void testListMarlinGroups () throws KustvaktException {
         createMarlinGroup();
-        subscribe(doryGroupName, "marlin");
-        
+        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 d8bc63f..e29b3d7 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
@@ -8,6 +8,7 @@
 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;
@@ -16,6 +17,8 @@
 import de.ids_mannheim.korap.entity.Role;
 import de.ids_mannheim.korap.entity.UserGroupMember;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
 import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Entity;
 import jakarta.ws.rs.core.Form;
@@ -28,6 +31,74 @@
     private UserGroupMemberDao memberDao;
 
     @Test
+    public void testInvitePendingMember ()
+            throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        inviteMember(doryGroupName, "dory", "marlin");
+        
+        // marlin has status PENDING in dory-group
+        Response response = inviteMember(doryGroupName, "dory", "marlin");
+        String entity = response.readEntity(String.class);
+        // System.out.println(entity);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.GROUP_MEMBER_EXISTS,
+                node.at("/errors/0/0").asInt());
+        assertEquals(
+                "Username marlin with status PENDING exists in the user-group "
+                        + "dory-group",
+                node.at("/errors/0/1").asText());
+        assertEquals(node.at("/errors/0/2").asText(),
+                "[marlin, PENDING, dory-group]");
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+
+    @Test
+    public void testInviteActiveMember ()
+            throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        inviteMember(doryGroupName, "dory", "nemo");
+        subscribe(doryGroupName, "nemo");
+        // nemo has status active in dory-group
+        Form form = new Form();
+        form.param("members", "nemo");
+        Response response = target().path(API_VERSION).path("group")
+                .path("@dory-group").path("invite").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("dory", "pass"))
+                .post(Entity.form(form));
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.GROUP_MEMBER_EXISTS,
+                node.at("/errors/0/0").asInt());
+        assertEquals(
+                "Username nemo with status ACTIVE exists in the user-group "
+                        + "dory-group",
+                node.at("/errors/0/1").asText());
+        assertEquals(node.at("/errors/0/2").asText(),
+                "[nemo, ACTIVE, dory-group]");
+        
+        deleteGroupByName(doryGroupName, "dory");
+        
+        testInviteMemberToDeletedGroup();
+    }
+
+    private void testInviteMemberToDeletedGroup () throws KustvaktException {
+        Response response = inviteMember(doryGroupName, "dory", "nemo");
+
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+//        String entity = response.readEntity(String.class);
+//        JsonNode node = JsonUtils.readTree(entity);
+//        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
+//        assertEquals(node.at("/errors/0/1").asText(),
+//                "Group deleted-group has been deleted.");
+//        assertEquals(node.at("/errors/0/2").asText(), "deleted-group");
+    }
+    
+    @Test
     public void testAddMemberRole () throws KustvaktException {
         createMarlinGroup();
         inviteMember(marlinGroupName, "marlin", "dory");
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupSubscriptionTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupSubscriptionTest.java
new file mode 100644
index 0000000..b08a27f
--- /dev/null
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupSubscriptionTest.java
@@ -0,0 +1,269 @@
+package de.ids_mannheim.korap.web.controller.usergroup;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import org.junit.jupiter.api.Test;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.google.common.net.HttpHeaders;
+
+import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.Attributes;
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
+import de.ids_mannheim.korap.utils.JsonUtils;
+import jakarta.ws.rs.ProcessingException;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.core.Form;
+import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.core.Response.Status;
+
+public class UserGroupSubscriptionTest extends UserGroupTestBase {
+    
+    @Test
+    public void testSubscribeNonExistentMember () throws KustvaktException {
+        createDoryGroup();
+        
+        Response response = subscribe(doryGroupName, "bruce");
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.GROUP_MEMBER_NOT_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "bruce is not found in the group");
+        
+        testSubscribeDeletedMember();
+        deleteGroupByName(doryGroupName, "dory");
+    }
+    
+    // pearl has GroupMemberStatus.DELETED in dory-group
+    private void testSubscribeDeletedMember () throws KustvaktException {
+        inviteMember(doryGroupName, "dory", "pearl");
+        // delete pending member
+        deleteMember(doryGroupName, "pearl", "dory");
+        
+        Response response = subscribe(doryGroupName, "pearl");
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.GROUP_MEMBER_DELETED,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "pearl has already been deleted from the group dory-group");
+        
+        testUnsubscribeDeletedMember();
+        testInviteDeletedMember("pearl", "dory");
+    }
+    
+    // marlin has GroupMemberStatus.PENDING in dory-group
+    @Test
+    public void testSubscribePendingMember () throws KustvaktException {
+        createDoryGroup();
+        testInviteMember(doryGroupName, "dory", "marlin");
+        Response response = subscribe(doryGroupName, "marlin");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        // retrieve marlin group
+        JsonNode node = listUserGroups("marlin");
+        assertEquals(1, node.size());
+        JsonNode group = node.get(0);
+        assertEquals(group.at("/name").asText(), "dory-group");
+        assertEquals(group.at("/owner").asText(), "dory");
+        // group members are not allowed to see other members
+        assertEquals(0, group.at("/members").size());
+        assertEquals(GroupMemberStatus.ACTIVE.name(),
+                group.at("/userMemberStatus").asText());
+        
+        assertEquals(2, group.at("/userRoles").size());
+        
+        // unsubscribe marlin from dory-group
+        testUnsubscribeActiveMember("dory-group");
+        checkGroupMemberRole("dory-group", "marlin");
+        testInviteDeletedMember("marlin", "dory");
+        
+        deleteGroupByName(doryGroupName, "dory");
+    }
+    
+    private void testInviteDeletedMember (String invitee, String invitor)
+            throws ProcessingException, KustvaktException {
+        
+        Response response = inviteMember(doryGroupName, invitor, invitee);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        // check member
+        JsonNode node = listUserGroups(invitee);
+        assertEquals(1, node.size());
+        JsonNode group = node.get(0);
+        assertEquals(GroupMemberStatus.PENDING.name(),
+                group.at("/userMemberStatus").asText());
+//        testDeletePendingMember();
+    }
+    
+    private void checkGroupMemberRole (String groupName,
+            String deletedMemberName) throws KustvaktException {
+        Response response = target().path(API_VERSION).path("admin")
+                .path("group").path("@" + groupName).request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("admin", "pass"))
+                .post(null);
+        String entity = response.readEntity(String.class);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(entity).at("/members");
+        JsonNode member;
+        for (int i = 0; i < node.size(); i++) {
+            member = node.get(i);
+            if (deletedMemberName.equals(member.at("/userId").asText())) {
+                System.out.println(deletedMemberName);
+                assertEquals(0, node.at("/roles").size());
+                break;
+            }
+        }
+    }
+    
+
+    @Test
+    public void testSubscribeMissingGroupName () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("subscribe").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("bruce", "pass"))
+                .post(Entity.form(new Form()));
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+    }
+
+
+    @Test
+    public void testSubscribeToNonExistentGroup () throws KustvaktException {
+        Response response = subscribe("non-existent", "pearl");
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "Group non-existent is not found");
+    }
+
+    @Test
+    public void testSubscribeToDeletedGroup ()
+            throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        // hard delete
+        deleteGroupByName(doryGroupName, "dory");
+        
+        Response response = subscribe(doryGroupName, "nemo");
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+//        String entity = response.readEntity(String.class);
+//        JsonNode node = JsonUtils.readTree(entity);
+//        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
+//        assertEquals(node.at("/errors/0/1").asText(),
+//                "Group new-user-group has been deleted.");
+        testUnsubscribeToDeletedGroup(doryGroupName);
+        
+    }
+
+    private void testUnsubscribeToDeletedGroup (String groupName)
+            throws ProcessingException, KustvaktException {
+        Response response = unsubscribe(doryGroupName, "nemo");
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+//        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+//        String entity = response.readEntity(String.class);
+//        JsonNode node = JsonUtils.readTree(entity);
+//        assertEquals(StatusCodes.GROUP_DELETED, node.at("/errors/0/0").asInt());
+//        assertEquals(node.at("/errors/0/1").asText(),
+//                "Group new-user-group has been deleted.");
+    }
+
+    private void testUnsubscribeActiveMember (String groupName)
+            throws ProcessingException, KustvaktException {
+        Response response = unsubscribe(groupName, "marlin");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = listUserGroups("marlin");
+        assertEquals(0, node.size());
+    }
+    
+    @Test
+    public void testUnsubscribePendingMember ()
+            throws ProcessingException, KustvaktException {
+        createDoryGroup();
+        testInviteMember(doryGroupName, "dory", "marlin");
+        JsonNode node = listUserGroups("marlin");
+        assertEquals(1, node.size());
+
+        Response response = unsubscribe(doryGroupName, "marlin");
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        node = listUserGroups("marlin");
+        assertEquals(0, node.size());
+        // invite marlin to dory-group to set back the
+        // GroupMemberStatus.PENDING
+        testInviteDeletedMember("marlin","dory");
+        deleteGroupByName(doryGroupName, "dory");
+    }
+
+    private void testUnsubscribeDeletedMember ()
+            throws ProcessingException, KustvaktException {
+        // pearl unsubscribes from dory-group
+        Response response = unsubscribe(doryGroupName, "pearl");
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.GROUP_MEMBER_DELETED,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "pearl has already been deleted from the group dory-group");
+        assertEquals(node.at("/errors/0/2").asText(), "[pearl, dory-group]");
+    }
+
+
+    @Test
+    public void testUnsubscribeMissingGroupName () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("unsubscribe").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
+                .delete();
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+    }
+
+    @Test
+    public void testUnsubscribeNonExistentMember () throws KustvaktException {
+        createDoryGroup();
+        Response response = target().path(API_VERSION).path("group")
+                .path("@dory-group").path("unsubscribe").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("bruce", "pass"))
+                .delete();
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.GROUP_MEMBER_NOT_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "bruce is not found in the group");
+        deleteGroupByName(doryGroupName, "dory");
+    }
+
+    @Test
+    public void testUnsubscribeToNonExistentGroup () throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("@tralala-group").path("unsubscribe").request()
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("pearl", "pass"))
+                .delete();
+        assertEquals(Status.NOT_FOUND.getStatusCode(), response.getStatus());
+        String entity = response.readEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(StatusCodes.NO_RESOURCE_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals(node.at("/errors/0/1").asText(),
+                "Group tralala-group is not found");
+    }
+
+}
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupTestBase.java b/src/test/java/de/ids_mannheim/korap/web/controller/usergroup/UserGroupTestBase.java
index 86fe916..a1a4ad9 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
@@ -8,6 +8,7 @@
 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.exceptions.KustvaktException;
 import de.ids_mannheim.korap.utils.JsonUtils;
 import jakarta.ws.rs.ProcessingException;
@@ -55,7 +56,7 @@
         return node;
     }
 
-    protected void inviteMember (String groupName, String invitor,
+    protected Response inviteMember (String groupName, String invitor,
             String invitee) throws KustvaktException {
         Form form = new Form();
         form.param("members", invitee);
@@ -65,20 +66,47 @@
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(invitor, "pass"))
                 .post(Entity.form(form));
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        return response;
+    }
+    
+    protected void testInviteMember (String groupName, String invitor,
+            String invitee)
+            throws ProcessingException, KustvaktException {
+        Response response = inviteMember(groupName, invitor, invitee);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
-
+        // list group
+        JsonNode node = listUserGroups(invitor);
+        node = node.get(0);
+        assertEquals(2, node.get("members").size());
+        assertEquals(node.at("/members/1/userId").asText(), invitee);
+        assertEquals(GroupMemberStatus.PENDING.name(),
+                node.at("/members/1/status").asText());
+        assertEquals(0, node.at("/members/1/roles").size());
     }
 
-    protected void subscribe (String groupName, String username)
+    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());
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        return response;
     }
     
+    protected Response unsubscribe (String groupName, String username)
+            throws KustvaktException {
+        Response response = target().path(API_VERSION).path("group")
+                .path("@" + groupName).path("unsubscribe").request()
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue(username, "pass"))
+                .delete();
+        return response;
+//        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+    }
+
     protected void addMemberRole (String groupName, String username,
             Form form) throws KustvaktException {
         Response response = target().path(API_VERSION).path("group")
@@ -89,6 +117,16 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
     }
 
+    protected void 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());
+    }
+    
     protected JsonNode createDoryGroup ()
             throws ProcessingException, KustvaktException {
         Response response = createUserGroup(doryGroupName,
diff --git a/src/test/resources/kustvakt-test.conf b/src/test/resources/kustvakt-test.conf
index f10d3e8..317b720 100644
--- a/src/test/resources/kustvakt-test.conf
+++ b/src/test/resources/kustvakt-test.conf
@@ -52,7 +52,7 @@
 # Delete configuration (default hard)
 #
 # delete.auto.group = hard
-delete.group = soft
+#delete.group = soft
 delete.group.member = soft
 
 # Virtual corpus and queries