Added delete-config & tests; updated lib & java versions; fixed bugs.

Change-Id: I7a2a7d316b28e856062b678a70d9c0d251ed1ee8
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
index 8fdf556..7b9c48c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
@@ -31,6 +31,10 @@
 
     private String authenticationScheme;
 
+    private boolean isSoftDeleteAutoGroup;
+    private boolean isSoftDeleteGroup;
+    private boolean isSoftDeleteGroupMember;
+
     public FullConfiguration (Properties properties) throws IOException {
         super(properties);
     }
@@ -44,23 +48,35 @@
 
         // EM: pattern for matching availability in Krill matches
         setLicensePatterns(properties);
-
+        setDeleteConfiguration(properties);
         ldapConfig = properties.getProperty("ldap.config");
     }
 
+    private void setDeleteConfiguration (Properties properties) {
+        setSoftDeleteGroup(parseDeleteConfig(properties.getProperty("delete.group", "")));
+        setSoftDeleteAutoGroup(parseDeleteConfig(properties.getProperty("delete.auto.group", "")));
+        setSoftDeleteGroupMember(parseDeleteConfig(
+                properties.getProperty("delete.group.member", "")));
+    }
+
+    private boolean parseDeleteConfig (String deleteConfig) {
+        return deleteConfig.equals("soft") ? true : false;
+    }
+
     private void setLicensePatterns (Properties properties) {
         setFreeLicensePattern(compilePattern(getFreeOnlyRegex()));
-        setPublicLicensePattern(
-                compilePattern(getFreeOnlyRegex() + "|" + getPublicOnlyRegex()));
-        setAllLicensePattern(compilePattern(
-                getFreeOnlyRegex() + "|" + getPublicOnlyRegex() + "|" + getAllOnlyRegex()));
+        setPublicLicensePattern(compilePattern(
+                getFreeOnlyRegex() + "|" + getPublicOnlyRegex()));
+        setAllLicensePattern(compilePattern(getFreeOnlyRegex() + "|"
+                + getPublicOnlyRegex() + "|" + getAllOnlyRegex()));
     }
 
     private void setLicenseRegex (Properties properties) {
         setFreeOnlyRegex(properties.getProperty("availability.regex.free", ""));
         freeRegexList = splitAndAddToList(getFreeOnlyRegex());
 
-        setPublicOnlyRegex(properties.getProperty("availability.regex.public", ""));
+        setPublicOnlyRegex(
+                properties.getProperty("availability.regex.public", ""));
         publicRegexList = splitAndAddToList(getPublicOnlyRegex());
 
         setAllOnlyRegex(properties.getProperty("availability.regex.all", ""));
@@ -76,7 +92,7 @@
                 list.add(s.trim());
             }
         }
-        else{
+        else {
             list = new ArrayList<>(1);
             list.add(regex);
         }
@@ -177,4 +193,28 @@
         this.allOnlyRegex = allOnlyRegex;
     }
 
+    public boolean isSoftDeleteGroup () {
+        return isSoftDeleteGroup;
+    }
+
+    public void setSoftDeleteGroup (boolean isSoftDeleteGroup) {
+        this.isSoftDeleteGroup = isSoftDeleteGroup;
+    }
+
+    public boolean isSoftDeleteGroupMember () {
+        return isSoftDeleteGroupMember;
+    }
+
+    public void setSoftDeleteGroupMember (boolean isSoftDeleteGroupMember) {
+        this.isSoftDeleteGroupMember = isSoftDeleteGroupMember;
+    }
+
+    public boolean isSoftDeleteAutoGroup () {
+        return isSoftDeleteAutoGroup;
+    }
+
+    public void setSoftDeleteAutoGroup (boolean isSoftDeleteAutoGroup) {
+        this.isSoftDeleteAutoGroup = isSoftDeleteAutoGroup;
+    }
+
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
index aeb413e..9803461 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupMemberDao.java
@@ -60,12 +60,18 @@
         entityManager.persist(member);
     }
 
-    public void deleteMember (String userId, int groupId, String deletedBy, boolean isSoftDelete)
-            throws KustvaktException {
+    public void deleteMember (String userId, int groupId, String deletedBy,
+            boolean isSoftDelete) throws KustvaktException {
         ParameterChecker.checkStringValue(userId, "userId");
         ParameterChecker.checkIntegerValue(groupId, "groupId");
 
         UserGroupMember member = retrieveMemberById(userId, groupId);
+        GroupMemberStatus status = member.getStatus();
+        if (isSoftDelete && status.equals(GroupMemberStatus.DELETED)) {
+            throw new KustvaktException(StatusCodes.DB_ENTRY_DELETED,
+                    userId + " has already been deleted from the group.",
+                    userId);
+        }
 
         if (isSoftDelete) {
             member.setStatus(GroupMemberStatus.DELETED);
diff --git a/full/src/main/java/de/ids_mannheim/korap/handlers/EntityDao.java b/full/src/main/java/de/ids_mannheim/korap/handlers/EntityDao.java
index 3d75697..8577a1b 100644
--- a/full/src/main/java/de/ids_mannheim/korap/handlers/EntityDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/handlers/EntityDao.java
@@ -266,12 +266,12 @@
             jlog.error("Could not create user account with username: {}",
                     user.getUsername());
             throw new DatabaseException(e, user.getUsername(), "korap_users",
-                    StatusCodes.ENTRY_EXISTS, "Username exists.",
+                    StatusCodes.DB_ENTRY_EXISTS, "Username exists.",
                     user.getUsername());
         }
         catch (DataAccessException e) {
             throw new DatabaseException(e, user.getUsername(), "korap_users",
-                    StatusCodes.ENTRY_EXISTS, "Username exists.",
+                    StatusCodes.DB_ENTRY_EXISTS, "Username exists.",
                     user.getUsername());
         }
 
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java b/full/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
index 02cff45..15f6052 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/UserGroupService.java
@@ -7,6 +7,7 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import de.ids_mannheim.korap.config.FullConfiguration;
 import de.ids_mannheim.korap.constant.GroupMemberStatus;
 import de.ids_mannheim.korap.constant.PredefinedRole;
 import de.ids_mannheim.korap.constant.UserGroupStatus;
@@ -46,6 +47,8 @@
     private UserGroupConverter converter;
     @Autowired
     private AuthenticationManagerIface authManager;
+    @Autowired
+    private FullConfiguration config;
 
     private static List<Role> memberRoles;
 
@@ -91,7 +94,7 @@
                 break;
             }
         }
-        
+
         return members;
     }
 
@@ -175,7 +178,13 @@
         User user = authManager.getUser(username);
         UserGroup userGroup = userGroupDao.retrieveGroupById(groupId);
         if (userGroup.getCreatedBy().equals(username) || user.isSystemAdmin()) {
-            userGroupDao.deleteGroup(groupId, username, false);
+            // soft delete
+            userGroupDao.deleteGroup(groupId, username,
+                    config.isSoftDeleteGroup());
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
         }
     }
 
@@ -190,7 +199,7 @@
     public void deleteAutoHiddenGroup (int groupId, String deletedBy)
             throws KustvaktException {
         // default hard delete
-        userGroupDao.deleteGroup(groupId, deletedBy, false);
+        userGroupDao.deleteGroup(groupId, deletedBy, config.isSoftDeleteAutoGroup());
     }
 
     /** Adds a user to the specified usergroup. If the username with 
@@ -216,9 +225,10 @@
         ParameterChecker.checkIntegerValue(groupId, "userGroupId");
 
         if (memberExists(username, groupId, status)) {
-            throw new KustvaktException(StatusCodes.ENTRY_EXISTS,
-                    "Username: " + username + " with status " + status
-                            + " exists in usergroup " + userGroup.getName());
+            throw new KustvaktException(StatusCodes.DB_ENTRY_EXISTS,
+                    "Username " + username + " with status " + status
+                            + " exists in user-group " + userGroup.getName(),
+                    username, status.name(), userGroup.getName());
         }
 
         setMemberRoles();
@@ -250,7 +260,7 @@
             return true;
         }
         else if (existingStatus.equals(GroupMemberStatus.DELETED)) {
-            // hard delete
+            // hard delete, not customizable
             groupMemberDao.deleteMember(username, groupId, "system", false);
         }
 
@@ -312,7 +322,8 @@
      */
     public void unsubscribe (int groupId, String username)
             throws KustvaktException {
-        groupMemberDao.deleteMember(username, groupId, username, true);
+        groupMemberDao.deleteMember(username, groupId, username,
+                config.isSoftDeleteGroupMember());
     }
 
 
@@ -340,7 +351,9 @@
         }
         else if (isUserGroupAdmin(deletedBy, userGroup)
                 || user.isSystemAdmin()) {
-            groupMemberDao.deleteMember(memberId, groupId, deletedBy, false);
+            // soft delete
+            groupMemberDao.deleteMember(memberId, groupId, deletedBy,
+                    config.isSoftDeleteGroupMember());
         }
         else {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
index 2ddec6f..ad92c84 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/UserGroupController.java
@@ -111,6 +111,12 @@
         }
     }
 
+    /** Only group owner and system admins can delete groups. 
+     * 
+     * @param securityContext
+     * @param groupId
+     * @return HTTP 200, if successful.
+     */
     @DELETE
     @Path("delete")
     @Consumes(MediaType.APPLICATION_JSON)
diff --git a/full/src/main/resources/kustvakt.conf b/full/src/main/resources/kustvakt.conf
index ee3bb7d..d8ff39d 100644
--- a/full/src/main/resources/kustvakt.conf
+++ b/full/src/main/resources/kustvakt.conf
@@ -17,6 +17,11 @@
 default.layer.d = mate
 default.layer.c = corenlp
 
+## delete configuration (default hard)
+# delete.auto.group = hard
+delete.group = soft
+delete.group.member = soft
+
 ## availability regex
 ## only support |
 availability.regex.free = CC-BY.*
diff --git a/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java b/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
index d9fdca4..d40ee83 100644
--- a/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupDaoTest.java
@@ -13,6 +13,7 @@
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
+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.UserGroupStatus;
@@ -35,10 +36,12 @@
     private VirtualCorpusDao virtualCorpusDao;
     @Autowired
     private RoleDao roleDao;
+    @Autowired
+    private FullConfiguration config;
 
     @Rule
     public ExpectedException thrown = ExpectedException.none();
-
+    
 
     @Test
     public void createDeleteNewUserGroup () throws KustvaktException {
@@ -74,7 +77,7 @@
         assertEquals(0, vc.size());
 
         // soft delete group
-        userGroupDao.deleteGroup(groupId, createdBy, true);
+        userGroupDao.deleteGroup(groupId, createdBy, config.isSoftDeleteGroup());
         group = userGroupDao.retrieveGroupById(groupId);
         assertEquals(UserGroupStatus.DELETED, group.getStatus());
 
diff --git a/full/src/test/java/de/ids_mannheim/korap/misc/KoralNodeTest.java b/full/src/test/java/de/ids_mannheim/korap/misc/KoralNodeTest.java
index 4e097ed..c0c7012 100644
--- a/full/src/test/java/de/ids_mannheim/korap/misc/KoralNodeTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/misc/KoralNodeTest.java
@@ -1,8 +1,12 @@
 package de.ids_mannheim.korap.misc;
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+
 import com.fasterxml.jackson.databind.node.ObjectNode;
+
 import de.ids_mannheim.korap.resource.rewrite.KoralNode;
 import de.ids_mannheim.korap.utils.JsonUtils;
-import org.junit.Test;
 
 /**
  * @author hanl
@@ -18,7 +22,7 @@
         KoralNode knode = KoralNode.wrapNode(node);
         knode.put("value_1", "setting_1");
 
-        System.out.println(knode.rawNode().toString());
+        assertEquals("{\"value_1\":\"setting_1\"}",knode.rawNode().toString());
     }
 
 
@@ -28,7 +32,7 @@
         node.put("value_1", "setting_1");
         KoralNode knode = KoralNode.wrapNode(node);
         knode.remove("value_1", null);
-        System.out.println(knode.rawNode().toString());
+        assertEquals("{}",knode.rawNode().toString());
     }
 
 
@@ -38,7 +42,7 @@
         node.put("value_1", "setting_1");
         KoralNode knode = KoralNode.wrapNode(node);
         knode.replace("value_1", "settings_2", null);
-        System.out.println(knode.rawNode().toString());
+        assertEquals("{\"value_1\":\"settings_2\"}",knode.rawNode().toString());
     }
 
 
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
index edafea8..537f299 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/UserGroupControllerTest.java
@@ -32,6 +32,22 @@
     private HttpAuthorizationHandler handler;
     private String username = "UserGroupControllerTest";
 
+    private JsonNode retrieveUserGroups (String username)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        ClientResponse response = resource().path("group").path("list")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue(username,
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .get(ClientResponse.class);
+        String entity = response.getEntity(String.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        return JsonUtils.readTree(entity);
+    }
+
     // dory is a group admin in dory group
     @Test
     public void testListDoryGroups () throws KustvaktException {
@@ -158,35 +174,16 @@
         assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
                 node.at("/members/1/roles/1").asText());
 
-        testDeleteGroupMemberUnauthorized(groupId);
-        testDeleteGroupMember(groupId);
+
+        testInviteMember(groupId);
+
+        testDeleteMemberUnauthorized(groupId);
+        testDeleteMember(groupId);
         testDeleteGroup(groupId);
     }
 
-    private void testDeleteGroupMemberUnauthorized (String groupId)
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
-        // nemo is a group member
-        ClientResponse response = resource().path("group").path("member")
-                .path("delete").queryParam("memberId", "marlin")
-                .queryParam("groupId", groupId)
-                .header(Attributes.AUTHORIZATION,
-                        handler.createBasicAuthorizationHeaderValue("nemo",
-                                "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .delete(ClientResponse.class);
 
-        String entity = response.getEntity(String.class);
-//        System.out.println(entity);
-        JsonNode node = JsonUtils.readTree(entity);
-        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
-        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
-                node.at("/errors/0/0").asInt());
-        assertEquals("Unauthorized operation for user: nemo",
-                node.at("/errors/0/1").asText());
-    }
-
-    private void testDeleteGroupMember (String groupId)
+    private void testDeleteMember (String groupId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
         // delete marlin from group
@@ -209,13 +206,80 @@
         String entity = response.getEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
         node = node.get(0);
-        assertEquals(2, node.get("members").size());
+        assertEquals(3, node.get("members").size());
         assertEquals("nemo", node.at("/members/1/userId").asText());
         assertEquals(GroupMemberStatus.PENDING.name(),
                 node.at("/members/1/status").asText());
 
     }
 
+    private void testDeleteMemberUnauthorized (String groupId)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        // nemo is a group member
+        ClientResponse response = resource().path("group").path("member")
+                .path("delete").queryParam("memberId", "marlin")
+                .queryParam("groupId", groupId)
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("nemo",
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .delete(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        //        System.out.println(entity);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Unauthorized operation for user: nemo",
+                node.at("/errors/0/1").asText());
+    }
+
+    private void testDeletePendingMember () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        // dory delete pearl
+        ClientResponse response = resource().path("group").path("member")
+                .path("delete").queryParam("memberId", "pearl")
+                // dory group
+                .queryParam("groupId", "2")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("dory",
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .delete(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        // check member
+        JsonNode node = retrieveUserGroups("pearl");
+        assertEquals(0, node.size());
+    }
+
+    @Test
+    public void testDeleteDeletedMember () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        ClientResponse response = resource().path("group").path("member")
+                .path("delete").queryParam("memberId", "pearl")
+                // dory group
+                .queryParam("groupId", "2")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("dory",
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .delete(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        System.out.println(entity);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.DB_ENTRY_DELETED,
+                node.at("/errors/0/0").asInt());
+        assertEquals("pearl has already been deleted from the group.",
+                node.at("/errors/0/1").asText());
+        assertEquals("pearl", node.at("/errors/0/2").asText());
+    }
+
     private void testDeleteGroup (String groupId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
@@ -242,10 +306,32 @@
     }
 
     @Test
+    public void testDeleteGroupUnauthorized () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        // dory is a group admin in marlin group
+        ClientResponse response = resource().path("group").path("delete")
+                .queryParam("groupId", "1")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("dory",
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .delete(ClientResponse.class);
+
+        String entity = response.getEntity(String.class);
+        //        System.out.println(entity);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.UNAUTHORIZED.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.AUTHORIZATION_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Unauthorized operation for user: dory",
+                node.at("/errors/0/1").asText());
+    }
+
+    @Test
     public void testDeleteGroupOwner () throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
         // delete marlin from marlin group
-        // dory is a VCA in marlin group
+        // dory is a group admin in marlin group
         ClientResponse response = resource().path("group").path("member")
                 .path("delete").queryParam("memberId", "marlin")
                 .queryParam("groupId", "1")
@@ -262,95 +348,51 @@
         assertEquals(StatusCodes.NOT_ALLOWED, node.at("/errors/0/0").asInt());
         assertEquals("Operation 'delete group owner'is not allowed.",
                 node.at("/errors/0/1").asText());
-
     }
 
-    //    @Test
-    //    public void testInviteMember () {
-    //
-    //    }
-    //
-    //    @Test
-    //    public void testInviteDeletedMember () {
-    //
-    //    }
-    //    
-    //    @Test
-    //    public void testDeletePendingMember () {
-    //
-    //    }
+    private void testInviteMember (String groupId)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        String[] members = new String[] { "darla" };
 
+        UserGroupJson userGroup = new UserGroupJson();
+        userGroup.setMembers(members);
+        userGroup.setId(Integer.parseInt(groupId));
 
-    // marlin has GroupMemberStatus.PENDING in dory group
-    @Test
-    public void testSubscribeMarlinToDoryGroup () throws KustvaktException {
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        form.add("groupId", "2");
-
-        ClientResponse response = resource().path("group").path("subscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path("group").path("member")
+                .path("invite").type(MediaType.APPLICATION_JSON)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION,
-                        handler.createBasicAuthorizationHeaderValue("marlin",
+                        handler.createBasicAuthorizationHeaderValue(username,
                                 "pass"))
-                .entity(form).post(ClientResponse.class);
+                .entity(userGroup).post(ClientResponse.class);
 
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
-        // retrieve marlin group
-        JsonNode node = retrieveMarlinGroups();
-        System.out.println(node);
-        assertEquals(2, node.size());
-
-        JsonNode group = node.get(1);
-        assertEquals(2, group.at("/id").asInt());
-        assertEquals("dory group", group.at("/name").asText());
-        assertEquals("dory", group.at("/owner").asText());
-        // group members are not allowed to see other members
-        assertEquals(0, group.at("/members").size());
-        assertEquals(GroupMemberStatus.ACTIVE.name(),
-                group.at("/userMemberStatus").asText());
-
-        // unsubscribe marlin from dory group
-        testUnsubscribe(form);
-
-        // invite marlin to dory group to set back the GroupMemberStatus.PENDING
-        testInviteMember();
-    }
-
-    private JsonNode retrieveMarlinGroups () throws UniformInterfaceException,
-            ClientHandlerException, KustvaktException {
-        ClientResponse response = resource().path("group").path("list")
+        // list group
+        response = resource().path("group").path("list")
                 .header(Attributes.AUTHORIZATION,
-                        handler.createBasicAuthorizationHeaderValue("marlin",
+                        handler.createBasicAuthorizationHeaderValue(username,
                                 "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .get(ClientResponse.class);
+
         String entity = response.getEntity(String.class);
 
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        JsonNode node = JsonUtils.readTree(entity);
+        node = node.get(0);
+        assertEquals(4, node.get("members").size());
 
-        return JsonUtils.readTree(entity);
+        assertEquals("darla", node.at("/members/3/userId").asText());
+        assertEquals(GroupMemberStatus.PENDING.name(),
+                node.at("/members/3/status").asText());
+        assertEquals(PredefinedRole.USER_GROUP_MEMBER.name(),
+                node.at("/members/3/roles/0").asText());
+        assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
+                node.at("/members/3/roles/1").asText());
     }
 
-    private void testUnsubscribe (MultivaluedMap<String, String> form)
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
-        ClientResponse response = resource().path("group").path("unsubscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .header(Attributes.AUTHORIZATION,
-                        handler.createBasicAuthorizationHeaderValue("marlin",
-                                "pass"))
-                .entity(form).post(ClientResponse.class);
-
-        assertEquals(Status.OK.getStatusCode(), response.getStatus());
-
-        JsonNode node = retrieveMarlinGroups();
-        assertEquals(1, node.size());
-    }
-
-    private void testInviteMember () throws UniformInterfaceException,
+    private void testInviteDeletedMember () throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
         String[] members = new String[] { "marlin" };
 
@@ -370,7 +412,7 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         // check member
-        JsonNode node = retrieveMarlinGroups();
+        JsonNode node = retrieveUserGroups("marlin");
         assertEquals(2, node.size());
         JsonNode group = node.get(1);
         assertEquals(GroupMemberStatus.PENDING.name(),
@@ -378,9 +420,110 @@
 
     }
 
+    @Test
+    public void testInvitePendingMember () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        // marlin has status PENDING in dory group
+        String[] members = new String[] { "marlin" };
+
+        UserGroupJson userGroup = new UserGroupJson();
+        userGroup.setMembers(members);
+        // dory group
+        userGroup.setId(2);
+
+        ClientResponse response = resource().path("group").path("member")
+                .path("invite").type(MediaType.APPLICATION_JSON)
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("dory",
+                                "pass"))
+                .entity(userGroup).post(ClientResponse.class);
+        String entity = response.getEntity(String.class);
+        //        System.out.println(entity);
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.DB_ENTRY_EXISTS,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Username marlin with status PENDING exists in user-group "
+                + "dory group", node.at("/errors/0/1").asText());
+        assertEquals("[marlin, PENDING, dory group]",
+                node.at("/errors/0/2").asText());
+    }
+
+    @Test
+    public void testInviteDeletedMember2 () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        // pearl has status deleted in dory group
+        String[] members = new String[] { "pearl" };
+
+        UserGroupJson userGroup = new UserGroupJson();
+        userGroup.setMembers(members);
+        // dory group
+        userGroup.setId(2);
+
+        ClientResponse response = resource().path("group").path("member")
+                .path("invite").type(MediaType.APPLICATION_JSON)
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("dory",
+                                "pass"))
+                .entity(userGroup).post(ClientResponse.class);
+
+//        String entity = response.getEntity(String.class);
+//        System.out.println(entity);
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        // check member
+        JsonNode node = retrieveUserGroups("pearl");
+        assertEquals(1, node.size());
+        JsonNode group = node.get(0);
+        assertEquals(GroupMemberStatus.PENDING.name(),
+                group.at("/userMemberStatus").asText());
+
+        testDeletePendingMember();
+    }
+
+
+    // marlin has GroupMemberStatus.PENDING in dory group
+    @Test
+    public void testSubscribePendingMember () throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("groupId", "2");
+
+        ClientResponse response = resource().path("group").path("subscribe")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("marlin",
+                                "pass"))
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        // retrieve marlin group
+        JsonNode node = retrieveUserGroups("marlin");
+        //        System.out.println(node);
+        assertEquals(2, node.size());
+
+        JsonNode group = node.get(1);
+        assertEquals(2, group.at("/id").asInt());
+        assertEquals("dory group", group.at("/name").asText());
+        assertEquals("dory", group.at("/owner").asText());
+        // group members are not allowed to see other members
+        assertEquals(0, group.at("/members").size());
+        assertEquals(GroupMemberStatus.ACTIVE.name(),
+                group.at("/userMemberStatus").asText());
+
+        // unsubscribe marlin from dory group
+        testUnsubscribeActiveMember(form);
+
+        // invite marlin to dory group to set back the GroupMemberStatus.PENDING
+        testInviteDeletedMember();
+    }
+
     // pearl has GroupMemberStatus.DELETED in dory group
     @Test
-    public void testSubscribePearlToDoryGroup () throws KustvaktException {
+    public void testSubscribeDeletedMember () throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("groupId", "2");
 
@@ -464,4 +607,22 @@
                 node.at("/errors/0/1").asText());
     }
 
+    private void testUnsubscribeActiveMember (
+            MultivaluedMap<String, String> form)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        ClientResponse response = resource().path("group").path("unsubscribe")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("marlin",
+                                "pass"))
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        JsonNode node = retrieveUserGroups("marlin");
+        assertEquals(1, node.size());
+    }
+
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
index 6207cb2..0df3720 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/controller/VirtualCorpusControllerTest.java
@@ -170,8 +170,8 @@
             ClientHandlerException, KustvaktException {
         ClientResponse response = resource().path("vc").path("search").path("4")
                 .header(Attributes.AUTHORIZATION,
-                        handler.createBasicAuthorizationHeaderValue(
-                                "gill", "pass"))
+                        handler.createBasicAuthorizationHeaderValue("gill",
+                                "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .get(ClientResponse.class);
         String entity = response.getEntity(String.class);
@@ -274,7 +274,7 @@
                 .get(ClientResponse.class);
         entity = response.getEntity(String.class);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
-//                System.out.println(entity);
+        //                System.out.println(entity);
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(2, node.size());
         assertEquals("new vc", node.get(1).get("name").asText());
@@ -361,9 +361,12 @@
 
         InputStream is = getClass().getClassLoader()
                 .getResourceAsStream("test-user.token");
-        BufferedReader reader = new BufferedReader(new InputStreamReader(is));
 
-        String authToken = reader.readLine();
+        String authToken;
+        try (BufferedReader reader =
+                new BufferedReader(new InputStreamReader(is));) {
+            authToken = reader.readLine();
+        }
 
         ClientResponse response = resource().path("vc").path("create")
                 .header(Attributes.AUTHORIZATION,
@@ -478,14 +481,13 @@
 
     @Test
     public void testDeleteVCUnauthorized () throws KustvaktException {
-        ClientResponse response =
-                resource().path("vc").path("delete").path("1")
-                        .header(Attributes.AUTHORIZATION,
-                                handler.createBasicAuthorizationHeaderValue(
-                                        "VirtualCorpusControllerTest", "pass"))
-                        .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+        ClientResponse response = resource().path("vc").path("delete").path("1")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue(
+                                "VirtualCorpusControllerTest", "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
 
-                        .delete(ClientResponse.class);
+                .delete(ClientResponse.class);
 
         String entity = response.getEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
@@ -582,8 +584,8 @@
 
         checkWWWAuthenticateHeader(response);
     }
-    
-    
+
+
     /**
      * @see VirtualCorpusServiceTest
      * @throws KustvaktException
@@ -591,8 +593,7 @@
     @Test
     public void testEditPublishVC () throws KustvaktException {
 
-        String json =
-                "{\"id\": \"2\", \"type\": \"PUBLISHED\"}";
+        String json = "{\"id\": \"2\", \"type\": \"PUBLISHED\"}";
 
         ClientResponse response = resource().path("vc").path("edit")
                 .header(Attributes.AUTHORIZATION,
@@ -619,13 +620,12 @@
         JsonNode n = node.get(1);
         assertEquals(VirtualCorpusType.PUBLISHED.displayName(),
                 n.get("type").asText());
-        
+
         //check VC access
         // need system admin account
-        
+
         // edit 2nd
-        json =
-                "{\"id\": \"2\", \"type\": \"PROJECT\"}";
+        json = "{\"id\": \"2\", \"type\": \"PROJECT\"}";
 
         response = resource().path("vc").path("edit")
                 .header(Attributes.AUTHORIZATION,
@@ -636,7 +636,7 @@
                 .post(ClientResponse.class, json);
 
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
-        
+
         response = resource().path("vc").path("list").path("user")
                 .header(Attributes.AUTHORIZATION,
                         handler.createBasicAuthorizationHeaderValue("dory",
@@ -663,7 +663,7 @@
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .get(ClientResponse.class);
         String entity = response.getEntity(String.class);
-//                System.out.println(entity);
+        //                System.out.println(entity);
         JsonNode node = JsonUtils.readTree(entity);
         assertEquals(1, node.at("/0/accessId").asInt());
         assertEquals(2, node.at("/0/vcId").asInt());
@@ -720,7 +720,7 @@
         assertEquals("group VC", node.at("/0/vcName").asText());
         assertEquals(2, node.at("/0/userGroupId").asInt());
         assertEquals("dory group", node.at("/0/userGroupName").asText());
-    }   
+    }
 
 
     @Test
@@ -844,11 +844,10 @@
         assertEquals("Unauthorized operation for user: dory",
                 node.at("/errors/0/1").asText());
     }
-    
+
     @Test
-    public void testCreateAccessByNonVCA ()
-            throws UniformInterfaceException, ClientHandlerException,
-            KustvaktException {
+    public void testCreateAccessByNonVCA () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
 
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         // nemo vc
diff --git a/full/src/test/resources/kustvakt-test.conf b/full/src/test/resources/kustvakt-test.conf
index 7ec4381..3d5eadd 100644
--- a/full/src/test/resources/kustvakt-test.conf
+++ b/full/src/test/resources/kustvakt-test.conf
@@ -22,6 +22,11 @@
 default.layer.d = mate
 default.layer.c = corenlp
 
+## delete configuration (default hard)
+# delete.auto.group = hard
+delete.group = soft
+delete.group.member = soft
+
 ## availability regex
 ## only support |
 availability.regex.free = CC-BY.*