Handled unique constraints.

Change-Id: I0ee968b18b028ded76af04a0473c7997a13f2f11
diff --git a/core/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java b/core/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
index 84dfaf7..8e9826b 100644
--- a/core/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
+++ b/core/src/main/java/de/ids_mannheim/korap/config/KustvaktConfiguration.java
@@ -99,11 +99,9 @@
         currentVersion = properties.getProperty("current.api.version", "v1.0");
         String supportedVersions =
                 properties.getProperty("supported.api.version", "");
-        if (supportedVersions.isEmpty()){
-            supportedVersions = currentVersion;
-        }
         this.supportedVersions = Arrays.stream(supportedVersions.split(" "))
                 .collect(Collectors.toSet());
+        this.supportedVersions.add(currentVersion);
 
         baseURL = properties.getProperty("kustvakt.base.url", "/api/*");
         maxhits = new Integer(properties.getProperty("maxhits", "50000"));
diff --git a/full/Changes b/full/Changes
index b81509d..91f4520 100644
--- a/full/Changes
+++ b/full/Changes
@@ -7,6 +7,9 @@
     - Fixed SpringJerseyTest ApplicationContext (margaretha)
     - Handled VCRef with username in VirtualCorpusRewrite (margaretha)
     - Enabled VCReferenceTest in maven test suite (margaretha)
+18/09/2018
+    - Handled unique constraints (margaretha)
+    - Added a controller for editing user group member roles (margaretha)
 
 # version 0.61.1
 28/08/2018
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java b/full/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java
index fc86f52..21216a8 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/NamedVCLoader.java
@@ -16,11 +16,8 @@
 
 import de.ids_mannheim.korap.KrillCollection;
 import de.ids_mannheim.korap.constant.VirtualCorpusType;
-import de.ids_mannheim.korap.dao.VirtualCorpusDao;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
-import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.service.VirtualCorpusService;
-import de.ids_mannheim.korap.user.User.CorpusAccess;
 import de.ids_mannheim.korap.util.QueryException;
 import de.ids_mannheim.korap.web.SearchKrill;
 
@@ -31,8 +28,6 @@
     @Autowired
     private SearchKrill searchKrill;
     @Autowired
-    private VirtualCorpusDao vcDao;
-    @Autowired
     private VirtualCorpusService vcService;
 
     private static Logger jlog = LogManager.getLogger(NamedVCLoader.class);
@@ -44,7 +39,8 @@
         String json = IOUtils.toString(is, "utf-8");
         if (json != null) {
             cacheVC(json, filename);
-            storeVC(filename, json);
+            vcService.storeVC(filename, VirtualCorpusType.SYSTEM, json, null,
+                    null, null, true, "system");
         }
     }
 
@@ -66,7 +62,8 @@
             String json = readFile(file, filename);
             if (json != null) {
                 cacheVC(json, filename);
-                storeVC(filename, json);
+                vcService.storeVC(filename, VirtualCorpusType.SYSTEM, json, null,
+                        null, null, true, "system");
             }
         }
     }
@@ -113,17 +110,4 @@
                 + KrillCollection.cache.calculateInMemorySize());
     }
 
-    private void storeVC (String name, String koralQuery)
-            throws KustvaktException {
-        if (!VirtualCorpusService.wordPattern.matcher(name).matches()) {
-            throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
-                    "Virtual corpus name must only contains letters, numbers, "
-                            + "underscores, hypens and spaces",
-                    name);
-        }
-        CorpusAccess requiredAccess =
-                vcService.determineRequiredAccess(koralQuery);
-        vcDao.createVirtualCorpus(name, VirtualCorpusType.SYSTEM,
-                requiredAccess, koralQuery, null, null, null, true, "system");
-    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
index 8898685..bd24b21 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/RoleDao.java
@@ -1,6 +1,8 @@
 package de.ids_mannheim.korap.dao;
 
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 import javax.persistence.EntityManager;
 import javax.persistence.PersistenceContext;
@@ -78,8 +80,7 @@
         return (Role) q.getSingleResult();
     }
 
-    @SuppressWarnings("unchecked")
-    public List<Role> retrieveRoleByGroupMemberId (int userId) {
+    public Set<Role> retrieveRoleByGroupMemberId (int userId) {
         CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
         CriteriaQuery<Role> query = criteriaBuilder.createQuery(Role.class);
 
@@ -91,7 +92,9 @@
         query.where(criteriaBuilder.equal(memberRole.get(UserGroupMember_.id),
                 userId));
         Query q = entityManager.createQuery(query);
-        return q.getResultList();
+        @SuppressWarnings("unchecked")
+        List<Role> resultList = q.getResultList();
+        return new HashSet<Role>(resultList);
     }
 
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/entity/Role.java b/full/src/main/java/de/ids_mannheim/korap/entity/Role.java
index 1474316..0eaf767 100644
--- a/full/src/main/java/de/ids_mannheim/korap/entity/Role.java
+++ b/full/src/main/java/de/ids_mannheim/korap/entity/Role.java
@@ -16,9 +16,10 @@
 import lombok.Getter;
 import lombok.Setter;
 
-/** Describes user roles for example in managing a group or 
- *  virtual corpora of a group.
- *  
+/**
+ * Describes user roles for example in managing a group or
+ * virtual corpora of a group.
+ * 
  * @author margaretha
  * @see Privilege
  */
@@ -26,7 +27,7 @@
 @Getter
 @Entity
 @Table(name = "role")
-public class Role implements Comparable<Role>{
+public class Role implements Comparable<Role> {
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     private int id;
@@ -46,12 +47,29 @@
 
     @Override
     public int compareTo (Role o) {
-        if (this.getId() > o.getId()){
+        if (this.getId() > o.getId()) {
             return 1;
         }
-        else if (this.getId() < o.getId()){
+        else if (this.getId() < o.getId()) {
             return -1;
         }
         return 0;
     }
+
+    @Override
+    public boolean equals (Object obj) {
+        Role r = (Role) obj;
+        if (this.id == r.getId() && this.name.equals(r.getName())) {
+            return true;
+        }
+        return false;
+    }
+    
+    @Override
+    public int hashCode () {
+        int hash = 7;
+        hash = 31 * hash + (int) id;
+        hash = 31 * hash + (name == null ? 0 : name.hashCode());
+        return hash;
+    }
 }
diff --git a/full/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2Scope.java b/full/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2Scope.java
index c44cc53..0dc20d5 100644
--- a/full/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2Scope.java
+++ b/full/src/main/java/de/ids_mannheim/korap/oauth2/constant/OAuth2Scope.java
@@ -24,7 +24,8 @@
     DELETE_USER_GROUP_MEMBER, 
     ADD_USER_GROUP_MEMBER, 
     
-    ADD_USER_GROUP_MEMBER_ROLE, 
+    EDIT_USER_GROUP_MEMBER_ROLE,
+    ADD_USER_GROUP_MEMBER_ROLE,
     DELETE_USER_GROUP_MEMBER_ROLE, 
            
     CREATE_VC, 
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 fdcc4ab..9133169 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
@@ -32,7 +32,9 @@
 import de.ids_mannheim.korap.web.controller.UserGroupController;
 import de.ids_mannheim.korap.web.input.UserGroupJson;
 
-/** UserGroupService defines the logic behind user group web controller.
+/**
+ * UserGroupService defines the logic behind user group web
+ * controller.
  * 
  * @see UserGroupController
  * 
@@ -42,8 +44,7 @@
 @Service
 public class UserGroupService {
 
-    private static Logger jlog =
-            LogManager.getLogger(UserGroupService.class);
+    private static Logger jlog = LogManager.getLogger(UserGroupService.class);
     @Autowired
     private UserGroupDao userGroupDao;
     @Autowired
@@ -61,10 +62,12 @@
 
     private static Set<Role> memberRoles;
 
-    /** Only users with {@link PredefinedRole#USER_GROUP_ADMIN} 
+    /**
+     * Only users with {@link PredefinedRole#USER_GROUP_ADMIN}
      * are allowed to see the members of the group.
      * 
-     * @param username username
+     * @param username
+     *            username
      * @return a list of usergroups
      * @throws KustvaktException
      * 
@@ -166,24 +169,27 @@
         }
     }
 
-    /** Group owner is automatically added when creating a group. 
-     *  Do not include owners in group members. 
-     *  
-     *  {@link PredefinedRole#USER_GROUP_MEMBER} and 
-     *  {@link PredefinedRole#VC_ACCESS_MEMBER} roles are 
-     *  automatically assigned to each group member. 
-     *  
-     *  {@link PredefinedRole#USER_GROUP_MEMBER} restrict users 
-     *  to see other group members and allow users to remove 
-     *  themselves from the groups.
-     *   
-     *  {@link PredefinedRole#VC_ACCESS_MEMBER} allow user to 
-     *  read group VC.
+    /**
+     * Group owner is automatically added when creating a group.
+     * Do not include owners in group members.
+     * 
+     * {@link PredefinedRole#USER_GROUP_MEMBER} and
+     * {@link PredefinedRole#VC_ACCESS_MEMBER} roles are
+     * automatically assigned to each group member.
+     * 
+     * {@link PredefinedRole#USER_GROUP_MEMBER} restrict users
+     * to see other group members and allow users to remove
+     * themselves from the groups.
+     * 
+     * {@link PredefinedRole#VC_ACCESS_MEMBER} allow user to
+     * read group VC.
      * 
      * @see /full/src/main/resources/db/predefined/V3.2__insert_predefined_roles.sql
      * 
-     * @param groupJson UserGroupJson object from json
-     * @param createdBy the user creating the group
+     * @param groupJson
+     *            UserGroupJson object from json
+     * @param createdBy
+     *            the user creating the group
      * @throws KustvaktException
      * 
      * 
@@ -240,19 +246,26 @@
                 config.isSoftDeleteAutoGroup());
     }
 
-    /** Adds a user to the specified usergroup. If the username with 
-     *  {@link GroupMemberStatus} DELETED exists as a member of the group, 
-     *  the entry will be deleted first, and a new entry will be added.
-     *  
-     *  If a username with other statuses exists, a KustvaktException will 
-     *  be thrown.    
+    /**
+     * Adds a user to the specified usergroup. If the username with
+     * {@link GroupMemberStatus} DELETED exists as a member of the
+     * group,
+     * the entry will be deleted first, and a new entry will be added.
+     * 
+     * If a username with other statuses exists, a KustvaktException
+     * will
+     * be thrown.
      * 
      * @see GroupMemberStatus
      * 
-     * @param username a username
-     * @param userGroup a user group
-     * @param createdBy the user (VCA/system) adding the user the user-group 
-     * @param status the status of the membership
+     * @param username
+     *            a username
+     * @param userGroup
+     *            a user group
+     * @param createdBy
+     *            the user (VCA/system) adding the user the user-group
+     * @param status
+     *            the status of the membership
      * @throws KustvaktException
      */
     public void inviteGroupMember (String username, UserGroup userGroup,
@@ -357,11 +370,15 @@
         return false;
     }
 
-    /** Updates the {@link GroupMemberStatus} of a pending member 
-     * to {@link GroupMemberStatus#ACTIVE} and add default member roles.
+    /**
+     * Updates the {@link GroupMemberStatus} of a pending member
+     * to {@link GroupMemberStatus#ACTIVE} and add default member
+     * roles.
      * 
-     * @param groupId groupId
-     * @param username the username of the group member
+     * @param groupId
+     *            groupId
+     * @param username
+     *            the username of the group member
      * @throws KustvaktException
      */
     public void acceptInvitation (int groupId, String username)
@@ -452,14 +469,19 @@
         }
     }
 
-    /** Updates the {@link GroupMemberStatus} of a member to 
+    /**
+     * Updates the {@link GroupMemberStatus} of a member to
      * {@link GroupMemberStatus#DELETED}
      * 
-     * @param userId user to be deleted
-     * @param groupId user-group id
-     * @param deletedBy user that issue the delete 
-     * @param isSoftDelete true if database entry is to be deleted 
-     * permanently, false otherwise
+     * @param userId
+     *            user to be deleted
+     * @param groupId
+     *            user-group id
+     * @param deletedBy
+     *            user that issue the delete
+     * @param isSoftDelete
+     *            true if database entry is to be deleted
+     *            permanently, false otherwise
      * @throws KustvaktException
      */
     private void doDeleteMember (String username, int groupId, String deletedBy,
@@ -495,8 +517,9 @@
 
     }
 
-    public void addMemberRoles (String username, int groupId,
-            String memberUsername, List<Integer> roleIds) throws KustvaktException {
+    public void editMemberRoles (String username, int groupId,
+            String memberUsername, List<Integer> roleIds)
+            throws KustvaktException {
 
         ParameterChecker.checkIntegerValue(groupId, "groupId");
         ParameterChecker.checkStringValue(username, "username");
@@ -504,9 +527,49 @@
 
         UserGroup userGroup = userGroupDao.retrieveGroupById(groupId, true);
         UserGroupStatus groupStatus = userGroup.getStatus();
-        if (groupStatus == UserGroupStatus.DELETED){
+        if (groupStatus == UserGroupStatus.DELETED) {
             throw new KustvaktException(StatusCodes.GROUP_DELETED,
-                    "Usergroup has been deleted."); 
+                    "Usergroup has been deleted.");
+        }
+        else if (isUserGroupAdmin(username, userGroup)
+                || adminDao.isAdmin(username)) {
+
+            UserGroupMember member =
+                    groupMemberDao.retrieveMemberById(memberUsername, groupId);
+
+            if (!member.getStatus().equals(GroupMemberStatus.ACTIVE)) {
+                throw new KustvaktException(StatusCodes.GROUP_MEMBER_INACTIVE,
+                        memberUsername + " has status " + member.getStatus(),
+                        memberUsername, member.getStatus().name());
+            }
+
+            Set<Role> roles = new HashSet<>();
+            for (int i = 0; i < roleIds.size(); i++) {
+                roles.add(roleDao.retrieveRoleById(roleIds.get(i)));
+            }
+            member.setRoles(roles);
+            groupMemberDao.updateMember(member);
+
+        }
+        else {
+            throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
+                    "Unauthorized operation for user: " + username, username);
+        }
+    }
+
+    public void addMemberRoles (String username, int groupId,
+            String memberUsername, List<Integer> roleIds)
+            throws KustvaktException {
+
+        ParameterChecker.checkIntegerValue(groupId, "groupId");
+        ParameterChecker.checkStringValue(username, "username");
+        ParameterChecker.checkStringValue(memberUsername, "memberUsername");
+
+        UserGroup userGroup = userGroupDao.retrieveGroupById(groupId, true);
+        UserGroupStatus groupStatus = userGroup.getStatus();
+        if (groupStatus == UserGroupStatus.DELETED) {
+            throw new KustvaktException(StatusCodes.GROUP_DELETED,
+                    "Usergroup has been deleted.");
         }
         else if (isUserGroupAdmin(username, userGroup)
                 || adminDao.isAdmin(username)) {
@@ -535,7 +598,8 @@
     }
 
     public void deleteMemberRoles (String username, int groupId,
-            String memberUsername, List<Integer> roleIds) throws KustvaktException {
+            String memberUsername, List<Integer> roleIds)
+            throws KustvaktException {
 
         ParameterChecker.checkIntegerValue(groupId, "groupId");
         ParameterChecker.checkStringValue(username, "username");
@@ -551,8 +615,8 @@
 
             Set<Role> roles = member.getRoles();
             Iterator<Role> i = roles.iterator();
-            while (i.hasNext()){
-                if (roleIds.contains(i.next().getId())){
+            while (i.hasNext()) {
+                if (roleIds.contains(i.next().getId())) {
                     i.remove();
                 }
             }
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 33af77b..5d2ce4b 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
@@ -1,5 +1,6 @@
 package de.ids_mannheim.korap.service;
 
+import java.sql.SQLException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Iterator;
@@ -212,7 +213,7 @@
         // check if hidden access exists
         if (access == null) {
             VirtualCorpus vc = vcDao.retrieveVCById(vcId);
-            // create and assign a hidden group
+            // create and assign a new hidden group
             int groupId = userGroupService.createAutoHiddenGroup(vcId);
             UserGroup autoHidden =
                     userGroupService.retrieveUserGroupById(groupId);
@@ -220,18 +221,28 @@
                     VirtualCorpusAccessStatus.HIDDEN);
         }
         else {
+            // should not happened
             jlog.error("Cannot publish VC with id: " + vcId
-                    + ". There have been hidden accesses for the VC already.");
+                    + ". Hidden access exists! Access id: " + access.getId());
         }
     }
 
     public int storeVC (VirtualCorpusJson vc, String username)
             throws KustvaktException {
-        ParameterChecker.checkStringValue(vc.getName(), "name");
-        ParameterChecker.checkObjectValue(vc.getType(), "type");
         ParameterChecker.checkStringValue(vc.getCorpusQuery(), "corpusQuery");
+        String koralQuery = serializeCorpusQuery(vc.getCorpusQuery());
 
-        String name = vc.getName();
+        return storeVC(vc.getName(), vc.getType(), koralQuery,
+                vc.getDefinition(), vc.getDescription(), vc.getStatus(),
+                vc.isCached(), username);
+    }
+
+    public int storeVC (String name, VirtualCorpusType type, String koralQuery,
+            String definition, String description, String status,
+            boolean isCached, String username) throws KustvaktException {
+        ParameterChecker.checkStringValue(name, "name");
+        ParameterChecker.checkObjectValue(type, "type");
+
         if (!wordPattern.matcher(name).matches()) {
             throw new KustvaktException(StatusCodes.INVALID_ARGUMENT,
                     "Virtual corpus name must only contains letters, numbers, "
@@ -239,20 +250,36 @@
                     name);
         }
 
-        if (vc.getType().equals(VirtualCorpusType.SYSTEM)
+        if (type.equals(VirtualCorpusType.SYSTEM) 
+                && !username.equals("system")
                 && !adminDao.isAdmin(username)) {
             throw new KustvaktException(StatusCodes.AUTHORIZATION_FAILED,
                     "Unauthorized operation for user: " + username, username);
         }
 
-        String koralQuery = serializeCorpusQuery(vc.getCorpusQuery());
         CorpusAccess requiredAccess = determineRequiredAccess(koralQuery);
 
-        int vcId = vcDao.createVirtualCorpus(vc.getName(), vc.getType(),
-                requiredAccess, koralQuery, vc.getDefinition(),
-                vc.getDescription(), vc.getStatus(), vc.isCached(), username);
+        int vcId = 0;
+        try {
+            vcId = vcDao.createVirtualCorpus(name, type, requiredAccess,
+                    koralQuery, definition, description, status, isCached,
+                    username);
 
-        if (vc.getType().equals(VirtualCorpusType.PUBLISHED)) {
+        }
+        catch (Exception e) {
+            Throwable cause = e;
+            Throwable lastCause = null;
+            while ((cause = cause.getCause()) != null
+                    && !cause.equals(lastCause)) {
+                if (cause instanceof SQLException) {
+                    break;
+                }
+                lastCause = cause;
+            }
+            throw new KustvaktException(StatusCodes.DB_INSERT_FAILED,
+                    cause.getMessage());
+        }
+        if (type.equals(VirtualCorpusType.PUBLISHED)) {
             publishVC(vcId);
         }
         // EM: should this return anything?
@@ -326,8 +353,23 @@
                     "Unauthorized operation for user: " + username, username);
         }
         else {
-            accessDao.createAccessToVC(vc, userGroup, username,
-                    VirtualCorpusAccessStatus.ACTIVE);
+            try {
+                accessDao.createAccessToVC(vc, userGroup, username,
+                        VirtualCorpusAccessStatus.ACTIVE);
+            }
+            catch (Exception e) {
+                Throwable cause = e;
+                Throwable lastCause = null;
+                while ((cause = cause.getCause()) != null
+                        && !cause.equals(lastCause)) {
+                    if (cause instanceof SQLException) {
+                        break;
+                    }
+                    lastCause = cause;
+                }
+                throw new KustvaktException(StatusCodes.DB_INSERT_FAILED,
+                        cause.getMessage());
+            }
             vcDao.editVirtualCorpus(vc, null, VirtualCorpusType.PUBLISHED, null,
                     null, null, null, null);
         }
@@ -417,8 +459,8 @@
 
     }
 
-    public VirtualCorpus searchVCByName (String username, String vcName, String createdBy)
-            throws KustvaktException {
+    public VirtualCorpus searchVCByName (String username, String vcName,
+            String createdBy) throws KustvaktException {
         VirtualCorpus vc = vcDao.retrieveVCByName(vcName, createdBy);
         checkVCAccess(vc, username);
         return vc;
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 3d9aeb9..6c9427d 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
@@ -24,12 +24,13 @@
 import de.ids_mannheim.korap.constant.UserGroupStatus;
 import de.ids_mannheim.korap.dto.UserGroupDto;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.oauth2.constant.OAuth2Scope;
 import de.ids_mannheim.korap.oauth2.service.OAuth2ScopeService;
 import de.ids_mannheim.korap.security.context.TokenContext;
 import de.ids_mannheim.korap.service.UserGroupService;
-import de.ids_mannheim.korap.web.KustvaktResponseHandler;
 import de.ids_mannheim.korap.web.APIVersionFilter;
+import de.ids_mannheim.korap.web.KustvaktResponseHandler;
 import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
 import de.ids_mannheim.korap.web.filter.BlockingFilter;
 import de.ids_mannheim.korap.web.filter.PiwikFilter;
@@ -267,6 +268,35 @@
         }
     }
 
+    @POST
+    @Path("member/role/edit")
+    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
+    public Response editMemberRoles (@Context SecurityContext securityContext,
+            @FormParam("groupId") int groupId,
+            @FormParam("memberUsername") String memberUsername,
+            @FormParam("roleIds") List<Integer> roleIds,
+            @PathParam("version") String version) {
+        double v = Double.valueOf(version.substring(1, version.length()));
+        if (v < 1.1) {
+            throw kustvaktResponseHandler.throwit(new KustvaktException(
+                    StatusCodes.UNSUPPORTED_API_VERSION,
+                    "Method is not supported in version " + version, version));
+        }
+
+        TokenContext context =
+                (TokenContext) securityContext.getUserPrincipal();
+        try {
+            scopeService.verifyScope(context,
+                    OAuth2Scope.EDIT_USER_GROUP_MEMBER_ROLE);
+            service.editMemberRoles(context.getUsername(), groupId,
+                    memberUsername, roleIds);
+            return Response.ok().build();
+        }
+        catch (KustvaktException e) {
+            throw kustvaktResponseHandler.throwit(e);
+        }
+    }
+
     /**
      * Adds roles of an active member of a user-group. Only user-group
      * admins
diff --git a/full/src/main/resources/db/new-mysql/V1.1__create_virtual_corpus_tables.sql b/full/src/main/resources/db/new-mysql/V1.1__create_virtual_corpus_tables.sql
index 55f2163..eb9d1df 100644
--- a/full/src/main/resources/db/new-mysql/V1.1__create_virtual_corpus_tables.sql
+++ b/full/src/main/resources/db/new-mysql/V1.1__create_virtual_corpus_tables.sql
@@ -64,6 +64,7 @@
   corpus_query TEXT NOT NULL,
   definition varchar(255) DEFAULT NULL,
   is_cached BOOLEAN DEFAULT 0,
+  UNIQUE INDEX unique_index (name,created_by),
   INDEX owner_index (created_by),
   INDEX type_index (type)
 );
diff --git a/full/src/main/resources/db/new-sqlite/V1.1__create_virtual_corpus_tables.sql b/full/src/main/resources/db/new-sqlite/V1.1__create_virtual_corpus_tables.sql
index 56fc105..0547591 100644
--- a/full/src/main/resources/db/new-sqlite/V1.1__create_virtual_corpus_tables.sql
+++ b/full/src/main/resources/db/new-sqlite/V1.1__create_virtual_corpus_tables.sql
@@ -78,6 +78,8 @@
 
 CREATE INDEX virtual_corpus_owner_index ON virtual_corpus(created_by);
 CREATE INDEX virtual_corpus_type_index ON virtual_corpus(type);
+CREATE UNIQUE INDEX  virtual_corpus_unique_name 
+	ON virtual_corpus(name,created_by);
 
 CREATE TABLE IF NOT EXISTS virtual_corpus_access (
   id INTEGER PRIMARY KEY AUTOINCREMENT,
@@ -97,6 +99,6 @@
 
 CREATE INDEX virtual_corpus_status_index 
 	ON virtual_corpus_access(status);
-CREATE INDEX virtual_corpus_access_unique_index 
+CREATE UNIQUE INDEX virtual_corpus_access_unique_index 
 	ON virtual_corpus_access(virtual_corpus_id,user_group_id);
 
diff --git a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
index db238ab..e0c06d6 100644
--- a/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
+++ b/full/src/main/resources/db/new-sqlite/V1.4__oauth2_tables.sql
@@ -20,8 +20,6 @@
 	   REFERENCES oauth2_client_url(url_hashcode)
 );
 
-CREATE UNIQUE INDEX client_id_index on oauth2_client(id);
-
 CREATE TABLE IF NOT EXISTS oauth2_access_scope (
 	id VARCHAR(255) PRIMARY KEY NOT NULL
 );
diff --git a/full/src/main/resources/kustvakt.conf b/full/src/main/resources/kustvakt.conf
index 3932b82..c5a4958 100644
--- a/full/src/main/resources/kustvakt.conf
+++ b/full/src/main/resources/kustvakt.conf
@@ -12,7 +12,7 @@
 ldap.config = file-path-to-ldap-config
 
 # Kustvakt
-current.api.version = v1.0
+current.api.version = v1.1
 # multiple versions separated by space
 supported.api.version = v1.0
 
diff --git a/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java b/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
index 959dce2..60fb14a 100644
--- a/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/config/SpringJerseyTest.java
@@ -23,7 +23,7 @@
 @ContextConfiguration("classpath:test-config.xml")
 public abstract class SpringJerseyTest extends JerseyTest {
 
-    public final static String API_VERSION = "v1.0";
+    public final static String API_VERSION = "v1.1";
 
     @Autowired
     protected GenericApplicationContext applicationContext;
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 1cdd7a5..d1fda36 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
@@ -70,12 +70,15 @@
         assertEquals(createdBy, m.getUserId());
 
         // member roles
-        List<Role> roles = roleDao.retrieveRoleByGroupMemberId(m.getId());
+        Set<Role> roles = roleDao.retrieveRoleByGroupMemberId(m.getId());
         assertEquals(2, roles.size());
+        ArrayList<Role> roleList = new ArrayList<>(2);
+        roleList.addAll(roles);
+        Collections.sort(roleList);
         assertEquals(PredefinedRole.USER_GROUP_ADMIN.getId(),
-                roles.get(0).getId());
+                roleList.get(0).getId());
         assertEquals(PredefinedRole.VC_ACCESS_ADMIN.getId(),
-                roles.get(1).getId());
+                roleList.get(1).getId());
 
         //retrieve VC by group
         List<VirtualCorpus> vc = virtualCorpusDao.retrieveVCByGroup(groupId);
diff --git a/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupMemberDaoTest.java b/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupMemberDaoTest.java
index ff86c80..8b38d7b 100644
--- a/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupMemberDaoTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/dao/UserGroupMemberDaoTest.java
@@ -3,6 +3,7 @@
 import static org.junit.Assert.assertEquals;
 
 import java.util.List;
+import java.util.Set;
 
 import org.junit.Test;
 import org.junit.runner.RunWith;
@@ -11,6 +12,7 @@
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 
 import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.entity.Role;
 import de.ids_mannheim.korap.entity.UserGroupMember;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 
@@ -21,13 +23,31 @@
     @Autowired
     private UserGroupMemberDao dao;
 
+    @Autowired
+    private RoleDao roleDao;
+
     @Test
     public void testRetrieveMemberByRole () throws KustvaktException {
         // dory group
         List<UserGroupMember> vcaAdmins = dao.retrieveMemberByRole(2,
                 PredefinedRole.VC_ACCESS_ADMIN.getId());
-//        System.out.println(vcaAdmins);
+        // System.out.println(vcaAdmins);
         assertEquals(1, vcaAdmins.size());
         assertEquals("dory", vcaAdmins.get(0).getUserId());
     }
+
+    @Test
+    public void testAddSameMemberRole () throws KustvaktException {
+        UserGroupMember member = dao.retrieveMemberById("dory", 1);
+        Set<Role> roles = member.getRoles();
+        Role adminRole = roleDao
+                .retrieveRoleById(PredefinedRole.USER_GROUP_ADMIN.getId());
+        roles.add(adminRole);
+        member.setRoles(roles);
+        dao.updateMember(member);
+
+        member = dao.retrieveMemberById("dory", 1);
+        member.getRoles();
+        assertEquals(2, roles.size());
+    }
 }
diff --git a/full/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusDaoTest.java b/full/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusDaoTest.java
index e15a518..5b7c9d7 100644
--- a/full/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusDaoTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/dao/VirtualCorpusDaoTest.java
@@ -5,6 +5,8 @@
 import java.util.Iterator;
 import java.util.List;
 
+import javax.persistence.PersistenceException;
+
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
@@ -61,21 +63,32 @@
         dao.retrieveVCById(id);
     }
 
+    @Test
+    public void testNonUniqueVC () throws KustvaktException {
+        thrown.expect(PersistenceException.class);
+        thrown.expectMessage("could not execute statement");
+        
+        dao.createVirtualCorpus("system VC", VirtualCorpusType.SYSTEM,
+                User.CorpusAccess.FREE, "corpusSigle=GOE", "definition",
+                "description", "experimental", false, "system");
+    }
 
     @Test
     public void retrieveSystemVC () throws KustvaktException {
-        List<VirtualCorpus> vc = dao.retrieveVCByType(VirtualCorpusType.SYSTEM, null);
+        List<VirtualCorpus> vc =
+                dao.retrieveVCByType(VirtualCorpusType.SYSTEM, null);
         assertEquals(1, vc.size());
     }
 
-
-    /** retrieve private and group VC
+    /**
+     * retrieve private and group VC
+     * 
      * @throws KustvaktException
      */
     @Test
     public void retrieveVCByUserDory () throws KustvaktException {
         List<VirtualCorpus> virtualCorpora = dao.retrieveVCByUser("dory");
-        //        System.out.println(virtualCorpora);
+        // System.out.println(virtualCorpora);
         assertEquals(4, virtualCorpora.size());
         // ordered by id
         Iterator<VirtualCorpus> i = virtualCorpora.iterator();
@@ -85,9 +98,10 @@
         assertEquals("published VC", i.next().getName());
     }
 
-
-    /** retrieves group VC and
-     *  excludes hidden published VC (user has never used it)
+    /**
+     * retrieves group VC and
+     * excludes hidden published VC (user has never used it)
+     * 
      * @throws KustvaktException
      */
     @Test
@@ -100,9 +114,10 @@
         assertEquals("nemo VC", i.next().getName());
     }
 
-
-    /** retrieves published VC by the owner and
-     *  excludes group vc when a user is a pending member
+    /**
+     * retrieves published VC by the owner and
+     * excludes group vc when a user is a pending member
+     * 
      * @throws KustvaktException
      */
     @Test
@@ -115,10 +130,10 @@
         assertEquals("marlin VC", i.next().getName());
     }
 
-
-
-    /** retrieves published VC from an auto-generated hidden group and 
-     *  excludes group vc when a user is a deleted member 
+    /**
+     * retrieves published VC from an auto-generated hidden group and
+     * excludes group vc when a user is a deleted member
+     * 
      * @throws KustvaktException
      */
     @Test
diff --git a/full/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java b/full/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java
index 6ae2e3d..3cca102 100644
--- a/full/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/service/VirtualCorpusServiceTest.java
@@ -4,7 +4,9 @@
 
 import java.util.List;
 
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.ExpectedException;
 import org.junit.runner.RunWith;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.test.context.ContextConfiguration;
@@ -26,6 +28,23 @@
     @Autowired
     private VirtualCorpusService vcService;
 
+    @Rule
+    public ExpectedException thrown = ExpectedException.none();
+
+    @Test
+    public void testCreateNonUniqueVC () throws KustvaktException {
+        thrown.expect(KustvaktException.class);
+        thrown.expectMessage("A UNIQUE constraint failed "
+                + "(UNIQUE constraint failed: virtual_corpus.name, "
+                + "virtual_corpus.created_by)");
+
+        VirtualCorpusJson vc = new VirtualCorpusJson();
+        vc.setCorpusQuery("corpusSigle=GOE");
+        vc.setName("dory VC");
+        vc.setType(VirtualCorpusType.PRIVATE);
+        vcService.storeVC(vc, "dory");
+    }
+
     @Test
     public void createDeletePublishVC () throws KustvaktException {
         String username = "VirtualCorpusServiceTest";
@@ -63,7 +82,8 @@
         // check VC
         VirtualCorpusDto vcDto = vcService.searchVCById("dory", vcId);
         assertEquals("group VC published", vcDto.getName());
-        assertEquals(VirtualCorpusType.PUBLISHED.displayName(), vcDto.getType());
+        assertEquals(VirtualCorpusType.PUBLISHED.displayName(),
+                vcDto.getType());
 
         // check access
         List<VirtualCorpusAccess> accesses =
@@ -72,7 +92,7 @@
 
         VirtualCorpusAccess access = accesses.get(1);
         assertEquals(VirtualCorpusAccessStatus.HIDDEN, access.getStatus());
-        
+
         // check auto hidden group
         UserGroup autoHiddenGroup = access.getUserGroup();
         assertEquals(UserGroupStatus.HIDDEN, autoHiddenGroup.getStatus());
diff --git a/full/src/test/java/de/ids_mannheim/korap/web/ApiVersionTest.java b/full/src/test/java/de/ids_mannheim/korap/web/ApiVersionTest.java
index b8ca799..92400e6 100644
--- a/full/src/test/java/de/ids_mannheim/korap/web/ApiVersionTest.java
+++ b/full/src/test/java/de/ids_mannheim/korap/web/ApiVersionTest.java
@@ -23,7 +23,7 @@
                 .accept(MediaType.APPLICATION_JSON).get(ClientResponse.class);
         assertEquals(HttpStatus.PERMANENT_REDIRECT_308, response.getStatus());
         URI location = response.getLocation();
-        assertEquals("/api/v1.0/search", location.getPath());
+        assertEquals("/api/"+API_VERSION+"/search", location.getPath());
     }
 
     @Test
@@ -34,6 +34,6 @@
                 .get(ClientResponse.class);
         assertEquals(HttpStatus.PERMANENT_REDIRECT_308, response.getStatus());
         URI location = response.getLocation();
-        assertEquals("/api/v1.0/search", location.getPath());
+        assertEquals("/api/"+API_VERSION+"/search", location.getPath());
     }
 }
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 093d586..86d902e 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
@@ -2,10 +2,13 @@
 
 import static org.junit.Assert.assertEquals;
 
+import java.util.Set;
+
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 
 import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.google.common.net.HttpHeaders;
@@ -20,6 +23,9 @@
 import de.ids_mannheim.korap.config.SpringJerseyTest;
 import de.ids_mannheim.korap.constant.GroupMemberStatus;
 import de.ids_mannheim.korap.constant.PredefinedRole;
+import de.ids_mannheim.korap.dao.UserGroupMemberDao;
+import de.ids_mannheim.korap.entity.Role;
+import de.ids_mannheim.korap.entity.UserGroupMember;
 import de.ids_mannheim.korap.exceptions.KustvaktException;
 import de.ids_mannheim.korap.exceptions.StatusCodes;
 import de.ids_mannheim.korap.utils.JsonUtils;
@@ -31,13 +37,17 @@
  */
 public class UserGroupControllerTest extends SpringJerseyTest {
 
+    @Autowired
+    private UserGroupMemberDao memberDao;
+
     private String username = "UserGroupControllerTest";
     private String admin = "admin";
 
     private JsonNode retrieveUserGroups (String username)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path("list")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("list")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -52,7 +62,8 @@
     // dory is a group admin in dory group
     @Test
     public void testListDoryGroups () throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path("list")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("list")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -72,7 +83,8 @@
     // nemo is a group member in dory group
     @Test
     public void testListNemoGroups () throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path("list")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("list")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("nemo", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -92,8 +104,8 @@
     // marlin has 2 groups
     @Test
     public void testListMarlinGroups () throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path("list")
-                .queryParam("username", "marlin")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("list").queryParam("username", "marlin")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("marlin", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -104,11 +116,10 @@
         assertEquals(2, node.size());
     }
 
-
     @Test
     public void testListGroupGuest () throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path("list")
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+        ClientResponse response = resource().path(API_VERSION).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);
@@ -129,8 +140,8 @@
         json.setName("new user group");
         json.setMembers(new String[] { "marlin", "nemo" });
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("create")
-                .type(MediaType.APPLICATION_JSON)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("create").type(MediaType.APPLICATION_JSON)
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").entity(json)
@@ -178,13 +189,12 @@
         testUnsubscribeToDeletedGroup(groupId);
     }
 
-
     private void testDeleteMember (String groupId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
         // delete marlin from group
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
-                .path("delete").path(groupId).path("marlin")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("delete").path(groupId).path("marlin")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -206,8 +216,8 @@
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
         // nemo is a group member
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
-                .path("delete").path(groupId).path("marlin")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("delete").path(groupId).path("marlin")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("nemo", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -227,7 +237,8 @@
     private void testDeletePendingMember () throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
         // dory delete pearl
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member")
                 // dory group
                 .path("delete").path("2").path("pearl")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
@@ -245,8 +256,8 @@
     @Test
     public void testDeleteDeletedMember () throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
-                .path("delete").path("2").path("pearl")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("delete").path("2").path("pearl")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -267,8 +278,8 @@
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
         // delete group
-        ClientResponse response = resource().path(API_VERSION).path("group").path("delete")
-                .path(groupId)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("delete").path(groupId)
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -277,8 +288,8 @@
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         // check group
-        response = resource().path(API_VERSION).path("group").path("list").path("system-admin")
-                .queryParam("username", username)
+        response = resource().path(API_VERSION).path("group").path("list")
+                .path("system-admin").queryParam("username", username)
                 .queryParam("status", "DELETED")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(admin, "pass"))
@@ -301,8 +312,8 @@
     public void testDeleteGroupUnauthorized () throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
         // dory is a group admin in marlin group
-        ClientResponse response = resource().path(API_VERSION).path("group").path("delete")
-                .path("1")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("delete").path("1")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -321,8 +332,8 @@
     @Test
     public void testDeleteDeletedGroup () throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path("delete")
-                .path("4")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("delete").path("4")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -343,8 +354,8 @@
             ClientHandlerException, KustvaktException {
         // delete marlin from marlin group
         // dory is a group admin in marlin group
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
-                .path("delete").path("1").path("marlin")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("delete").path("1").path("marlin")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -368,8 +379,8 @@
         userGroup.setMembers(members);
         userGroup.setId(Integer.parseInt(groupId));
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
-                .path("invite").type(MediaType.APPLICATION_JSON)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("invite").type(MediaType.APPLICATION_JSON)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
@@ -405,8 +416,8 @@
         // dory group
         userGroup.setId(2);
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
-                .path("invite").type(MediaType.APPLICATION_JSON)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("invite").type(MediaType.APPLICATION_JSON)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
@@ -434,8 +445,8 @@
         // dory group
         userGroup.setId(2);
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
-                .path("invite").type(MediaType.APPLICATION_JSON)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("invite").type(MediaType.APPLICATION_JSON)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
@@ -464,8 +475,8 @@
         // dory group
         userGroup.setId(2);
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
-                .path("invite").type(MediaType.APPLICATION_JSON)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("invite").type(MediaType.APPLICATION_JSON)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
@@ -484,7 +495,6 @@
                 node.at("/errors/0/2").asText());
     }
 
-
     @Test
     public void testInviteActiveMember () throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
@@ -496,8 +506,8 @@
         // dory group
         userGroup.setId(2);
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
-                .path("invite").type(MediaType.APPLICATION_JSON)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("invite").type(MediaType.APPLICATION_JSON)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
@@ -528,8 +538,8 @@
         // dory's deleted group
         userGroup.setId(4);
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("member")
-                .path("invite").type(MediaType.APPLICATION_JSON)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("invite").type(MediaType.APPLICATION_JSON)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
@@ -551,8 +561,8 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("groupId", "2");
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("subscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("subscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("marlin", "pass"))
@@ -578,7 +588,6 @@
         assertEquals(PredefinedRole.VC_ACCESS_MEMBER.name(),
                 group.at("/userRoles/1").asText());
 
-
         // unsubscribe marlin from dory group
         testUnsubscribeActiveMember(form);
         checkGroupMemberRole("2", "marlin");
@@ -594,8 +603,8 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("groupId", "2");
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("subscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("subscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("pearl", "pass"))
@@ -612,7 +621,8 @@
 
     @Test
     public void testSubscribeMissingGroupId () throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path("subscribe")
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("subscribe")
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("bruce", "pass"))
@@ -632,8 +642,8 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("groupId", "2");
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("subscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("subscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("bruce", "pass"))
@@ -654,8 +664,8 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("groupId", "100");
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("subscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("subscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("pearl", "pass"))
@@ -677,8 +687,8 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("groupId", groupId);
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("subscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("subscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("nemo", "pass"))
@@ -697,8 +707,8 @@
             MultivaluedMap<String, String> form)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path("unsubscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("unsubscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("marlin", "pass"))
@@ -712,7 +722,8 @@
 
     private void checkGroupMemberRole (String groupId, String deletedMemberName)
             throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path(groupId)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path(groupId)
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(admin, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -741,8 +752,8 @@
         // dory group
         form.add("groupId", "2");
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("unsubscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("unsubscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("pearl", "pass"))
@@ -771,8 +782,8 @@
         // dory group
         form.add("groupId", "2");
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("unsubscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("unsubscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("marlin", "pass"))
@@ -792,8 +803,8 @@
     public void testUnsubscribeMissingGroupId () throws KustvaktException {
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("unsubscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("unsubscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("marlin", "pass"))
@@ -815,8 +826,8 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("groupId", "2");
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("unsubscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("unsubscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("bruce", "pass"))
@@ -838,8 +849,8 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("groupId", "100");
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("unsubscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("unsubscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("pearl", "pass"))
@@ -862,8 +873,8 @@
         MultivaluedMap<String, String> form = new MultivaluedMapImpl();
         form.add("groupId", groupId);
 
-        ClientResponse response = resource().path(API_VERSION).path("group").path("unsubscribe")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("unsubscribe").type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("nemo", "pass"))
@@ -877,4 +888,94 @@
         assertEquals("Group new user group has been deleted.",
                 node.at("/errors/0/1").asText());
     }
+
+    @Test
+    public void testAddSameMemberRole () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("groupId", "1");
+        form.add("memberUsername", "dory");
+        form.add("roleIds", "1");
+
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("role").path("add")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        UserGroupMember member = memberDao.retrieveMemberById("dory", 1);
+        Set<Role> roles = member.getRoles();
+        assertEquals(2, roles.size());
+    }
+
+    @Test
+    public void testDeleteAddMemberRole () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("groupId", "1");
+        form.add("memberUsername", "dory");
+        form.add("roleIds", "1");
+
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("role").path("delete")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        UserGroupMember member = memberDao.retrieveMemberById("dory", 1);
+        Set<Role> roles = member.getRoles();
+        assertEquals(1, roles.size());
+
+        testAddSameMemberRole();
+    }
+
+    @Test
+    public void testEditMemberRoleEmpty () throws UniformInterfaceException,
+            ClientHandlerException, KustvaktException {
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        form.add("groupId", "1");
+        form.add("memberUsername", "dory");
+
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("role").path("edit")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        UserGroupMember member = memberDao.retrieveMemberById("dory", 1);
+        Set<Role> roles = member.getRoles();
+        assertEquals(0, roles.size());
+
+        testEditMemberRole(form);
+    }
+
+    private void testEditMemberRole (MultivaluedMap<String, String> form)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        form.add("roleIds", "1");
+        form.add("roleIds", "3");
+
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path("member").path("role").path("edit")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
+                .entity(form).post(ClientResponse.class);
+
+        assertEquals(Status.OK.getStatusCode(), response.getStatus());
+
+        UserGroupMember member = memberDao.retrieveMemberById("dory", 1);
+        Set<Role> roles = member.getRoles();
+        assertEquals(2, roles.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 9cb2445..6530e7d 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
@@ -14,6 +14,7 @@
 import javax.ws.rs.core.MediaType;
 import javax.ws.rs.core.MultivaluedMap;
 
+import org.apache.http.HttpStatus;
 import org.apache.http.entity.ContentType;
 import org.junit.Test;
 
@@ -63,7 +64,8 @@
     private JsonNode testSearchVC (String username, String vcId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path(vcId)
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path(vcId)
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -78,7 +80,8 @@
     private JsonNode testListVC (String username)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("list")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("list")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -91,12 +94,11 @@
         return JsonUtils.readTree(entity);
     }
 
-
     private JsonNode testListOwnerVC (String username)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("list")
-                .path("user")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("list").path("user")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -112,8 +114,8 @@
 
     private void testDeleteVC (String vcId, String username)
             throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("delete")
-                .path(vcId)
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("delete").path(vcId)
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -125,8 +127,8 @@
 
     private JsonNode testlistAccessByVC (String username, String vcId)
             throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("access")
-                .path("list").queryParam("vcId", vcId)
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("access").path("list").queryParam("vcId", vcId)
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -161,10 +163,12 @@
     public void testSearchPrivateVCUnauthorized ()
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("1").header(
-                Attributes.AUTHORIZATION,
-                HttpAuthorizationHandler.createBasicAuthorizationHeaderValue(
-                        "VirtualCorpusControllerTest", "pass"))
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("1")
+                .header(Attributes.AUTHORIZATION,
+                        HttpAuthorizationHandler
+                                .createBasicAuthorizationHeaderValue(
+                                        "VirtualCorpusControllerTest", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .get(ClientResponse.class);
         String entity = response.getEntity(String.class);
@@ -194,7 +198,8 @@
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("2")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("2")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("marlin", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -250,8 +255,8 @@
     @Test
     public void testListVCByOtherUser () throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("list")
-                .queryParam("createdBy", "dory")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("list").queryParam("createdBy", "dory")
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("pearl", "pass"))
@@ -271,8 +276,8 @@
     @Test
     public void testListVCByGuest () throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("list")
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("list").header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .get(ClientResponse.class);
         String entity = response.getEntity(String.class);
         JsonNode node = JsonUtils.readTree(entity);
@@ -291,10 +296,12 @@
         String json = "{\"name\": \"new vc\",\"type\": \"PRIVATE\","
                 + "\"corpusQuery\": \"corpusSigle=GOE\"}";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("create").header(
-                Attributes.AUTHORIZATION,
-                HttpAuthorizationHandler.createBasicAuthorizationHeaderValue(
-                        "VirtualCorpusControllerTest", "pass"))
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("create")
+                .header(Attributes.AUTHORIZATION,
+                        HttpAuthorizationHandler
+                                .createBasicAuthorizationHeaderValue(
+                                        "VirtualCorpusControllerTest", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
                 .post(ClientResponse.class, json);
@@ -321,10 +328,12 @@
     public void testCreatePublishVC () throws KustvaktException {
         String json = "{\"name\": \"new published vc\",\"type\": \"PUBLISHED\""
                 + ",\"corpusQuery\": \"corpusSigle=GOE\"}";
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("create").header(
-                Attributes.AUTHORIZATION,
-                HttpAuthorizationHandler.createBasicAuthorizationHeaderValue(
-                        "VirtualCorpusControllerTest", "pass"))
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("create")
+                .header(Attributes.AUTHORIZATION,
+                        HttpAuthorizationHandler
+                                .createBasicAuthorizationHeaderValue(
+                                        "VirtualCorpusControllerTest", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
                 .post(ClientResponse.class, json);
@@ -365,7 +374,8 @@
     private JsonNode testCheckHiddenGroup (String groupId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("group").path(groupId)
+        ClientResponse response = resource().path(API_VERSION).path("group")
+                .path(groupId)
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("admin", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -390,7 +400,8 @@
             authToken = reader.readLine();
         }
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("create")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("create")
                 .header(Attributes.AUTHORIZATION,
                         AuthenticationScheme.API.displayName() + " "
                                 + authToken)
@@ -421,7 +432,8 @@
                 + "UiLCJleHAiOjE1MzA2MTgyOTR9.JUMvTQZ4tvdRXFBpQKzoNxrq7"
                 + "CuYAfytr_LWqY8woJs";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("create")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("create")
                 .header(Attributes.AUTHORIZATION,
                         AuthenticationScheme.API.displayName() + " "
                                 + authToken)
@@ -445,10 +457,12 @@
         String json = "{\"name\": \"new vc\",\"type\": \"SYSTEM\","
                 + "\"corpusQuery\": \"creationDate since 1820\"}";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("create").header(
-                Attributes.AUTHORIZATION,
-                HttpAuthorizationHandler.createBasicAuthorizationHeaderValue(
-                        "VirtualCorpusControllerTest", "pass"))
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("create")
+                .header(Attributes.AUTHORIZATION,
+                        HttpAuthorizationHandler
+                                .createBasicAuthorizationHeaderValue(
+                                        "VirtualCorpusControllerTest", "pass"))
                 .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
                 .entity(json).post(ClientResponse.class);
 
@@ -464,16 +478,18 @@
 
         checkWWWAuthenticateHeader(response);
     }
-    
+
     @Test
     public void testCreateVCInvalidName () throws KustvaktException {
         String json = "{\"name\": \"new $vc\",\"type\": \"PRIVATE\","
                 + "\"corpusQuery\": \"creationDate since 1820\"}";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("create").header(
-                Attributes.AUTHORIZATION,
-                HttpAuthorizationHandler.createBasicAuthorizationHeaderValue(
-                        "VirtualCorpusControllerTest", "pass"))
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("create")
+                .header(Attributes.AUTHORIZATION,
+                        HttpAuthorizationHandler
+                                .createBasicAuthorizationHeaderValue(
+                                        "VirtualCorpusControllerTest", "pass"))
                 .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
                 .entity(json).post(ClientResponse.class);
         String entity = response.getEntity(String.class);
@@ -489,7 +505,8 @@
         String json = "{\"name\": \"new vc\",\"type\": \"PRIVATE\","
                 + "\"corpusQuery\": \"creationDate since 1820\"}";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("create")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("create")
                 .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
                 .entity(json).post(ClientResponse.class);
 
@@ -509,10 +526,12 @@
     public void testCreateVCWithoutcorpusQuery () throws KustvaktException {
         String json = "{\"name\": \"new vc\",\"type\": \"PRIVATE\"}";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("create").header(
-                Attributes.AUTHORIZATION,
-                HttpAuthorizationHandler.createBasicAuthorizationHeaderValue(
-                        "VirtualCorpusControllerTest", "pass"))
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("create")
+                .header(Attributes.AUTHORIZATION,
+                        HttpAuthorizationHandler
+                                .createBasicAuthorizationHeaderValue(
+                                        "VirtualCorpusControllerTest", "pass"))
                 .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
                 .entity(json).post(ClientResponse.class);
         String entity = response.getEntity(String.class);
@@ -531,10 +550,12 @@
         String json = "{\"name\": \"new vc\",\"corpusQuery\": "
                 + "\"creationDate since 1820\"}";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("create").header(
-                Attributes.AUTHORIZATION,
-                HttpAuthorizationHandler.createBasicAuthorizationHeaderValue(
-                        "VirtualCorpusControllerTest", "pass"))
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("create")
+                .header(Attributes.AUTHORIZATION,
+                        HttpAuthorizationHandler
+                                .createBasicAuthorizationHeaderValue(
+                                        "VirtualCorpusControllerTest", "pass"))
                 .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
                 .entity(json).post(ClientResponse.class);
         String entity = response.getEntity(String.class);
@@ -553,10 +574,12 @@
         String json = "{\"name\": \"new vc\",\"type\": \"PRIVAT\","
                 + "\"corpusQuery\": \"creationDate since 1820\"}";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("create").header(
-                Attributes.AUTHORIZATION,
-                HttpAuthorizationHandler.createBasicAuthorizationHeaderValue(
-                        "VirtualCorpusControllerTest", "pass"))
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("create")
+                .header(Attributes.AUTHORIZATION,
+                        HttpAuthorizationHandler
+                                .createBasicAuthorizationHeaderValue(
+                                        "VirtualCorpusControllerTest", "pass"))
                 .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
                 .entity(json).post(ClientResponse.class);
         String entity = response.getEntity(String.class);
@@ -574,7 +597,8 @@
 
     @Test
     public void testDeleteVCUnauthorized () throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("delete").path("1")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("delete").path("1")
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
@@ -602,7 +626,8 @@
         // 1st edit
         String json = "{\"id\": \"1\", \"name\": \"edited vc\"}";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("edit")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("edit")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -636,10 +661,12 @@
     public void testEditVCNotOwner () throws KustvaktException {
         String json = "{\"id\": \"1\", \"name\": \"edited vc\"}";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("edit").header(
-                Attributes.AUTHORIZATION,
-                HttpAuthorizationHandler.createBasicAuthorizationHeaderValue(
-                        "VirtualCorpusControllerTest", "pass"))
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("edit")
+                .header(Attributes.AUTHORIZATION,
+                        HttpAuthorizationHandler
+                                .createBasicAuthorizationHeaderValue(
+                                        "VirtualCorpusControllerTest", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
                 .header(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON)
                 .post(ClientResponse.class, json);
@@ -656,7 +683,6 @@
         checkWWWAuthenticateHeader(response);
     }
 
-
     /**
      * @see VirtualCorpusServiceTest
      * @throws KustvaktException
@@ -667,7 +693,8 @@
         String vcId = "2";
         String json = "{\"id\": \"" + vcId + "\", \"type\": \"PUBLISHED\"}";
 
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("edit")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("edit")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -715,8 +742,8 @@
 
     @Test
     public void testlistAccessMissingId () throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("access")
-                .path("list")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("access").path("list")
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
@@ -733,8 +760,9 @@
 
     @Test
     public void testlistAccessByGroup () throws KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("access")
-                .path("list").path("byGroup").queryParam("groupId", "2")
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("access").path("list").path("byGroup")
+                .queryParam("groupId", "2")
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -749,28 +777,12 @@
         assertEquals("dory group", node.at("/0/userGroupName").asText());
     }
 
-
     @Test
     public void testCreateDeleteAccess () throws UniformInterfaceException,
             ClientHandlerException, KustvaktException {
 
         String vcId = "5";
-
-        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
-        // marlin vc
-        form.add("vcId", vcId);
-        // marlin group
-        form.add("groupId", "1");
-
-        ClientResponse response;
-        // share VC
-        response = resource().path(API_VERSION).path("vc").path("access").path("share")
-                .type(MediaType.APPLICATION_FORM_URLENCODED)
-                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
-                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
-                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").entity(form)
-                .post(ClientResponse.class);
-
+        ClientResponse response = testShareVC(vcId);
         assertEquals(Status.OK.getStatusCode(), response.getStatus());
 
         // list vc access by marlin
@@ -784,8 +796,7 @@
 
         String accessId = node.at("/accessId").asText();
 
-        // delete access
-        // unauthorized
+        testCreateNonUniqueAccess(vcId);
         testDeleteAccessUnauthorized(accessId);
 
         // delete access
@@ -797,6 +808,37 @@
         assertEquals(0, node.size());
     }
 
+    private ClientResponse testShareVC (String vcId)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+
+        MultivaluedMap<String, String> form = new MultivaluedMapImpl();
+        // marlin vc
+        form.add("vcId", vcId);
+        // marlin group
+        form.add("groupId", "1");
+
+        return resource().path(API_VERSION).path("vc").path("access")
+                .path("share").type(MediaType.APPLICATION_FORM_URLENCODED)
+                .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
+                        .createBasicAuthorizationHeaderValue("marlin", "pass"))
+                .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").entity(form)
+                .post(ClientResponse.class);
+    }
+
+    private void testCreateNonUniqueAccess (String vcId)
+            throws UniformInterfaceException, ClientHandlerException,
+            KustvaktException {
+        ClientResponse response = testShareVC(vcId);
+        JsonNode node = JsonUtils.readTree(response.getEntity(String.class));
+        
+        assertEquals(HttpStatus.SC_BAD_REQUEST, response.getStatus());
+        assertEquals(StatusCodes.DB_INSERT_FAILED,
+                node.at("/errors/0/0").asInt());
+        assertTrue(node.at("/errors/0/1").asText()
+                .startsWith("[SQLITE_CONSTRAINT_UNIQUE]"));
+    }
+
     @Test
     public void testCreateAccessByVCAButNotVCOwner ()
             throws UniformInterfaceException, ClientHandlerException,
@@ -810,8 +852,9 @@
 
         // share VC
         // dory is VCA in marlin group
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("access")
-                .path("share").type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("access").path("share")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("dory", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").entity(form)
@@ -838,8 +881,9 @@
 
         // share VC
         // nemo is not VCA in marlin group
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("access")
-                .path("share").type(MediaType.APPLICATION_FORM_URLENCODED)
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("access").path("share")
+                .type(MediaType.APPLICATION_FORM_URLENCODED)
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue("nemo", "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32").entity(form)
@@ -857,8 +901,8 @@
     private void testDeleteAccess (String username, String accessId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("access")
-                .path("delete").path(accessId)
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("access").path("delete").path(accessId)
                 .header(Attributes.AUTHORIZATION, HttpAuthorizationHandler
                         .createBasicAuthorizationHeaderValue(username, "pass"))
                 .header(HttpHeaders.X_FORWARDED_FOR, "149.27.0.32")
@@ -870,8 +914,8 @@
     private void testDeleteAccessUnauthorized (String accessId)
             throws UniformInterfaceException, ClientHandlerException,
             KustvaktException {
-        ClientResponse response = resource().path(API_VERSION).path("vc").path("access")
-                .path("delete").path(accessId)
+        ClientResponse response = resource().path(API_VERSION).path("vc")
+                .path("access").path("delete").path(accessId)
                 .header(Attributes.AUTHORIZATION,
                         HttpAuthorizationHandler
                                 .createBasicAuthorizationHeaderValue(
diff --git a/full/src/test/resources/kustvakt-test.conf b/full/src/test/resources/kustvakt-test.conf
index 9a522ba..356d46e 100644
--- a/full/src/test/resources/kustvakt-test.conf
+++ b/full/src/test/resources/kustvakt-test.conf
@@ -13,7 +13,7 @@
 
 # Kustvakt
 
-current.api.version = v1.0
+current.api.version = v1.1
 # multiple versions separated by space
 supported.api.version = v0.1 v1.0