From 0479a62156453a268e80a7c975d3831692945afc Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Mon, 12 Aug 2019 22:52:09 +0200
Subject: [PATCH] Added web service controller logic and tests

---
 .../ia2/gms/controller/GroupsController.java  |   8 +-
 .../gms/controller/WebServiceController.java  |  72 +++++-
 .../inaf/ia2/gms/model/AddMemberRequest.java  |  27 +++
 .../ia2/gms/model/AddPermissionWsRequest.java |  39 ++++
 .../ia2/gms/model/CreateGroupRequest.java     |   4 +
 .../ia2/gms/model/PrepareToJoinRequest.java   |  23 ++
 .../inaf/ia2/gms/persistence/GroupsDAO.java   |  42 +++-
 .../ia2/gms/persistence/MembershipsDAO.java   |  68 +++++-
 .../ia2/gms/persistence/PermissionsDAO.java   |  36 +++
 .../persistence/model/MembershipEntity.java   |   4 +
 .../persistence/model/PermissionEntity.java   |   7 +-
 .../inaf/ia2/gms/service/GroupsService.java   |  44 +++-
 .../inaf/ia2/gms/service/MembersService.java  |  17 ++
 .../ia2/gms/service/PermissionsService.java   |  24 +-
 .../controller/WebServiceControllerTest.java  | 215 ++++++++++++++++++
 .../PermissionsServiceIntegrationTest.java    |   2 +-
 16 files changed, 601 insertions(+), 31 deletions(-)
 create mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/AddMemberRequest.java
 create mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/AddPermissionWsRequest.java
 create mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/PrepareToJoinRequest.java
 create mode 100644 gms/src/test/java/it/inaf/ia2/gms/controller/WebServiceControllerTest.java

diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java
index 11b05b0..4b91c0e 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsController.java
@@ -82,7 +82,13 @@ public class GroupsController {
     @DeleteMapping(value = "/group/{groupId}", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
     public ResponseEntity<?> deleteGroup(@PathVariable("groupId") String groupId, PaginatedModelRequest request) {
 
-        GroupEntity parent = groupsService.deleteGroup(groupId, session.getUserId());
+        GroupEntity group = groupsService.getGroupById(groupId);
+
+        if (permissionsService.getGroupPermission(group, session.getUserId()) != Permission.ADMIN) {
+            throw new UnauthorizedException("Missing admin privileges");
+        }
+
+        GroupEntity parent = groupsService.deleteGroup(group);
 
         PaginatedData<GroupNode> groupsPanel = getGroupsPanel(parent.getId(), request);
 
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/WebServiceController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/WebServiceController.java
index a6ff8c8..f71e4e3 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/WebServiceController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/WebServiceController.java
@@ -1,15 +1,28 @@
 package it.inaf.ia2.gms.controller;
 
+import it.inaf.ia2.gms.exception.BadRequestException;
+import it.inaf.ia2.gms.model.AddPermissionWsRequest;
+import it.inaf.ia2.gms.model.Permission;
+import it.inaf.ia2.gms.model.PrepareToJoinRequest;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.persistence.model.MembershipEntity;
+import it.inaf.ia2.gms.persistence.model.PermissionEntity;
 import it.inaf.ia2.gms.service.GroupsService;
+import it.inaf.ia2.gms.service.MembersService;
+import it.inaf.ia2.gms.service.PermissionsService;
+import java.util.Arrays;
 import java.util.List;
 import java.util.Optional;
+import javax.validation.Valid;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
 import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
@@ -19,11 +32,17 @@ public class WebServiceController {
     @Autowired
     private GroupsService groupsService;
 
+    @Autowired
+    private MembersService membersService;
+
+    @Autowired
+    private PermissionsService permissionsService;
+
     /**
      * Creates a group and its ancestors if they are missing. It doesn't fail if
      * the last group already exists.
      */
-    @PostMapping("/group")
+    @PostMapping(value = "/group", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
     public ResponseEntity<GroupEntity> createGroup(@RequestBody List<String> names) {
 
         GroupEntity group = groupsService.getRoot();
@@ -40,27 +59,64 @@ public class WebServiceController {
         return new ResponseEntity<>(group, HttpStatus.CREATED);
     }
 
-    public void deleteGroup() {
+    @DeleteMapping("/group")
+    public ResponseEntity<?> deleteGroupByPath(@RequestParam("names") String[] names) {
 
-    }
+        GroupEntity groupToDelete = getGroupByNames(Arrays.asList(names));
+
+        groupsService.deleteGroup(groupToDelete);
 
-    public void addMember() {
+        return ResponseEntity.noContent().build();
+    }
 
+    @PostMapping(value = "/member", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+    public ResponseEntity<MembershipEntity> addMember(@RequestBody MembershipEntity request) {
+        MembershipEntity membership = membersService.addMember(request.getGroupId(), request.getUserId());
+        return new ResponseEntity<>(membership, HttpStatus.CREATED);
     }
 
-    public void removeMember() {
+    @DeleteMapping("/member")
+    public ResponseEntity<?> removeMember(@RequestParam("names") String[] names, @RequestParam("userId") String userId) {
+
+        GroupEntity group = getGroupByNames(Arrays.asList(names));
 
+        membersService.removeMember(group.getId(), userId);
+
+        return ResponseEntity.noContent().build();
     }
 
-    public void addPrivilege() {
+    @PostMapping(value = "/permission", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+    public ResponseEntity<PermissionEntity> addPermission(@Valid @RequestBody AddPermissionWsRequest request) {
+
+        GroupEntity group = getGroupByNames(request.getNames());
 
+        PermissionEntity newPermission = permissionsService.addPermission(group, request.getUserId(), request.getPermission());
+
+        return new ResponseEntity<>(newPermission, HttpStatus.CREATED);
     }
 
-    public void deletePrivilege() {
+    @DeleteMapping("/permission")
+    public ResponseEntity<?> deletePermission(@RequestParam("names") String[] names,
+            @RequestParam("userId") String userId, @RequestParam("permission") Permission permission) {
+
+        GroupEntity group = getGroupByNames(Arrays.asList(names));
 
+        permissionsService.deletePermission(group, userId, permission);
+
+        return ResponseEntity.noContent().build();
     }
 
-    public void prepareToJoin() {
+    @PostMapping(value = "/prepare-join", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
+    public ResponseEntity<?> prepareToJoin(@Valid @RequestBody PrepareToJoinRequest request) {
+
+        permissionsService.movePermissions(request.getFromUserId(), request.getToUserId());
+        membersService.moveMemberships(request.getFromUserId(), request.getToUserId());
+
+        return ResponseEntity.ok().build();
+    }
 
+    private GroupEntity getGroupByNames(List<String> names) {
+        return groupsService.findGroupByNames(names)
+                .orElseThrow(() -> new BadRequestException("Unable to find requested group"));
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/AddMemberRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/AddMemberRequest.java
new file mode 100644
index 0000000..a22dc85
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/AddMemberRequest.java
@@ -0,0 +1,27 @@
+package it.inaf.ia2.gms.model;
+
+import javax.validation.constraints.NotEmpty;
+
+public class AddMemberRequest extends PaginatedModelRequest {
+
+    @NotEmpty
+    private String groupId;
+    @NotEmpty
+    private String userId;
+
+    public String getGroupId() {
+        return groupId;
+    }
+
+    public void setGroupId(String groupId) {
+        this.groupId = groupId;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/AddPermissionWsRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/AddPermissionWsRequest.java
new file mode 100644
index 0000000..6e45d07
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/AddPermissionWsRequest.java
@@ -0,0 +1,39 @@
+package it.inaf.ia2.gms.model;
+
+import java.util.List;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
+
+public class AddPermissionWsRequest {
+
+    @NotNull
+    private List<String> names;
+    @NotEmpty
+    private String userId;
+    @NotNull
+    private Permission permission;
+
+    public List<String> getNames() {
+        return names;
+    }
+
+    public void setNames(List<String> names) {
+        this.names = names;
+    }
+
+    public String getUserId() {
+        return userId;
+    }
+
+    public void setUserId(String userId) {
+        this.userId = userId;
+    }
+
+    public Permission getPermission() {
+        return permission;
+    }
+
+    public void setPermission(Permission permission) {
+        this.permission = permission;
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/CreateGroupRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/CreateGroupRequest.java
index c7444a9..d13ab80 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/model/CreateGroupRequest.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/CreateGroupRequest.java
@@ -1,8 +1,12 @@
 package it.inaf.ia2.gms.model;
 
+import javax.validation.constraints.NotEmpty;
+
 public class CreateGroupRequest extends PaginatedModelRequest {
 
+    @NotEmpty
     private String parentGroupId;
+    @NotEmpty
     private String newGroupName;
 
     public String getNewGroupName() {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/PrepareToJoinRequest.java b/gms/src/main/java/it/inaf/ia2/gms/model/PrepareToJoinRequest.java
new file mode 100644
index 0000000..84681f7
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/PrepareToJoinRequest.java
@@ -0,0 +1,23 @@
+package it.inaf.ia2.gms.model;
+
+public class PrepareToJoinRequest {
+
+    private String fromUserId;
+    private String toUserId;
+
+    public String getFromUserId() {
+        return fromUserId;
+    }
+
+    public void setFromUserId(String fromUserId) {
+        this.fromUserId = fromUserId;
+    }
+
+    public String getToUserId() {
+        return toUserId;
+    }
+
+    public void setToUserId(String toUserId) {
+        this.toUserId = toUserId;
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
index 9f84eae..80d0c87 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/GroupsDAO.java
@@ -3,6 +3,8 @@ package it.inaf.ia2.gms.persistence;
 import it.inaf.ia2.gms.model.GroupBreadcrumb;
 import it.inaf.ia2.gms.persistence.model.GroupEntity;
 import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -131,15 +133,7 @@ public class GroupsDAO {
             ps.setObject(1, getSubGroupsPath(path), Types.OTHER);
             return ps;
         }, resultSet -> {
-            List<GroupEntity> groups = new ArrayList<>();
-            while (resultSet.next()) {
-                GroupEntity group = new GroupEntity();
-                group.setId(resultSet.getString("id"));
-                group.setName(resultSet.getString("name"));
-                group.setPath(resultSet.getString("path"));
-                groups.add(group);
-            }
-            return groups;
+            return getGroupsFromResultSet(resultSet);
         });
     }
 
@@ -151,6 +145,36 @@ public class GroupsDAO {
         return path;
     }
 
+    public List<GroupEntity> findGroupsByNames(List<String> names) {
+
+        String sql = "SELECT id, name, path from gms_group WHERE name IN ("
+                + String.join(",", names.stream().map(g -> "?").collect(Collectors.toList()))
+                + ")";
+
+        return jdbcTemplate.query(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            int i = 0;
+            for (String name : names) {
+                ps.setString(++i, name);
+            }
+            return ps;
+        }, resultSet -> {
+            return getGroupsFromResultSet(resultSet);
+        });
+    }
+
+    private List<GroupEntity> getGroupsFromResultSet(ResultSet resultSet) throws SQLException {
+        List<GroupEntity> groups = new ArrayList<>();
+        while (resultSet.next()) {
+            GroupEntity group = new GroupEntity();
+            group.setId(resultSet.getString("id"));
+            group.setName(resultSet.getString("name"));
+            group.setPath(resultSet.getString("path"));
+            groups.add(group);
+        }
+        return groups;
+    }
+
     public Map<String, Boolean> getHasChildrenMap(Set<String> groupIds) {
 
         if (groupIds.isEmpty()) {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
index d6b832d..a18832c 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/MembershipsDAO.java
@@ -1,13 +1,79 @@
 package it.inaf.ia2.gms.persistence;
 
 import it.inaf.ia2.gms.persistence.model.MembershipEntity;
+import java.sql.PreparedStatement;
+import java.util.ArrayList;
 import java.util.List;
+import javax.sql.DataSource;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.stereotype.Component;
 
 @Component
 public class MembershipsDAO {
 
+    private final JdbcTemplate jdbcTemplate;
+
+    @Autowired
+    public MembershipsDAO(DataSource dataSource) {
+        jdbcTemplate = new JdbcTemplate(dataSource);
+    }
+
     public List<MembershipEntity> findByGroup(String groupId) {
-        throw new UnsupportedOperationException("TODO");
+
+        String sql = "SELECT user_id FROM gms_membership WHERE group_id = ?";
+
+        return jdbcTemplate.query(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setString(1, groupId);
+            return ps;
+        }, resultSet -> {
+            List<MembershipEntity> members = new ArrayList<>();
+            while (resultSet.next()) {
+                MembershipEntity membership = new MembershipEntity();
+                membership.setGroupId(groupId);
+                membership.setUserId(resultSet.getString("user_id"));
+                members.add(membership);
+            }
+            return members;
+        });
+    }
+
+    public MembershipEntity addMember(MembershipEntity membership) {
+
+        String sql = "INSERT INTO gms_membership (group_id, user_id) VALUES (?, ?)";
+
+        jdbcTemplate.update(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setString(1, membership.getGroupId());
+            ps.setString(2, membership.getUserId());
+            return ps;
+        });
+
+        return membership;
+    }
+
+    public void removeMembership(String groupId, String userId) {
+
+        String sql = "DELETE FROM gms_membership WHERE group_id = ? AND user_id = ?";
+
+        jdbcTemplate.update(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setString(1, groupId);
+            ps.setString(2, userId);
+            return ps;
+        });
+    }
+
+    public void moveMemberships(String fromUserId, String toUserId) {
+
+        String sql = "UPDATE gms_membership SET user_id = ? WHERE user_id = ?";
+
+        jdbcTemplate.update(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setString(1, toUserId);
+            ps.setString(2, fromUserId);
+            return ps;
+        });
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
index 4162110..e442b65 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/PermissionsDAO.java
@@ -6,6 +6,7 @@ import java.sql.PreparedStatement;
 import java.sql.Types;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Optional;
 import javax.sql.DataSource;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.jdbc.core.JdbcTemplate;
@@ -84,6 +85,29 @@ public class PermissionsDAO {
         });
     }
 
+    public Optional<PermissionEntity> findPermissionEntity(String groupId, String userId, Permission permission) {
+
+        String sql = "SELECT group_path FROM gms_permission WHERE group_id = ? AND user_id = ? AND permission = ?";
+
+        return jdbcTemplate.query(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setString(1, groupId);
+            ps.setString(2, userId);
+            ps.setObject(3, permission.toString(), Types.OTHER);
+            return ps;
+        }, resultSet -> {
+            if (resultSet.next()) {
+                PermissionEntity permissionEntity = new PermissionEntity();
+                permissionEntity.setGroupId(groupId);
+                permissionEntity.setUserId(userId);
+                permissionEntity.setPermission(permission);
+                permissionEntity.setGroupPath(resultSet.getString("group_path"));
+                return Optional.of(permissionEntity);
+            }
+            return Optional.empty();
+        });
+    }
+
     public List<PermissionEntity> getGroupsPermissions(String groupId) {
 
         String sql = "SELECT user_id, permission, group_path FROM gms_permission WHERE group_id = ?";
@@ -118,4 +142,16 @@ public class PermissionsDAO {
             return ps;
         });
     }
+
+    public void movePermissions(String fromUserId, String toUserId) {
+
+        String sql = "UPDATE gms_permission SET user_id = ? WHERE user_id = ?";
+
+        jdbcTemplate.update(conn -> {
+            PreparedStatement ps = conn.prepareStatement(sql);
+            ps.setString(1, toUserId);
+            ps.setString(2, fromUserId);
+            return ps;
+        });
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java
index 9625a87..81c793c 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/MembershipEntity.java
@@ -1,8 +1,12 @@
 package it.inaf.ia2.gms.persistence.model;
 
+import javax.validation.constraints.NotEmpty;
+
 public class MembershipEntity {
 
+    @NotEmpty
     private String groupId;
+    @NotEmpty
     private String userId;
 
     public String getGroupId() {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java
index 957e658..98bfc6f 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/PermissionEntity.java
@@ -1,13 +1,18 @@
 package it.inaf.ia2.gms.persistence.model;
 
 import it.inaf.ia2.gms.model.Permission;
+import javax.validation.constraints.NotEmpty;
+import javax.validation.constraints.NotNull;
 
 public class PermissionEntity {
 
+    @NotEmpty
     private String userId;
+    @NotEmpty
     private String groupId;
+    @NotNull
     private Permission permission;
-    
+
     // The group path is copied here for performance reasons
     private String groupPath;
 
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java
index 12aa216..1647616 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsService.java
@@ -81,13 +81,7 @@ public class GroupsService {
         return groupsDAO.updateGroup(group);
     }
 
-    public GroupEntity deleteGroup(String groupId, String userId) {
-
-        GroupEntity group = getGroupById(groupId);
-
-        if (permissionsService.getGroupPermission(group, userId) != Permission.ADMIN) {
-            throw new UnauthorizedException("Missing admin privileges");
-        }
+    public GroupEntity deleteGroup(GroupEntity group) {
 
         if (ROOT.equals(group.getId())) {
             throw new UnauthorizedException("It is not possible to remove the ROOT");
@@ -97,7 +91,7 @@ public class GroupsService {
         GroupEntity parent = groupsDAO.findGroupByPath(parentPath)
                 .orElseThrow(() -> new BadRequestException("No group found at path " + parentPath));
 
-        groupsDAO.deleteGroupById(groupId);
+        groupsDAO.deleteGroupById(group.getId());
 
         return parent;
     }
@@ -123,4 +117,38 @@ public class GroupsService {
     public Optional<GroupEntity> findGroupByParentAndName(GroupEntity parent, String childName) {
         return groupsDAO.findGroupByParentAndName(parent.getPath(), childName);
     }
+
+    /**
+     * Retrieves a group given the sequence of group names corresponding to its
+     * path.
+     */
+    public Optional<GroupEntity> findGroupByNames(List<String> names) {
+
+        // There can be groups with the same name under different parents, so
+        // after retrieving this list it is necessary to match for the correct
+        // group
+        List<GroupEntity> groups = groupsDAO.findGroupsByNames(names);
+
+        String parentPath = "";
+        GroupEntity group = null;
+        for (String name : names) {
+            group = findGroup(groups, parentPath, name);
+            if (group == null) {
+                break;
+            } else {
+                parentPath = group.getPath();
+            }
+        }
+
+        return Optional.ofNullable(group);
+    }
+
+    private GroupEntity findGroup(List<GroupEntity> groups, String parentPath, String groupName) {
+        for (GroupEntity group : groups) {
+            if (parentPath.equals(group.getParentPath()) && groupName.equals(group.getName())) {
+                return group;
+            }
+        }
+        return null;
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/MembersService.java b/gms/src/main/java/it/inaf/ia2/gms/service/MembersService.java
index 7f9ab1d..b05fb52 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/MembersService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/MembersService.java
@@ -29,4 +29,21 @@ public class MembersService {
 
         return rapClient.getUsers(userIdentifiers);
     }
+
+    public MembershipEntity addMember(String groupId, String userId) {
+
+        MembershipEntity membership = new MembershipEntity();
+        membership.setGroupId(groupId);
+        membership.setUserId(userId);
+
+        return membershipsDAO.addMember(membership);
+    }
+
+    public void removeMember(String groupId, String userId) {
+        membershipsDAO.removeMembership(groupId, userId);
+    }
+
+    public void moveMemberships(String fromUserId, String toUserId) {
+        membershipsDAO.moveMemberships(fromUserId, toUserId);
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java b/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java
index ad1f5a9..96170b1 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/PermissionsService.java
@@ -10,6 +10,7 @@ import it.inaf.ia2.gms.rap.RapClient;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.Optional;
 import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
@@ -59,7 +60,26 @@ public class PermissionsService {
         return PermissionUtils.getGroupPermission(group, permissions).orElse(null);
     }
 
-    public void deletePermission(String userId, String groupId, Permission permission) {
-        permissionsDAO.deletePermission(userId, groupId, permission);
+    public void deletePermission(GroupEntity group, String userId, Permission permission) {
+        permissionsDAO.deletePermission(group.getId(), userId, permission);
+    }
+
+    public PermissionEntity addPermission(GroupEntity group, String userId, Permission permission) {
+
+        PermissionEntity permissionEntity = new PermissionEntity();
+        permissionEntity.setGroupId(group.getId());
+        permissionEntity.setUserId(userId);
+        permissionEntity.setPermission(permission);
+        permissionEntity.setGroupPath(group.getPath());
+
+        return permissionsDAO.createPermission(permissionEntity);
+    }
+
+    public Optional<PermissionEntity> findPermissionEntity(String groupId, String userId, Permission permission) {
+        return permissionsDAO.findPermissionEntity(groupId, userId, permission);
+    }
+    
+    public void movePermissions(String fromUserId, String toUserId) {
+        permissionsDAO.movePermissions(fromUserId, toUserId);
     }
 }
diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/WebServiceControllerTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/WebServiceControllerTest.java
new file mode 100644
index 0000000..fe53d1f
--- /dev/null
+++ b/gms/src/test/java/it/inaf/ia2/gms/controller/WebServiceControllerTest.java
@@ -0,0 +1,215 @@
+package it.inaf.ia2.gms.controller;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import it.inaf.ia2.gms.model.AddPermissionWsRequest;
+import it.inaf.ia2.gms.model.Permission;
+import it.inaf.ia2.gms.model.PrepareToJoinRequest;
+import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.persistence.model.MembershipEntity;
+import it.inaf.ia2.gms.persistence.model.PermissionEntity;
+import it.inaf.ia2.gms.service.GroupsService;
+import it.inaf.ia2.gms.service.MembersService;
+import it.inaf.ia2.gms.service.PermissionsService;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import static org.hamcrest.CoreMatchers.is;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import static org.mockito.ArgumentMatchers.eq;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.servlet.MockMvc;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+@RunWith(MockitoJUnitRunner.class)
+public class WebServiceControllerTest {
+
+    @Mock
+    private GroupsService groupsService;
+
+    @Mock
+    private MembersService membersService;
+
+    @Mock
+    private PermissionsService permissionsService;
+
+    @InjectMocks
+    private WebServiceController controller;
+
+    @Autowired
+    private MockMvc mockMvc;
+
+    private final ObjectMapper mapper = new ObjectMapper();
+
+    @Before
+    public void init() {
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    @Test
+    public void testCreateGroup() throws Exception {
+
+        GroupEntity root = new GroupEntity();
+        root.setId(GroupsService.ROOT);
+        root.setName(GroupsService.ROOT);
+        root.setPath("");
+
+        GroupEntity lbt = new GroupEntity();
+        lbt.setId("lbt_id");
+        lbt.setName("LBT");
+        lbt.setPath("lbt_id");
+
+        GroupEntity inaf = getInafGroup();
+
+        when(groupsService.getRoot()).thenReturn(root);
+        when(groupsService.findGroupByParentAndName(eq(root), eq("LBT"))).thenReturn(Optional.of(lbt));
+        when(groupsService.addGroup(eq(lbt), eq("INAF"))).thenReturn(inaf);
+
+        List<String> names = getNames("LBT", "INAF");
+
+        mockMvc.perform(post("/ws/group")
+                .content(mapper.writeValueAsString(names))
+                .contentType(MediaType.APPLICATION_JSON_UTF8))
+                .andExpect(status().isCreated())
+                .andExpect(jsonPath("$.id", is("inaf_id")))
+                .andExpect(jsonPath("$.name", is("INAF")))
+                .andExpect(jsonPath("$.path", is("lbt_id.inaf_id")))
+                .andExpect(jsonPath("$.parentPath", is("lbt_id")));
+    }
+
+    @Test
+    public void testDeleteGroupByPath() throws Exception {
+
+        List<String> names = getNames("LBT", "INAF");
+
+        GroupEntity inaf = getInafGroup();
+
+        when(groupsService.findGroupByNames(names)).thenReturn(Optional.of(inaf));
+
+        mockMvc.perform(delete("/ws/group?names=LBT&names=INAF"))
+                .andExpect(status().isNoContent());
+
+        verify(groupsService, times(1)).deleteGroup(eq(inaf));
+    }
+
+    @Test
+    public void testAddMember() throws Exception {
+
+        MembershipEntity membership = new MembershipEntity();
+        membership.setGroupId("group_id");
+        membership.setUserId("user_id");
+
+        when(membersService.addMember(eq("group_id"), eq("user_id")))
+                .thenReturn(membership);
+
+        mockMvc.perform(post("/ws/member")
+                .content(mapper.writeValueAsString(membership))
+                .contentType(MediaType.APPLICATION_JSON_UTF8))
+                .andExpect(status().isCreated())
+                .andExpect(jsonPath("$.groupId", is("group_id")))
+                .andExpect(jsonPath("$.userId", is("user_id")));
+    }
+
+    @Test
+    public void testRemoveMember() throws Exception {
+
+        List<String> names = getNames("LBT", "INAF");
+
+        GroupEntity inaf = getInafGroup();
+
+        when(groupsService.findGroupByNames(names)).thenReturn(Optional.of(inaf));
+
+        mockMvc.perform(delete("/ws/member?names=LBT&names=INAF&userId=user_id"))
+                .andExpect(status().isNoContent());
+
+        verify(membersService, times(1)).removeMember(eq(inaf.getId()), eq("user_id"));
+    }
+
+    @Test
+    public void testAddPermission() throws Exception {
+
+        List<String> names = getNames("LBT", "INAF");
+
+        AddPermissionWsRequest request = new AddPermissionWsRequest();
+        request.setNames(names);
+        request.setUserId("user_id");
+        request.setPermission(Permission.ADMIN);
+
+        GroupEntity inaf = getInafGroup();
+        when(groupsService.findGroupByNames(names)).thenReturn(Optional.of(inaf));
+
+        PermissionEntity permissionEntity = new PermissionEntity();
+        permissionEntity.setGroupId(inaf.getId());
+        permissionEntity.setUserId(request.getUserId());
+        permissionEntity.setPermission(request.getPermission());
+        permissionEntity.setGroupPath(inaf.getPath());
+
+        when(permissionsService.addPermission(eq(inaf), eq(request.getUserId()),
+                eq(request.getPermission()))).thenReturn(permissionEntity);
+
+        mockMvc.perform(post("/ws/permission")
+                .content(mapper.writeValueAsString(request))
+                .contentType(MediaType.APPLICATION_JSON_UTF8))
+                .andExpect(status().isCreated())
+                .andExpect(jsonPath("$.groupId", is(inaf.getId())))
+                .andExpect(jsonPath("$.userId", is(request.getUserId())))
+                .andExpect(jsonPath("$.permission", is("ADMIN")));
+
+        verify(permissionsService, times(1))
+                .addPermission(eq(inaf), eq(request.getUserId()), eq(request.getPermission()));
+    }
+
+    @Test
+    public void testDeletePermission() throws Exception {
+
+        List<String> names = getNames("LBT", "INAF");
+        GroupEntity inaf = getInafGroup();
+        when(groupsService.findGroupByNames(names)).thenReturn(Optional.of(inaf));
+
+        mockMvc.perform(delete("/ws/permission?names=LBT&names=INAF&userId=user_id&permission=ADMIN"))
+                .andExpect(status().isNoContent());
+
+        verify(permissionsService, times(1)).deletePermission(eq(inaf), eq("user_id"), eq(Permission.ADMIN));
+    }
+
+    @Test
+    public void testPrepareToJoin() throws Exception {
+
+        PrepareToJoinRequest request = new PrepareToJoinRequest();
+        request.setFromUserId("from_user");
+        request.setToUserId("to_user");
+
+        mockMvc.perform(post("/ws/prepare-join")
+                .content(mapper.writeValueAsString(request))
+                .contentType(MediaType.APPLICATION_JSON_UTF8))
+                .andExpect(status().isOk());
+
+        verify(permissionsService, times(1)).movePermissions(request.getFromUserId(), request.getToUserId());
+        verify(membersService, times(1)).moveMemberships(request.getFromUserId(), request.getToUserId());
+    }
+
+    private List<String> getNames(String... names) {
+        return Stream.of(names).collect(Collectors.toList());
+    }
+
+    private GroupEntity getInafGroup() {
+        GroupEntity inaf = new GroupEntity();
+        inaf.setId("inaf_id");
+        inaf.setName("INAF");
+        inaf.setPath("lbt_id.inaf_id");
+        return inaf;
+    }
+}
diff --git a/gms/src/test/java/it/inaf/ia2/gms/service/PermissionsServiceIntegrationTest.java b/gms/src/test/java/it/inaf/ia2/gms/service/PermissionsServiceIntegrationTest.java
index fc04b4e..07be295 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/service/PermissionsServiceIntegrationTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/service/PermissionsServiceIntegrationTest.java
@@ -67,6 +67,6 @@ public class PermissionsServiceIntegrationTest {
         assertEquals(Permission.ADMIN, permissions.get(0).getPermission());
         assertEquals(USER_ID, permissions.get(0).getUser().getId());
 
-        permissionsService.deletePermission(USER_ID, GroupsService.ROOT, Permission.ADMIN);
+        permissionsService.deletePermission(root, USER_ID, Permission.ADMIN);
     }
 }
-- 
GitLab