Added services: create user group, subscribe and unsubscribe.

Change-Id: Id812bcfdcd602be0401af0b2caaf95364ca47c34
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
index 3422de7..b02dd57 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/UserGroupDao.java
@@ -1,7 +1,6 @@
 package de.ids_mannheim.korap.dao;
 
 import java.util.ArrayList;
-import java.util.Iterator;
 import java.util.List;
 
 import javax.persistence.EntityManager;
@@ -10,7 +9,6 @@
 import javax.persistence.Query;
 import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
-import javax.persistence.criteria.Expression;
 import javax.persistence.criteria.Join;
 import javax.persistence.criteria.ListJoin;
 import javax.persistence.criteria.Predicate;
@@ -62,8 +60,10 @@
         entityManager.persist(group);
 
         List<Role> roles = new ArrayList<Role>(2);
-        roles.add(roleDao.retrieveRoleById(PredefinedRole.USER_GROUP_ADMIN.getId()));
-        roles.add(roleDao.retrieveRoleById(PredefinedRole.VC_ACCESS_ADMIN.getId()));
+        roles.add(roleDao
+                .retrieveRoleById(PredefinedRole.USER_GROUP_ADMIN.getId()));
+        roles.add(roleDao
+                .retrieveRoleById(PredefinedRole.VC_ACCESS_ADMIN.getId()));
 
         UserGroupMember owner = new UserGroupMember();
         owner.setUserId(createdBy);
@@ -81,7 +81,15 @@
         ParameterChecker.checkIntegerValue(groupId, "groupId");
         ParameterChecker.checkStringValue(deletedBy, "deletedBy");
 
-        UserGroup group = retrieveGroupById(groupId);
+        UserGroup group = null;
+        try {
+            group = retrieveGroupById(groupId);
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND,
+                    "groupId: " + groupId);
+        }
+
         if (isSoftDelete) {
             group.setStatus(UserGroupStatus.DELETED);
             group.setDeletedBy(deletedBy);
@@ -203,7 +211,7 @@
 
 
     }
-    
+
     public void addVCToGroup (VirtualCorpus virtualCorpus, String createdBy,
             VirtualCorpusAccessStatus status, UserGroup group) {
         VirtualCorpusAccess accessGroup = new VirtualCorpusAccess();
@@ -223,7 +231,8 @@
         }
     }
 
-    public void deleteVCFromGroup (int virtualCorpusId, int groupId) throws KustvaktException {
+    public void deleteVCFromGroup (int virtualCorpusId, int groupId)
+            throws KustvaktException {
         ParameterChecker.checkIntegerValue(virtualCorpusId, "virtualCorpusId");
         ParameterChecker.checkIntegerValue(groupId, "groupId");
 
@@ -232,16 +241,21 @@
                 criteriaBuilder.createQuery(VirtualCorpusAccess.class);
 
         Root<VirtualCorpusAccess> root = query.from(VirtualCorpusAccess.class);
-        Join<VirtualCorpusAccess, VirtualCorpus> vc = root.join(VirtualCorpusAccess_.virtualCorpus);
-        Join<VirtualCorpusAccess, UserGroup> group = root.join(VirtualCorpusAccess_.userGroup);
+        Join<VirtualCorpusAccess, VirtualCorpus> vc =
+                root.join(VirtualCorpusAccess_.virtualCorpus);
+        Join<VirtualCorpusAccess, UserGroup> group =
+                root.join(VirtualCorpusAccess_.userGroup);
 
-        Predicate virtualCorpus = criteriaBuilder.equal(vc.get(VirtualCorpus_.id), virtualCorpusId);
-        Predicate userGroup = criteriaBuilder.equal(group.get(UserGroup_.id), groupId);
-        
+        Predicate virtualCorpus = criteriaBuilder
+                .equal(vc.get(VirtualCorpus_.id), virtualCorpusId);
+        Predicate userGroup =
+                criteriaBuilder.equal(group.get(UserGroup_.id), groupId);
+
         query.select(root);
         query.where(criteriaBuilder.and(virtualCorpus, userGroup));
         Query q = entityManager.createQuery(query);
-        VirtualCorpusAccess vcAccess = (VirtualCorpusAccess) q.getSingleResult();
+        VirtualCorpusAccess vcAccess =
+                (VirtualCorpusAccess) q.getSingleResult();
         entityManager.remove(vcAccess);
     }
 
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 c0cced2..3b4e355 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
@@ -3,6 +3,7 @@
 import java.util.List;
 
 import javax.persistence.EntityManager;
+import javax.persistence.NoResultException;
 import javax.persistence.PersistenceContext;
 import javax.persistence.Query;
 import javax.persistence.criteria.CriteriaBuilder;
@@ -20,6 +21,7 @@
 import de.ids_mannheim.korap.entity.UserGroupMember;
 import de.ids_mannheim.korap.entity.UserGroupMember_;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.utils.ParameterChecker;
 
 @Transactional
@@ -49,6 +51,11 @@
         ParameterChecker.checkIntegerValue(groupId, "groupId");
 
         UserGroupMember member = retrieveMemberById(userId, groupId);
+        if (member.getStatus().equals(GroupMemberStatus.DELETED)) {
+            throw new KustvaktException(StatusCodes.NOTHING_CHANGED, "Username "
+                    + userId + " had been deleted in group " + groupId, userId);
+        }
+
         member.setStatus(GroupMemberStatus.ACTIVE);
         entityManager.persist(member);
     }
@@ -58,13 +65,14 @@
         ParameterChecker.checkStringValue(userId, "userId");
         ParameterChecker.checkIntegerValue(groupId, "groupId");
 
-        UserGroupMember m = retrieveMemberById(userId, groupId);
+        UserGroupMember member = retrieveMemberById(userId, groupId);
+
         if (isSoftDelete) {
-            m.setStatus(GroupMemberStatus.DELETED);
-            entityManager.persist(m);
+            member.setStatus(GroupMemberStatus.DELETED);
+            entityManager.persist(member);
         }
         else {
-            entityManager.remove(m);
+            entityManager.remove(member);
         }
     }
 
@@ -88,11 +96,20 @@
         query.select(root);
         query.where(predicate);
         Query q = entityManager.createQuery(query);
-        return (UserGroupMember) q.getSingleResult();
+
+        try {
+            return (UserGroupMember) q.getSingleResult();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND,
+                    "Username " + userId + " is not found in group " + groupId,
+                    userId);
+        }
+
     }
 
-    public List<UserGroupMember> retrieveMemberByRole (int groupId,
-            int roleId) {
+    public List<UserGroupMember> retrieveMemberByRole (int groupId, int roleId)
+            throws KustvaktException {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<UserGroupMember> query =
                 criteriaBuilder.createQuery(UserGroupMember.class);
@@ -110,10 +127,19 @@
         query.select(root);
         query.where(predicate);
         Query q = entityManager.createQuery(query);
-        return q.getResultList();
+        try {
+            return q.getResultList();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(
+                    StatusCodes.NO_RESULT_FOUND, "No member with role " + roleId
+                            + " is found in group " + groupId,
+                    String.valueOf(roleId));
+        }
     }
 
-    public List<UserGroupMember> retrieveMemberByGroupId (int groupId) {
+    public List<UserGroupMember> retrieveMemberByGroupId (int groupId)
+            throws KustvaktException {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<UserGroupMember> query =
                 criteriaBuilder.createQuery(UserGroupMember.class);
@@ -129,6 +155,14 @@
         query.select(root);
         query.where(predicate);
         Query q = entityManager.createQuery(query);
-        return q.getResultList();
+
+        try {
+            return q.getResultList();
+        }
+        catch (NoResultException e) {
+            throw new KustvaktException(StatusCodes.NO_RESULT_FOUND,
+                    "No member is found in group " + groupId,
+                    String.valueOf(groupId));
+        }
     }
 }
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 9c18314..f704668 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
@@ -6,14 +6,19 @@
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
+import de.ids_mannheim.korap.constant.GroupMemberStatus;
 import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.constant.UserGroupStatus;
+import de.ids_mannheim.korap.dao.RoleDao;
 import de.ids_mannheim.korap.dao.UserGroupDao;
 import de.ids_mannheim.korap.dao.UserGroupMemberDao;
 import de.ids_mannheim.korap.dto.UserGroupDto;
 import de.ids_mannheim.korap.dto.converter.UserGroupConverter;
+import de.ids_mannheim.korap.entity.Role;
 import de.ids_mannheim.korap.entity.UserGroup;
 import de.ids_mannheim.korap.entity.UserGroupMember;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.web.input.UserGroupJson;
 
 @Service
 public class UserGroupService {
@@ -23,22 +28,30 @@
     @Autowired
     private UserGroupMemberDao groupMemberDao;
     @Autowired
+    private RoleDao roleDao;
+    @Autowired
     private UserGroupConverter converter;
 
 
+    /** Only USER_GROUP_ADMINs are allowed to see the members of the group.
+     * 
+     * @param username username
+     * @return a list of usergroups
+     * @throws KustvaktException
+     */
     public List<UserGroupDto> retrieveUserGroup (String username)
             throws KustvaktException {
 
         List<UserGroup> userGroups =
                 userGroupDao.retrieveGroupByUserId(username);
-        
+
         ArrayList<UserGroupDto> dtos = new ArrayList<>(userGroups.size());
-        
+
         List<UserGroupMember> groupAdmins;
         for (UserGroup group : userGroups) {
             groupAdmins = groupMemberDao.retrieveMemberByRole(group.getId(),
                     PredefinedRole.USER_GROUP_ADMIN.getId());
-            
+
             List<UserGroupMember> members = null;
             for (UserGroupMember admin : groupAdmins) {
                 if (admin.getUserId().equals(username)) {
@@ -53,4 +66,63 @@
         return dtos;
     }
 
+
+    /** Group owner is automatically added when creating a group. 
+     *  Do not include owners in group members. 
+     *  
+     *  USER_GROUP_MEMBER and VC_ACCESS_MEMBER roles are automatically 
+     *  assigned to each group member. 
+     *  
+     *  USER_GROUP_MEMBER cannot see other group members and may remove 
+     *  himself from the group.
+     *   
+     *  VC_ACCESS_MEMBER can only read group VC.
+     * 
+     * @see /full/src/main/resources/db/predefined/V3.2__insert_predefined_roles.sql
+     * 
+     * @param groupJson UserGroupJson object from json
+     * @param username the user creating the group
+     * @throws KustvaktException
+     * 
+     * 
+     */
+    public void createUserGroup (UserGroupJson groupJson, String username)
+            throws KustvaktException {
+
+        int groupId = userGroupDao.createGroup(groupJson.getName(), username,
+                UserGroupStatus.ACTIVE);
+        UserGroup group = userGroupDao.retrieveGroupById(groupId);
+
+        List<Role> roles = new ArrayList<Role>(2);
+        roles.add(roleDao
+                .retrieveRoleById(PredefinedRole.USER_GROUP_MEMBER.getId()));
+        roles.add(roleDao
+                .retrieveRoleById(PredefinedRole.VC_ACCESS_MEMBER.getId()));
+
+        UserGroupMember m;
+        for (String memberId : groupJson.getMembers()) {
+            if (memberId.equals(username)) {
+                // skip owner, already added while creating group.
+                continue;
+            }
+
+            m = new UserGroupMember();
+            m.setUserId(memberId);
+            m.setCreatedBy(username);
+            m.setGroup(group);
+            m.setStatus(GroupMemberStatus.PENDING);
+            m.setRoles(roles);
+        }
+    }
+
+    public void subscribe (int groupId, String username)
+            throws KustvaktException {
+        groupMemberDao.approveMember(username, groupId);
+    }
+
+
+    public void unsubscribe (int groupId, String username)
+            throws KustvaktException {
+        groupMemberDao.deleteMember(username, groupId, true);
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java b/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java
index 8916477..03c7091 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/VirtualCorpusService.java
@@ -28,7 +28,7 @@
 import de.ids_mannheim.korap.utils.KoralCollectionQueryBuilder;
 import de.ids_mannheim.korap.web.SearchKrill;
 import de.ids_mannheim.korap.web.controller.VirtualCorpusController;
-import de.ids_mannheim.korap.web.input.VirtualCorpusFromJson;
+import de.ids_mannheim.korap.web.input.VirtualCorpusJson;
 
 /** VirtualCorpusService handles the logic behind {@link VirtualCorpusController}. 
  *  It communicates with {@link VirtualCorpusDao} and returns DTO to  
@@ -54,7 +54,7 @@
     @Autowired
     private VirtualCorpusConverter converter;
 
-    public void storeVC (VirtualCorpusFromJson vc, String username)
+    public void storeVC (VirtualCorpusJson vc, String username)
             throws KustvaktException {
 
         User user = authManager.getUser(username);
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 7f888b1..8f51287 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
@@ -2,7 +2,10 @@
 
 import java.util.List;
 
+import javax.ws.rs.Consumes;
+import javax.ws.rs.FormParam;
 import javax.ws.rs.GET;
+import javax.ws.rs.POST;
 import javax.ws.rs.Path;
 import javax.ws.rs.Produces;
 import javax.ws.rs.core.Context;
@@ -27,6 +30,7 @@
 import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
 import de.ids_mannheim.korap.web.filter.DemoUserFilter;
 import de.ids_mannheim.korap.web.filter.PiwikFilter;
+import de.ids_mannheim.korap.web.input.UserGroupJson;
 
 @Controller
 @Path("group")
@@ -42,10 +46,10 @@
     private FullResponseHandler responseHandler;
     @Autowired
     private UserGroupService service;
-    
+
     @GET
-    @Path("user")
-    public Response getUserGroup (@Context SecurityContext securityContext){
+    @Path("list")
+    public Response getUserGroup (@Context SecurityContext securityContext) {
         String result;
         TokenContext context =
                 (TokenContext) securityContext.getUserPrincipal();
@@ -66,4 +70,95 @@
         }
         return Response.ok(result).build();
     }
+
+
+    /** Creates a user group where the user in token context is the 
+     * group owner, and assigns the listed group members with status 
+     * GroupMemberStatus.PENDING. 
+     * 
+     * Invitations must be sent to these proposed members. If a member
+     * accepts the invitation, update his GroupMemberStatus to 
+     * GroupMemberStatus.ACTIVE by using 
+     * {@link UserGroupController#subscribeToGroup(SecurityContext, String)}.
+     * 
+     * If he rejects the invitation, update his GroupMemberStatus 
+     * to GroupMemberStatus.DELETED using 
+     * {@link UserGroupController#unsubscribeFromGroup(SecurityContext, String)}.
+     * 
+     *  
+     * 
+     * @param securityContext
+     * @param group UserGroupJson
+     * @return if successful, HTTP response status OK
+     */
+    @POST
+    @Path("create")
+    @Consumes(MediaType.APPLICATION_JSON)
+    public Response createGroup (@Context SecurityContext securityContext,
+            UserGroupJson group) {
+        TokenContext context =
+                (TokenContext) securityContext.getUserPrincipal();
+        try {
+            if (context.isDemo()) {
+                throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                        "Operation is not permitted for user: "
+                                + context.getUsername(),
+                        context.getUsername());
+            }
+
+            service.createUserGroup(group, context.getUsername());
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+        return Response.ok().build();
+    }
+
+
+    @POST
+    @Path("subscribe")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response subscribeToGroup (@Context SecurityContext securityContext,
+            @FormParam("groupId") int groupId) {
+        TokenContext context =
+                (TokenContext) securityContext.getUserPrincipal();
+        try {
+            if (context.isDemo()) {
+                throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                        "Operation is not permitted for user: "
+                                + context.getUsername(),
+                        context.getUsername());
+            }
+
+            service.subscribe(groupId, context.getUsername());
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+        return Response.ok().build();
+    }
+
+    @POST
+    @Path("unsubscribe")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response unsubscribeFromGroup (
+            @Context SecurityContext securityContext,
+            @FormParam("groupId") int groupId) {
+        TokenContext context =
+                (TokenContext) securityContext.getUserPrincipal();
+        try {
+            if (context.isDemo()) {
+                throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                        "Operation is not permitted for user: "
+                                + context.getUsername(),
+                        context.getUsername());
+            }
+
+            service.unsubscribe(groupId, context.getUsername());
+        }
+        catch (KustvaktException e) {
+            throw responseHandler.throwit(e);
+        }
+        return Response.ok().build();
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java b/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
index 3f122f3..f6b460c 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/controller/VirtualCorpusController.java
@@ -2,6 +2,7 @@
 
 import java.util.List;
 
+import javax.ws.rs.Consumes;
 import javax.ws.rs.DELETE;
 import javax.ws.rs.GET;
 import javax.ws.rs.POST;
@@ -26,12 +27,11 @@
 import de.ids_mannheim.korap.service.VirtualCorpusService;
 import de.ids_mannheim.korap.user.TokenContext;
 import de.ids_mannheim.korap.utils.JsonUtils;
-import de.ids_mannheim.korap.utils.ParameterChecker;
 import de.ids_mannheim.korap.web.FullResponseHandler;
 import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
 import de.ids_mannheim.korap.web.filter.DemoUserFilter;
 import de.ids_mannheim.korap.web.filter.PiwikFilter;
-import de.ids_mannheim.korap.web.input.VirtualCorpusFromJson;
+import de.ids_mannheim.korap.web.input.VirtualCorpusJson;
 
 @Controller
 @Path("vc")
@@ -50,14 +50,10 @@
 
     @POST
     @Path("store")
+    @Consumes("application/json")
     public Response storeVC (@Context SecurityContext securityContext,
-            String json) {
+            VirtualCorpusJson vc) {
         try {
-            ParameterChecker.checkStringValue(json, "json string");
-
-            // create vc object from json
-            VirtualCorpusFromJson vc =
-                    JsonUtils.convertToClass(json, VirtualCorpusFromJson.class);
             jlog.debug(vc.toString());
 
             // get user info
@@ -119,6 +115,14 @@
     //        return Response.ok().build();
     //    }
 
+    /** Only VC owner and system admin can delete VCs. VC-access admins 
+     *  can delete VC-accesses e.g. of project VCs, but not the VCs 
+     *  themselves. 
+     * 
+     * @param securityContext
+     * @param vcId
+     * @return HTTP status 200, if successful
+     */
     @DELETE
     @Path("delete")
     public Response deleteVC (@Context SecurityContext securityContext,
@@ -139,5 +143,7 @@
         }
         return Response.ok().build();
     }
+    
+    
 
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/input/UserGroupJson.java b/full/src/main/java/de/ids_mannheim/korap/web/input/UserGroupJson.java
new file mode 100644
index 0000000..13f3fdd
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/web/input/UserGroupJson.java
@@ -0,0 +1,14 @@
+package de.ids_mannheim.korap.web.input;
+
+import java.util.List;
+
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class UserGroupJson {
+
+    private String name;
+    private List<String> members;
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/web/input/VirtualCorpusFromJson.java b/full/src/main/java/de/ids_mannheim/korap/web/input/VirtualCorpusJson.java
similarity index 96%
rename from full/src/main/java/de/ids_mannheim/korap/web/input/VirtualCorpusFromJson.java
rename to full/src/main/java/de/ids_mannheim/korap/web/input/VirtualCorpusJson.java
index d6ddbd0..58379e9 100644
--- a/full/src/main/java/de/ids_mannheim/korap/web/input/VirtualCorpusFromJson.java
+++ b/full/src/main/java/de/ids_mannheim/korap/web/input/VirtualCorpusJson.java
@@ -8,7 +8,7 @@
 
 @Getter
 @Setter
-public class VirtualCorpusFromJson {
+public class VirtualCorpusJson {
 
     private String name;
     private VirtualCorpusType type;
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/service/full/UserGroupServiceTest.java b/full/src/test/java/de/ids_mannheim/korap/web/service/full/UserGroupServiceTest.java
index 1fde8bd..a0db38b 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/service/full/UserGroupServiceTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/service/full/UserGroupServiceTest.java
@@ -2,15 +2,17 @@
 
 import static org.junit.Assert.assertEquals;
 
+import javax.ws.rs.core.MediaType;
+import javax.ws.rs.core.MultivaluedMap;
+
 import org.eclipse.jetty.http.HttpHeaders;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import com.fasterxml.jackson.databind.JsonNode;
-import com.sun.jersey.api.client.ClientHandlerException;
 import com.sun.jersey.api.client.ClientResponse;
-import com.sun.jersey.api.client.UniformInterfaceException;
 import com.sun.jersey.api.client.ClientResponse.Status;
+import com.sun.jersey.core.util.MultivaluedMapImpl;
 
 import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
 import de.ids_mannheim.korap.config.Attributes;
@@ -27,7 +29,7 @@
     // dory is a group admin in dory group
     @Test
     public void testRetrieveDoryGroups () throws KustvaktException {
-        ClientResponse response = resource().path("group").path("user")
+        ClientResponse response = resource().path("group").path("list")
                 .header(Attributes.AUTHORIZATION,
                         handler.createBasicAuthorizationHeaderValue("dory",
                                 "pass"))
@@ -35,19 +37,19 @@
                 .get(ClientResponse.class);
         String 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(1, node.at("/0/id").asInt());
         assertEquals("dory group", node.at("/0/name").asText());
         assertEquals("dory", node.at("/0/owner").asText());
         assertEquals(3, node.at("/0/members").size());
     }
-    
+
     // nemo is a group member in dory group
     @Test
     public void testRetrieveNemoGroups () throws KustvaktException {
-        ClientResponse response = resource().path("group").path("user")
+        ClientResponse response = resource().path("group").path("list")
                 .header(Attributes.AUTHORIZATION,
                         handler.createBasicAuthorizationHeaderValue("nemo",
                                 "pass"))
@@ -55,9 +57,76 @@
                 .get(ClientResponse.class);
         String 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(1, node.at("/0/id").asInt());
+        assertEquals("dory group", node.at("/0/name").asText());
+        assertEquals("dory", node.at("/0/owner").asText());
+        // group members are not allowed to see other members
+        assertEquals(0, node.at("/0/members").size());
+    }
+
+    // marlin does not have any group
+    @Test
+    public void testRetrieveMarlinGroups () throws KustvaktException {
+        ClientResponse response = resource().path("group").path("list")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("marlin",
+                                "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);
+        assertEquals(0, node.size());
+    }
+
+    
+    @Test
+    public void testRetrieveUserGroupUnauthorized () throws KustvaktException {
+        ClientResponse response = resource().path("group").path("list")
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .get(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("Operation is not permitted for user: guest",
+                node.at("/errors/0/1").asText());
+    }
+
+    // marlin has GroupMemberStatus.PENDING in dory group
+    @Test
+    public void testSubscribeMarlinToDoryGroup () throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("groupId", "1");
+
+        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);
+        String entity = response.getEntity(String.class);
         
+        // retrieve marlin group
+        response = resource().path("group").path("list")
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("marlin",
+                                "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .get(ClientResponse.class);
+        entity = response.getEntity(String.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+        
+        JsonNode node = JsonUtils.readTree(entity);
+        assertEquals(1, node.size());
         assertEquals(1, node.at("/0/id").asInt());
         assertEquals("dory group", node.at("/0/name").asText());
         assertEquals("dory", node.at("/0/owner").asText());
@@ -65,20 +134,90 @@
         assertEquals(0, node.at("/0/members").size());
     }
     
+    // pearl has GroupMemberStatus.DELETED in dory group
     @Test
-    public void testRetrieveUserGroupUnauthorized () throws KustvaktException {
-        ClientResponse response = resource().path("group").path("user")
+    public void testSubscribePearlToDoryGroup () throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("groupId", "1");
+
+        ClientResponse response = resource().path("group").path("subscribe")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
-                .get(ClientResponse.class);
+                .header(Attributes.AUTHORIZATION,
+                        handler.createBasicAuthorizationHeaderValue("pearl",
+                                "pass"))
+                .entity(form).post(ClientResponse.class);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+        
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.NOTHING_CHANGED,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Username pearl had been deleted in group 1",
+                node.at("/errors/0/1").asText());
+    }
+    
+    @Test
+    public void testSubscribeMissingGroupId () throws KustvaktException {
+        ClientResponse response = resource().path("group").path("subscribe")
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(Attributes.AUTHORIZATION, handler
+                        .createBasicAuthorizationHeaderValue("bruce", "pass"))
+                .post(ClientResponse.class);
+        String entity = response.getEntity(String.class);
+        JsonNode node = JsonUtils.readTree(entity);
+
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.INVALID_ARGUMENT,
+                node.at("/errors/0/0").asInt());
+        assertEquals("groupId", node.at("/errors/0/1").asText());
+        assertEquals("0", node.at("/errors/0/2").asText());
+    }
+
+    @Test
+    public void testSubscribeNonExistentMember () throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("groupId", "1");
+
+        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("bruce",
+                                "pass"))
+                .entity(form).post(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,
+
+        assertEquals(Status.BAD_REQUEST.getStatusCode(), response.getStatus());
+        assertEquals(StatusCodes.NO_RESULT_FOUND,
                 node.at("/errors/0/0").asInt());
-        assertEquals("Operation is not permitted for user: guest",
+        assertEquals("Username bruce is not found in group 1",
                 node.at("/errors/0/1").asText());
-        
     }
+
+    @Test
+    public void testSubscribeToNonExistentGroup () throws KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("groupId", "100");
+
+        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("pearl",
+                                "pass"))
+                .entity(form).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.NO_RESULT_FOUND,
+                node.at("/errors/0/0").asInt());
+        assertEquals("Username pearl is not found in group 100",
+                node.at("/errors/0/1").asText());
+    }
+
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/service/full/VirtualCorpusServiceTest.java b/full/src/test/java/de/ids_mannheim/korap/web/service/full/VirtualCorpusServiceTest.java
index 6c5cd74..b487099 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/service/full/VirtualCorpusServiceTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/service/full/VirtualCorpusServiceTest.java
@@ -11,6 +11,7 @@
 import java.util.Map.Entry;
 import java.util.Set;
 
+import org.apache.http.entity.ContentType;
 import org.eclipse.jetty.http.HttpHeaders;
 import org.junit.Test;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -100,9 +101,11 @@
                 .header(Attributes.AUTHORIZATION,
                         handler.createBasicAuthorizationHeaderValue(
                                 "test class", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").entity(json)
-                .post(ClientResponse.class);
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+                .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
+                .post(ClientResponse.class, json);
         String entity = response.getEntity(String.class);
+        System.out.println(entity);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         // retrieve user VC