From 1e9783fdb319917f8ce94894dccdc62c4017e84e Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Mon, 21 Sep 2020 16:34:57 +0200
Subject: [PATCH] Added panel showing the pending invited registration
 requests; Added lock attribute on groups to avoid removal of special groups;
 Other improvements

---
 gms-ui/package.json                           |   4 +-
 gms-ui/src/api/server/index.js                |  16 ++-
 .../src/components/GenericSearchResults.vue   |  27 ++---
 gms-ui/src/components/GroupsPanel.vue         |   2 +-
 .../components/InvitedRegistrationPanel.vue   |  50 +++++++++
 gms-ui/src/components/Main.vue                |   3 +
 gms-ui/src/components/PermissionsPanel.vue    |   2 +-
 gms-ui/src/components/UserSearchResult.vue    |   2 +-
 .../src/components/modals/AddMemberModal.vue  |   2 +-
 .../components/modals/AddPermissionModal.vue  |   2 +-
 .../modals/ConfirmDeleteInvitedModal.vue      |  37 +++++++
 gms-ui/src/components/modals/SearchUser.vue   |   2 +-
 gms-ui/src/store.js                           |  17 +++-
 .../controller/GroupsTabResponseBuilder.java  |   6 ++
 .../InvitedRegistrationController.java        |   8 ++
 .../manager/InvitedRegistrationManager.java   |  47 +++++++++
 .../java/it/inaf/ia2/gms/model/GroupNode.java |   9 ++
 .../gms/model/response/GroupsTabResponse.java |   9 ++
 .../response/InvitedRegistrationItem.java     |  50 +++++++++
 .../inaf/ia2/gms/persistence/GroupsDAO.java   |  23 +++--
 .../persistence/InvitedRegistrationDAO.java   |  70 ++++++++++++-
 .../gms/persistence/model/GroupEntity.java    |   9 ++
 .../model/InvitedRegistration.java            |  14 ++-
 .../inaf/ia2/gms/service/GroupsService.java   |   6 +-
 .../ia2/gms/service/GroupsTreeBuilder.java    |   1 +
 gms/src/main/resources/sql/init.sql           |   1 +
 .../resources/static/help/help-admin.html     |   8 ++
 .../static/help/img/gms-invited-panel.jpg     | Bin 0 -> 28890 bytes
 .../GroupsTabResponseBuilderTest.java         |   6 +-
 .../InvitedRegistrationControllerTest.java    |  44 ++++++++
 .../InvitedRegistrationDAOTest.java           |  95 ++++++++++++++++++
 31 files changed, 537 insertions(+), 35 deletions(-)
 create mode 100644 gms-ui/src/components/InvitedRegistrationPanel.vue
 create mode 100644 gms-ui/src/components/modals/ConfirmDeleteInvitedModal.vue
 create mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/response/InvitedRegistrationItem.java
 create mode 100644 gms/src/main/resources/static/help/img/gms-invited-panel.jpg
 create mode 100644 gms/src/test/java/it/inaf/ia2/gms/controller/InvitedRegistrationControllerTest.java
 create mode 100644 gms/src/test/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAOTest.java

diff --git a/gms-ui/package.json b/gms-ui/package.json
index 287b62f..65869b3 100644
--- a/gms-ui/package.json
+++ b/gms-ui/package.json
@@ -45,7 +45,9 @@
       "plugin:vue/essential",
       "eslint:recommended"
     ],
-    "rules": {},
+    "rules": {
+      "no-console": "warn"
+    },
     "parserOptions": {
       "parser": "babel-eslint"
     }
diff --git a/gms-ui/src/api/server/index.js b/gms-ui/src/api/server/index.js
index f7079c8..cfb352e 100644
--- a/gms-ui/src/api/server/index.js
+++ b/gms-ui/src/api/server/index.js
@@ -17,7 +17,7 @@ function apiRequest(options, showLoading = true, handleValidationErrors = false)
         loading(false);
       })
       .catch(error => {
-        if(handleValidationErrors && error.response && error.response.status === 400) {
+        if (handleValidationErrors && error.response && error.response.status === 400) {
           reject(error.response.data);
         } else {
           dispatchApiErrorEvent(error);
@@ -363,5 +363,19 @@ export default {
         'Cache-Control': 'no-cache'
       }
     }, false);
+  },
+  deleteInvitedRegistration(requestId, groupId) {
+    let url = BASE_API_URL + 'registration?' +
+      'request_id=' + requestId + '&group_id=' + groupId;
+    return apiRequest({
+      method: 'DELETE',
+      url: url,
+      withCredentials: true,
+      headers: {
+        'Content-Type': 'application/json',
+        'Accept': 'application/json',
+        'Cache-Control': 'no-cache'
+      }
+    });
   }
 };
diff --git a/gms-ui/src/components/GenericSearchResults.vue b/gms-ui/src/components/GenericSearchResults.vue
index e0277a7..ea163ca 100644
--- a/gms-ui/src/components/GenericSearchResults.vue
+++ b/gms-ui/src/components/GenericSearchResults.vue
@@ -1,18 +1,21 @@
 <template>
 <div class="mt-sm-3">
   <div>
-    <p>Search results:</p>
-    <b-list-group v-for="item in model.genericSearchResults.items" v-bind:key="item.id">
-      <b-list-group-item href="#" v-on:click="openSearchResult(item)">
-        <span class="float-left">
-          <font-awesome-icon icon="folder" v-if="item.type === 'GROUP'"></font-awesome-icon>
-          <font-awesome-icon icon="user" v-if="item.type === 'USER'"></font-awesome-icon>
-          {{item.label}}
-        </span>
-      </b-list-group-item>
-    </b-list-group>
-    <Paginator :paginatedPanel="model.genericSearchResults" :onUpdate="updateSearchResults" :paginatorInput="input.genericSearch" />
+    <div v-if="model.genericSearchResults.items && model.genericSearchResults.items.length > 0">
+      <p>Search results:</p>
+      <b-list-group v-for="item in model.genericSearchResults.items" v-bind:key="item.id">
+        <b-list-group-item href="#" v-on:click="openSearchResult(item)">
+          <span class="float-left">
+            <font-awesome-icon icon="folder" v-if="item.type === 'GROUP'"></font-awesome-icon>
+            <font-awesome-icon icon="user" v-if="item.type === 'USER'"></font-awesome-icon>
+            {{item.label}}
+          </span>
+        </b-list-group-item>
+      </b-list-group>
+      <Paginator :paginatedPanel="model.genericSearchResults" :onUpdate="updateSearchResults" :paginatorInput="input.genericSearch" />
+    </div>
   </div>
+  <p v-if="model.genericSearchResults.items && model.genericSearchResults.items.length === 0">No entries were found matching your search</p>
 </div>
 </template>
 
@@ -29,7 +32,7 @@ export default {
     model: state => state.model,
     input: state => state.input
   }),
-  created () {
+  created() {
     this.updateSearchResults();
   },
   watch: {
diff --git a/gms-ui/src/components/GroupsPanel.vue b/gms-ui/src/components/GroupsPanel.vue
index 748119a..b77bcb1 100644
--- a/gms-ui/src/components/GroupsPanel.vue
+++ b/gms-ui/src/components/GroupsPanel.vue
@@ -14,7 +14,7 @@
             <font-awesome-icon icon="edit"></font-awesome-icon>
           </a>
           &nbsp;
-          <a href="#" v-on:click.stop.prevent="openRemoveGroupModal(group)" class="text-danger" title="Delete">
+          <a href="#" v-on:click.stop.prevent="openRemoveGroupModal(group)" class="text-danger" title="Delete" v-if="!group.locked">
             <font-awesome-icon icon="trash"></font-awesome-icon>
           </a>
         </span>
diff --git a/gms-ui/src/components/InvitedRegistrationPanel.vue b/gms-ui/src/components/InvitedRegistrationPanel.vue
new file mode 100644
index 0000000..a087bc6
--- /dev/null
+++ b/gms-ui/src/components/InvitedRegistrationPanel.vue
@@ -0,0 +1,50 @@
+<template>
+<b-tab :title="'Invited (' + registrations.length + ')'" :title-link-class="{ 'd-none': registrations.length === 0 }">
+  <table class="table b-table table-striped table-hover" v-if="registrations">
+    <thead>
+      <tr>
+        <th>Email</th>
+        <th>Permission</th>
+        <th>Submitted at</th>
+        <th></th>
+      </tr>
+    </thead>
+    <tbody>
+      <tr v-for="(reg, index) in registrations" v-bind:key="index">
+        <td>{{reg.email}}</td>
+        <td>{{reg.permission}}</td>
+        <td>{{reg.creationTime}}</td>
+        <td>
+          <a href="#" v-on:click.stop="openConfirmDeleteInvitedModal(reg)" class="text-danger" title="Remove permission">
+            <font-awesome-icon icon="trash"></font-awesome-icon>
+          </a>
+        </td>
+      </tr>
+    </tbody>
+  </table>
+  <ConfirmDeleteInvitedModal ref="confirmDeleteInvitedModal" />
+</b-tab>
+</template>
+
+<script>
+import ConfirmDeleteInvitedModal from './modals/ConfirmDeleteInvitedModal.vue';
+
+export default {
+  name: 'InvitedRegistrationPanel',
+  components: {
+    ConfirmDeleteInvitedModal
+  },
+  computed: {
+    registrations() {
+      return this.$store.state.model.invitedRegistrations === null ? [] : this.$store.state.model.invitedRegistrations;
+    }
+  },
+  methods: {
+    openConfirmDeleteInvitedModal(reg) {
+      let breadcrumbs = this.$store.state.model.breadcrumbs;
+      let currentGroupId = breadcrumbs[breadcrumbs.length - 1].groupId;
+      this.$refs.confirmDeleteInvitedModal.openConfirmDeleteInvitedModal(reg, currentGroupId);
+    }
+  }
+}
+</script>
diff --git a/gms-ui/src/components/Main.vue b/gms-ui/src/components/Main.vue
index 7c7829f..189dcac 100644
--- a/gms-ui/src/components/Main.vue
+++ b/gms-ui/src/components/Main.vue
@@ -6,6 +6,7 @@
       <GroupsPanel />
       <MembersPanel />
       <PermissionsPanel />
+      <InvitedRegistrationPanel />
       <template slot="tabs-end">
         <b-button variant="primary" class="in-tabs-header-btn" v-if="showAddGroupBtn" v-on:click="openAddGroupModal">Add group</b-button>
         <b-button variant="primary" class="in-tabs-header-btn" v-if="showAddMemberBtn" v-on:click="openAddMemberModal">Add member</b-button>
@@ -25,6 +26,7 @@ import GroupsBreadcrumb from './GroupsBreadcrumb.vue'
 import GroupsPanel from './GroupsPanel.vue'
 import MembersPanel from './MembersPanel.vue'
 import PermissionsPanel from './PermissionsPanel.vue'
+import InvitedRegistrationPanel from './InvitedRegistrationPanel.vue'
 import AddGroupModal from './modals/AddGroupModal.vue'
 import AddMemberModal from './modals/AddMemberModal.vue'
 import AddPermissionModal from './modals/AddPermissionModal.vue'
@@ -37,6 +39,7 @@ export default {
     GroupsPanel,
     MembersPanel,
     PermissionsPanel,
+    InvitedRegistrationPanel,
     AddGroupModal,
     AddMemberModal,
     AddPermissionModal
diff --git a/gms-ui/src/components/PermissionsPanel.vue b/gms-ui/src/components/PermissionsPanel.vue
index c6631b8..88f2aaa 100644
--- a/gms-ui/src/components/PermissionsPanel.vue
+++ b/gms-ui/src/components/PermissionsPanel.vue
@@ -1,7 +1,7 @@
 <template>
 <b-tab title="Permissions" :title-link-class="{ 'd-none': (model.permission !== 'ADMIN') }">
   <div v-if="model.permissionsPanel !== null">
-    <table class="table b-table table-striped table-hover">
+    <table class="table b-table table-striped table-hover" v-if="model.permissionsPanel.items.length > 0">
       <thead>
         <tr>
           <th>User</th>
diff --git a/gms-ui/src/components/UserSearchResult.vue b/gms-ui/src/components/UserSearchResult.vue
index d5ccf14..0f95b90 100644
--- a/gms-ui/src/components/UserSearchResult.vue
+++ b/gms-ui/src/components/UserSearchResult.vue
@@ -29,7 +29,7 @@
         <b-row>
           <b-col lg="10">
             <b-list-group>
-              <b-list-group-item v-for="(identity, index) in user.identities" v-bind:key="index">
+              <b-list-group-item :class="{ 'list-group-item-info': identity.primary }" v-for="(identity, index) in user.identities" v-bind:key="index">
                 <dl class="mb-0 ml-0 row">
                   <dt class="col-3">Type</dt><dd class="col-9">{{identity.type}}</dd>
                   <dt class="col-3">Email</dt><dd class="col-9">{{identity.email}}</dd>
diff --git a/gms-ui/src/components/modals/AddMemberModal.vue b/gms-ui/src/components/modals/AddMemberModal.vue
index 13e91d9..11634bd 100644
--- a/gms-ui/src/components/modals/AddMemberModal.vue
+++ b/gms-ui/src/components/modals/AddMemberModal.vue
@@ -1,5 +1,5 @@
 <template>
-<b-modal id="add-member-modal" title="Add member" ok-title="Add" @shown="afterShow" @ok="addMember">
+<b-modal id="add-member-modal" title="Add member" ok-title="Add" @shown="afterShow" @ok="addMember" size="lg">
   <SearchUser ref="searchUser" @searchUserEnter="addMember" />
 </b-modal>
 </template>
diff --git a/gms-ui/src/components/modals/AddPermissionModal.vue b/gms-ui/src/components/modals/AddPermissionModal.vue
index b210f47..e988ee3 100644
--- a/gms-ui/src/components/modals/AddPermissionModal.vue
+++ b/gms-ui/src/components/modals/AddPermissionModal.vue
@@ -1,5 +1,5 @@
 <template>
-<b-modal id="add-permission-modal" title="Add permission" @show="beforeShow" @shown="afterShow" :ok-title="okTitle" @ok="addPermission" :ok-variant="okBtnVariant">
+<b-modal id="add-permission-modal" title="Add permission" @show="beforeShow" @shown="afterShow" :ok-title="okTitle" @ok="addPermission" :ok-variant="okBtnVariant" size="lg">
   <SearchUser ref="searchUser" @searchUserEnter="addPermission" />
   <b-alert :show="!!existingPermission" variant="warning" class="mt-3">
     <strong>Warning</strong>: the user has already a permission ({{existingPermission}}). Click confirm to override it.
diff --git a/gms-ui/src/components/modals/ConfirmDeleteInvitedModal.vue b/gms-ui/src/components/modals/ConfirmDeleteInvitedModal.vue
new file mode 100644
index 0000000..e248834
--- /dev/null
+++ b/gms-ui/src/components/modals/ConfirmDeleteInvitedModal.vue
@@ -0,0 +1,37 @@
+<template>
+<b-modal id="confirm-delete-invited-modal" title="Confirm action" ok-title="Delete" @ok="deleteInvitedRegistration" ok-variant="danger">
+  <p v-if="invitedRegistration">Are you sure that you want to remove the invited registration for {{invitedRegistration.email}}?</p>
+</b-modal>
+</template>
+
+<script>
+import client from 'api-client';
+
+export default {
+  name: 'ConfirmDeleteInvitedModal',
+  data: function() {
+    return {
+      invitedRegistration: null,
+      groupId: null
+    }
+  },
+  methods: {
+    openConfirmDeleteInvitedModal(invitedRegistration, groupId) {
+      this.invitedRegistration = invitedRegistration;
+      this.groupId = groupId;
+      this.$bvModal.show('confirm-delete-invited-modal');
+    },
+    deleteInvitedRegistration() {
+      let regId = this.invitedRegistration.id;
+      client.deleteInvitedRegistration(regId, this.groupId)
+        .then(() => {
+          let model = this.$store.state.model;
+          this.$store.commit('removeInvitedRegistration', regId);
+          if (model.invitedRegistrations.length === 0) {
+            this.$store.commit('setTabIndex', model.leaf ? 1 : 0);
+          }
+        });
+    }
+  }
+}
+</script>
diff --git a/gms-ui/src/components/modals/SearchUser.vue b/gms-ui/src/components/modals/SearchUser.vue
index a6b0581..5f96f4b 100644
--- a/gms-ui/src/components/modals/SearchUser.vue
+++ b/gms-ui/src/components/modals/SearchUser.vue
@@ -50,7 +50,7 @@ export default {
             let user = res[i];
             this.users.push({
               value: user.id,
-              text: user.displayName
+              text: user.displayName + ' [' + user.id + ']'
             });
           }
           if (this.users.length > 0) {
diff --git a/gms-ui/src/store.js b/gms-ui/src/store.js
index fe02ab5..a0e8e38 100644
--- a/gms-ui/src/store.js
+++ b/gms-ui/src/store.js
@@ -15,6 +15,7 @@ export default new Vuex.Store({
       groupsPanel: null,
       permissionsPanel: null,
       membersPanel: null,
+      invitedRegistrations: null,
       permission: null,
       leaf: false,
       user: null,
@@ -45,12 +46,14 @@ export default new Vuex.Store({
       state.model.breadcrumbs = model.breadcrumbs;
       state.model.groupsPanel = model.groupsPanel;
       state.model.permission = model.permission;
+      state.model.invitedRegistrations = model.invitedRegistrations;
       state.model.user = model.user;
     },
     updateGroups(state, model) {
       state.model.breadcrumbs = model.breadcrumbs;
       state.model.groupsPanel = model.groupsPanel;
       state.model.permission = model.permission;
+      state.model.invitedRegistrations = model.invitedRegistrations;
       state.model.leaf = model.leaf;
     },
     updateGroupsPanel(state, groupsPanel) {
@@ -59,7 +62,7 @@ export default new Vuex.Store({
     },
     updatePermissionsPanel(state, permissionsPanel) {
       state.model.permissionsPanel = permissionsPanel;
-      for(let up of permissionsPanel.items) {
+      for (let up of permissionsPanel.items) {
         Vue.set(up, 'editable', false);
       }
       state.input.paginatorPage = permissionsPanel.currentPage;
@@ -94,6 +97,18 @@ export default new Vuex.Store({
     },
     setGenericSearchFilter(state, filter) {
       state.input.genericSearch.filter = filter;
+    },
+    removeInvitedRegistration(state, regId) {
+      let index = -1;
+      for (var i = 0; i < state.model.invitedRegistrations.length; i++) {
+        if (state.model.invitedRegistrations[i].id === regId) {
+          index = i;
+          break;
+        }
+      }
+      if (index !== -1) {
+        state.model.invitedRegistrations.splice(index, 1);
+      }
     }
   },
   actions: {
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java
index f560d5d..2718b19 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilder.java
@@ -2,6 +2,7 @@ package it.inaf.ia2.gms.controller;
 
 import it.inaf.ia2.gms.authn.SessionData;
 import it.inaf.ia2.gms.manager.GroupsManager;
+import it.inaf.ia2.gms.manager.InvitedRegistrationManager;
 import it.inaf.ia2.gms.manager.PermissionsManager;
 import it.inaf.ia2.gms.model.Permission;
 import it.inaf.ia2.gms.model.request.GroupsRequest;
@@ -30,6 +31,9 @@ public class GroupsTabResponseBuilder {
     @Autowired
     private GroupsTreeBuilder groupsListBuilder;
 
+    @Autowired
+    private InvitedRegistrationManager invitedRegistrationManager;
+
     public GroupsTabResponse getGroupsTab(GroupsRequest request) {
 
         GroupEntity group = groupsService.getGroupById(request.getGroupId());
@@ -46,6 +50,8 @@ public class GroupsTabResponseBuilder {
 
         response.setLeaf(group.isLeaf());
 
+        response.setInvitedRegistrations(invitedRegistrationManager.getInvitedRegistrationsForGroup(group));
+
         return response;
     }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java b/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java
index 0a4554c..82dd95a 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/controller/InvitedRegistrationController.java
@@ -10,7 +10,9 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
 import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
@@ -55,6 +57,12 @@ public class InvitedRegistrationController {
         }
     }
 
+    @DeleteMapping(value = "/registration", produces = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<?> deleteInvitedRegistration(@RequestParam("request_id") String requestId, @RequestParam("group_id") String groupId) {
+        invitedRegistrationManager.deleteInvitedRegistration(requestId, groupId);
+        return ResponseEntity.noContent().build();
+    }
+
     private String getFileContent(String templateFileName) throws IOException {
         try (InputStream in = InvitedRegistrationController.class.getClassLoader().getResourceAsStream("templates/" + templateFileName)) {
             Scanner s = new Scanner(in).useDelimiter("\\A");
diff --git a/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java b/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java
index 73bdea1..f30dca3 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/manager/InvitedRegistrationManager.java
@@ -1,8 +1,10 @@
 package it.inaf.ia2.gms.manager;
 
+import it.inaf.ia2.gms.exception.BadRequestException;
 import it.inaf.ia2.gms.exception.NotFoundException;
 import it.inaf.ia2.gms.exception.UnauthorizedException;
 import it.inaf.ia2.gms.model.Permission;
+import it.inaf.ia2.gms.model.response.InvitedRegistrationItem;
 import it.inaf.ia2.gms.persistence.GroupsDAO;
 import it.inaf.ia2.gms.persistence.InvitedRegistrationDAO;
 import it.inaf.ia2.gms.persistence.LoggingDAO;
@@ -14,8 +16,10 @@ import it.inaf.ia2.gms.service.PermissionsService;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
+import java.util.ArrayList;
 import java.util.Base64;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import java.util.UUID;
@@ -119,4 +123,47 @@ public class InvitedRegistrationManager extends UserAwareComponent {
 
         return Optional.ofNullable(invitedRegistration);
     }
+
+    public List<InvitedRegistrationItem> getInvitedRegistrationsForGroup(GroupEntity group) {
+
+        if (permissionsManager.getCurrentUserPermission(group) != Permission.ADMIN) {
+            return null;
+        }
+
+        List<InvitedRegistrationItem> items = new ArrayList<>();
+
+        for (InvitedRegistration reg : invitedRegistrationDAO.getPendingInvitedRegistrationsForGroup(group.getId())) {
+
+            Map<String, Permission> map = reg.getGroupsPermissions();
+
+            if (map != null) {
+                for (Permission permission : map.values()) {
+                    InvitedRegistrationItem item = new InvitedRegistrationItem()
+                            .setId(reg.getId())
+                            .setEmail(reg.getEmail())
+                            .setPermission(permission)
+                            .setCreationTime(reg.getCreationTime());
+                    items.add(item);
+                }
+            }
+        }
+
+        return items;
+    }
+
+    public void deleteInvitedRegistration(String registrationId, String groupId) {
+
+        GroupEntity group = groupsDAO.findGroupById(groupId)
+                .orElseThrow(() -> new BadRequestException("No group found for given id: " + groupId));
+
+        if (permissionsManager.getUserPermission(group, getCurrentUserId()) != Permission.ADMIN) {
+            throw new UnauthorizedException("Only administrators can delete invited registrations!");
+        }
+
+        invitedRegistrationDAO.deleteInvitedRegistrationRequest(registrationId, groupId);
+
+        loggingDAO.logAction("Deleted invited registration request. "
+                + "[request_id=" + registrationId + ", group_id=" + groupId
+                + ", group_name=" + group.getName() + "]");
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java b/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java
index 381d846..b7237fc 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/GroupNode.java
@@ -7,6 +7,7 @@ public class GroupNode {
     private Permission permission;
     private boolean hasChildren;
     private boolean leaf;
+    private boolean locked;
 
     public String getGroupId() {
         return groupId;
@@ -47,4 +48,12 @@ public class GroupNode {
     public void setLeaf(boolean leaf) {
         this.leaf = leaf;
     }
+
+    public boolean isLocked() {
+        return locked;
+    }
+
+    public void setLocked(boolean locked) {
+        this.locked = locked;
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/response/GroupsTabResponse.java b/gms/src/main/java/it/inaf/ia2/gms/model/response/GroupsTabResponse.java
index 0eb8665..d3d75da 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/model/response/GroupsTabResponse.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/response/GroupsTabResponse.java
@@ -11,6 +11,7 @@ public class GroupsTabResponse {
     private PaginatedData<GroupNode> groupsPanel;
     // current group permissions
     private Permission permission;
+    private List<InvitedRegistrationItem> invitedRegistrations;
 
     private boolean leaf;
 
@@ -45,4 +46,12 @@ public class GroupsTabResponse {
     public void setLeaf(boolean leaf) {
         this.leaf = leaf;
     }
+
+    public List<InvitedRegistrationItem> getInvitedRegistrations() {
+        return invitedRegistrations;
+    }
+
+    public void setInvitedRegistrations(List<InvitedRegistrationItem> invitedRegistrations) {
+        this.invitedRegistrations = invitedRegistrations;
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/response/InvitedRegistrationItem.java b/gms/src/main/java/it/inaf/ia2/gms/model/response/InvitedRegistrationItem.java
new file mode 100644
index 0000000..cc31f77
--- /dev/null
+++ b/gms/src/main/java/it/inaf/ia2/gms/model/response/InvitedRegistrationItem.java
@@ -0,0 +1,50 @@
+package it.inaf.ia2.gms.model.response;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import it.inaf.ia2.gms.model.Permission;
+import java.util.Date;
+
+public class InvitedRegistrationItem {
+
+    private String id;
+    private String email;
+    private Permission permission;
+    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd")
+    private Date creationTime;
+
+    public String getId() {
+        return id;
+    }
+
+    public InvitedRegistrationItem setId(String id) {
+        this.id = id;
+        return this;
+    }
+
+    public String getEmail() {
+        return email;
+    }
+
+    public InvitedRegistrationItem setEmail(String email) {
+        this.email = email;
+        return this;
+    }
+
+    public Permission getPermission() {
+        return permission;
+    }
+
+    public InvitedRegistrationItem setPermission(Permission permission) {
+        this.permission = permission;
+        return this;
+    }
+
+    public Date getCreationTime() {
+        return creationTime;
+    }
+
+    public InvitedRegistrationItem setCreationTime(Date creationTime) {
+        this.creationTime = creationTime;
+        return this;
+    }
+}
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 4228593..3cbffa5 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
@@ -84,7 +84,7 @@ public class GroupsDAO {
 
     public Optional<GroupEntity> findGroupById(String groupId) {
 
-        String sql = "SELECT id, name, path, is_leaf from gms_group WHERE id = ?";
+        String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE id = ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
@@ -97,6 +97,7 @@ public class GroupsDAO {
                 group.setName(resultSet.getString("name"));
                 group.setPath(resultSet.getString("path"));
                 group.setLeaf(resultSet.getBoolean("is_leaf"));
+                group.setLocked(resultSet.getBoolean("locked"));
                 return Optional.of(group);
             }
             return Optional.empty();
@@ -105,7 +106,7 @@ public class GroupsDAO {
 
     public Optional<GroupEntity> findGroupByPath(String path) {
 
-        String sql = "SELECT id, name, is_leaf from gms_group WHERE path = ?";
+        String sql = "SELECT id, name, is_leaf, locked from gms_group WHERE path = ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
@@ -117,6 +118,7 @@ public class GroupsDAO {
                 group.setId(resultSet.getString("id"));
                 group.setName(resultSet.getString("name"));
                 group.setLeaf(resultSet.getBoolean("is_leaf"));
+                group.setLocked(resultSet.getBoolean("locked"));
                 group.setPath(path);
                 return Optional.of(group);
             }
@@ -132,7 +134,7 @@ public class GroupsDAO {
 
         return jdbcTemplate.query(conn -> {
 
-            String sql = "SELECT id, name, path, is_leaf from gms_group WHERE id IN (";
+            String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE id IN (";
             sql += String.join(",", identifiers.stream().map(p -> "?").collect(Collectors.toList()));
             sql += ")";
 
@@ -150,6 +152,7 @@ public class GroupsDAO {
                 group.setName(resultSet.getString("name"));
                 group.setPath(resultSet.getString("path"));
                 group.setLeaf(resultSet.getBoolean("is_leaf"));
+                group.setLocked(resultSet.getBoolean("locked"));
                 groups.add(group);
             }
             return groups;
@@ -158,7 +161,7 @@ public class GroupsDAO {
 
     public Optional<GroupEntity> findGroupByParentAndName(String parentPath, String childName) {
 
-        String sql = "SELECT id, path, is_leaf from gms_group WHERE name = ? AND path ~ ?";
+        String sql = "SELECT id, path, is_leaf, locked from gms_group WHERE name = ? AND path ~ ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
@@ -172,6 +175,7 @@ public class GroupsDAO {
                 group.setName(childName);
                 group.setPath(resultSet.getString("path"));
                 group.setLeaf(resultSet.getBoolean("is_leaf"));
+                group.setLocked(resultSet.getBoolean("locked"));
                 return Optional.of(group);
             }
             return Optional.empty();
@@ -191,9 +195,9 @@ public class GroupsDAO {
 
         String sql;
         if (hasSearchFilter) {
-            sql = "SELECT id, name, path, is_leaf FROM gms_group WHERE path ~ ? AND name ILIKE ? ORDER BY name";
+            sql = "SELECT id, name, path, is_leaf, locked FROM gms_group WHERE path ~ ? AND name ILIKE ? ORDER BY name";
         } else {
-            sql = "SELECT id, name, path, is_leaf FROM gms_group WHERE path ~ ? ORDER BY name";
+            sql = "SELECT id, name, path, is_leaf, locked FROM gms_group WHERE path ~ ? ORDER BY name";
         }
 
         return jdbcTemplate.query(conn -> {
@@ -218,7 +222,7 @@ public class GroupsDAO {
 
     public List<GroupEntity> getAllChildren(String path) {
 
-        String sql = "SELECT id, name, path, is_leaf FROM gms_group WHERE path <@ ? AND path <> ? ORDER BY nlevel(path) DESC";
+        String sql = "SELECT id, name, path, is_leaf, locked FROM gms_group WHERE path <@ ? AND path <> ? ORDER BY nlevel(path) DESC";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
@@ -232,7 +236,7 @@ public class GroupsDAO {
 
     public List<GroupEntity> findGroupsByNames(List<String> names) {
 
-        String sql = "SELECT id, name, path, is_leaf from gms_group WHERE name IN ("
+        String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE name IN ("
                 + String.join(",", names.stream().map(g -> "?").collect(Collectors.toList()))
                 + ")";
 
@@ -256,6 +260,7 @@ public class GroupsDAO {
             group.setName(resultSet.getString("name"));
             group.setPath(resultSet.getString("path"));
             group.setLeaf(resultSet.getBoolean("is_leaf"));
+            group.setLocked(resultSet.getBoolean("locked"));
             groups.add(group);
         }
         return groups;
@@ -326,7 +331,7 @@ public class GroupsDAO {
 
     public List<GroupEntity> searchGroups(String searchText) {
 
-        String sql = "SELECT id, name, path, is_leaf from gms_group WHERE name ILIKE ?";
+        String sql = "SELECT id, name, path, is_leaf, locked from gms_group WHERE name ILIKE ?";
 
         return jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sql);
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java
index af61db9..43415d6 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAO.java
@@ -4,6 +4,9 @@ import it.inaf.ia2.gms.model.Permission;
 import it.inaf.ia2.gms.persistence.model.InvitedRegistration;
 import java.sql.PreparedStatement;
 import java.sql.Types;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -54,7 +57,7 @@ public class InvitedRegistrationDAO {
 
     public Optional<InvitedRegistration> getInvitedRegistrationFromToken(String tokenHash) {
 
-        String sqlReq = "SELECT id, email FROM invited_registration_request WHERE token_hash = ? AND done IS NOT true";
+        String sqlReq = "SELECT id, email, creation_time FROM invited_registration_request WHERE token_hash = ? AND done IS NOT true";
 
         InvitedRegistration registration = jdbcTemplate.query(conn -> {
             PreparedStatement ps = conn.prepareStatement(sqlReq);
@@ -65,6 +68,7 @@ public class InvitedRegistrationDAO {
                 InvitedRegistration reg = new InvitedRegistration();
                 reg.setId(resultSet.getString("id"));
                 reg.setEmail(resultSet.getString("email"));
+                reg.setCreationTime(new Date(resultSet.getDate("creation_time").getTime()));
                 return reg;
             }
             return null;
@@ -126,7 +130,7 @@ public class InvitedRegistrationDAO {
             return;
         }
 
-        String sql = "DELETE FROM invited_registration_request_group WHERE group_id = ("
+        String sql = "DELETE FROM invited_registration_request_group WHERE group_id IN ("
                 + String.join(",", groupIds.stream().map(g -> "?").collect(Collectors.toList()))
                 + ")";
 
@@ -143,4 +147,66 @@ public class InvitedRegistrationDAO {
         jdbcTemplate.update("DELETE FROM invited_registration_request WHERE id NOT IN "
                 + "(SELECT request_id FROM invited_registration_request_group)");
     }
+
+    public List<InvitedRegistration> getPendingInvitedRegistrationsForGroup(String groupId) {
+
+        String sql = "SELECT id, email, creation_time, permission\n"
+                + "FROM invited_registration_request r\n"
+                + "JOIN invited_registration_request_group rg ON r.id = rg.request_id\n"
+                + "WHERE done IS NOT TRUE AND rg.group_id = ?";
+
+        return jdbcTemplate.query(sql,
+                ps -> {
+                    ps.setString(1, groupId);
+                },
+                rs -> {
+                    // key: id
+                    Map<String, InvitedRegistration> map = new HashMap<>();
+
+                    while (rs.next()) {
+
+                        String id = rs.getString("id");
+
+                        InvitedRegistration reg = map.get(id);
+                        if (reg == null) {
+                            String email = rs.getString("email");
+                            Date creationTime = new Date(rs.getDate("creation_time").getTime());
+                            reg = new InvitedRegistration()
+                                    .setId(id)
+                                    .setEmail(email)
+                                    .setCreationTime(creationTime);
+                            map.put(id, reg);
+                        }
+
+                        if (reg.getGroupsPermissions() == null) {
+                            reg.setGroupsPermissions(new HashMap<>());
+                        }
+
+                        Permission permission = Permission.valueOf(rs.getString("permission"));
+
+                        reg.getGroupsPermissions().put(groupId, permission);
+                    }
+
+                    List<InvitedRegistration> registrations = new ArrayList<>(map.values());
+
+                    Collections.sort(registrations, (reg1, reg2) -> reg1.getEmail().compareToIgnoreCase(reg2.getEmail()));
+
+                    return registrations;
+                });
+    }
+
+    public void deleteInvitedRegistrationRequest(String requestId, String groupId) {
+
+        String sql = "DELETE FROM invited_registration_request_group\n"
+                + "WHERE request_id = ? AND group_id = ?";
+
+        jdbcTemplate.update(sql, ps -> {
+            ps.setString(1, requestId);
+            ps.setString(2, groupId);
+        });
+
+        // Cleanup orphan invited requests
+        jdbcTemplate.update("DELETE FROM invited_registration_request WHERE id NOT IN "
+                + "(SELECT request_id FROM invited_registration_request_group)");
+    }
 }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/GroupEntity.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/GroupEntity.java
index 1dc3ed1..20590cf 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/GroupEntity.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/GroupEntity.java
@@ -8,6 +8,7 @@ public class GroupEntity {
     private String name;
     private String path;
     private boolean leaf;
+    private boolean locked;
 
     public String getId() {
         return id;
@@ -41,6 +42,14 @@ public class GroupEntity {
         this.leaf = leaf;
     }
 
+    public boolean isLocked() {
+        return locked;
+    }
+
+    public void setLocked(boolean locked) {
+        this.locked = locked;
+    }
+
     public String getParentPath() {
         if (path.isEmpty()) {
             return null;
diff --git a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java
index e7a922b..9acc15e 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/persistence/model/InvitedRegistration.java
@@ -1,6 +1,7 @@
 package it.inaf.ia2.gms.persistence.model;
 
 import it.inaf.ia2.gms.model.Permission;
+import java.util.Date;
 import java.util.Map;
 
 public class InvitedRegistration {
@@ -10,6 +11,7 @@ public class InvitedRegistration {
     private String email;
     private boolean done;
     private String userId;
+    private Date creationTime;
     private Map<String, Permission> groupsPermissions;
 
     public String getId() {
@@ -52,8 +54,18 @@ public class InvitedRegistration {
         return userId;
     }
 
-    public void setUserId(String userId) {
+    public InvitedRegistration setUserId(String userId) {
         this.userId = userId;
+        return this;
+    }
+
+    public Date getCreationTime() {
+        return creationTime;
+    }
+
+    public InvitedRegistration setCreationTime(Date creationTime) {
+        this.creationTime = creationTime;
+        return this;
     }
 
     public Map<String, Permission> getGroupsPermissions() {
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 14d503e..f38717f 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
@@ -99,6 +99,10 @@ public class GroupsService {
             throw new UnauthorizedException("It is not possible to remove the ROOT");
         }
 
+        if (group.isLocked()) {
+            throw new UnauthorizedException("Group " + group.getId() + " is locked and can't be deleted");
+        }
+
         String parentPath = group.getParentPath();
         GroupEntity parent = groupsDAO.findGroupByPath(parentPath)
                 .orElseThrow(() -> new BadRequestException("No group found at path " + parentPath));
@@ -117,7 +121,7 @@ public class GroupsService {
             groupsDAO.deleteGroup(g);
         }
 
-        loggingDAO.logAction("Group deleted, group_id=" + group.getId());
+        loggingDAO.logAction("Group deleted [group_id=" + group.getId() + ", group_name=" + group.getName() + "]");
 
         return parent;
     }
diff --git a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java
index 80c5384..e3c2145 100644
--- a/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java
+++ b/gms/src/main/java/it/inaf/ia2/gms/service/GroupsTreeBuilder.java
@@ -60,6 +60,7 @@ public class GroupsTreeBuilder {
                 node.setGroupId(group.getId());
                 node.setGroupName(group.getName());
                 node.setLeaf(group.isLeaf());
+                node.setLocked(group.isLocked());
                 node.setPermission(permission);
                 nodes.add(node);
             });
diff --git a/gms/src/main/resources/sql/init.sql b/gms/src/main/resources/sql/init.sql
index 6f4693e..05aec53 100644
--- a/gms/src/main/resources/sql/init.sql
+++ b/gms/src/main/resources/sql/init.sql
@@ -5,6 +5,7 @@ CREATE TABLE gms_group (
   name text NOT NULL,
   path ltree NOT NULL,
   is_leaf boolean,
+  locked boolean,
   primary key(id)
 );
 
diff --git a/gms/src/main/resources/static/help/help-admin.html b/gms/src/main/resources/static/help/help-admin.html
index f452453..093446d 100644
--- a/gms/src/main/resources/static/help/help-admin.html
+++ b/gms/src/main/resources/static/help/help-admin.html
@@ -102,6 +102,14 @@
                 You can also use the edit icon (<img src="img/pencil-icon.jpg" alt="" />)
                 in order to change a permission.</p>
 
+            <h2 class="mt-4">Pending invited registrations</h2>
+            
+            <p>If a registration invitation has been sent and the user has not yet registered an additional tab appears:</p>
+            
+            <p>
+                <img src="img/gms-invited-panel.jpg" alt="" />
+            </p>
+            
             <h2 class="mt-4">Seeing information about users</h2>
 
             <p>You can click on the user names in the Members or Permissions tab in order to see a detailed page about a specific user.</p>
diff --git a/gms/src/main/resources/static/help/img/gms-invited-panel.jpg b/gms/src/main/resources/static/help/img/gms-invited-panel.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3f2b7d1cbe2bbac8dd040dfe569ffa9f99748b3c
GIT binary patch
literal 28890
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

literal 0
HcmV?d00001

diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java
index c4ac899..937fbe7 100644
--- a/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java
+++ b/gms/src/test/java/it/inaf/ia2/gms/controller/GroupsTabResponseBuilderTest.java
@@ -2,6 +2,7 @@ package it.inaf.ia2.gms.controller;
 
 import it.inaf.ia2.gms.authn.SessionData;
 import it.inaf.ia2.gms.manager.GroupsManager;
+import it.inaf.ia2.gms.manager.InvitedRegistrationManager;
 import it.inaf.ia2.gms.manager.PermissionsManager;
 import it.inaf.ia2.gms.model.GroupNode;
 import it.inaf.ia2.gms.model.Permission;
@@ -31,7 +32,7 @@ public class GroupsTabResponseBuilderTest {
 
     @Mock
     private GroupsManager groupsManager;
-    
+
     @Mock
     private GroupsService groupsService;
 
@@ -41,6 +42,9 @@ public class GroupsTabResponseBuilderTest {
     @Mock
     private GroupsTreeBuilder groupsTreeBuilder;
 
+    @Mock
+    private InvitedRegistrationManager invitedRegistrationManager;
+
     @InjectMocks
     private GroupsTabResponseBuilder groupsTabResponseBuilder;
 
diff --git a/gms/src/test/java/it/inaf/ia2/gms/controller/InvitedRegistrationControllerTest.java b/gms/src/test/java/it/inaf/ia2/gms/controller/InvitedRegistrationControllerTest.java
new file mode 100644
index 0000000..c6c1c2e
--- /dev/null
+++ b/gms/src/test/java/it/inaf/ia2/gms/controller/InvitedRegistrationControllerTest.java
@@ -0,0 +1,44 @@
+package it.inaf.ia2.gms.controller;
+
+import it.inaf.ia2.gms.manager.InvitedRegistrationManager;
+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 org.mockito.junit.MockitoJUnitRunner;
+import org.springframework.test.web.servlet.MockMvc;
+import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
+import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
+
+@RunWith(MockitoJUnitRunner.class)
+public class InvitedRegistrationControllerTest {
+
+    @Mock
+    private InvitedRegistrationManager manager;
+
+    @InjectMocks
+    private InvitedRegistrationController controller;
+
+    private MockMvc mockMvc;
+
+    @Before
+    public void init() {
+        mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
+    }
+
+    @Test
+    public void testDeleteInvitedRegistration() throws Exception {
+
+        mockMvc.perform(delete("/registration?request_id=req1&group_id=group1"))
+                .andDo(print())
+                .andExpect(status().isNoContent());
+
+        verify(manager, times(1)).deleteInvitedRegistration(eq("req1"), eq("group1"));
+    }
+}
diff --git a/gms/src/test/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAOTest.java b/gms/src/test/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAOTest.java
new file mode 100644
index 0000000..555f27b
--- /dev/null
+++ b/gms/src/test/java/it/inaf/ia2/gms/persistence/InvitedRegistrationDAOTest.java
@@ -0,0 +1,95 @@
+package it.inaf.ia2.gms.persistence;
+
+import it.inaf.ia2.gms.DataSourceConfig;
+import it.inaf.ia2.gms.HooksConfig;
+import it.inaf.ia2.gms.model.Permission;
+import it.inaf.ia2.gms.persistence.model.GroupEntity;
+import it.inaf.ia2.gms.persistence.model.InvitedRegistration;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import javax.sql.DataSource;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@ContextConfiguration(classes = {DataSourceConfig.class, HooksConfig.class})
+public class InvitedRegistrationDAOTest {
+
+    @Autowired
+    private DataSource dataSource;
+
+    private GroupsDAO groupsDAO;
+
+    private InvitedRegistrationDAO dao;
+
+    @Before
+    public void setUp() {
+        groupsDAO = new GroupsDAO(dataSource);
+        dao = new InvitedRegistrationDAO(dataSource);
+    }
+
+    @Test
+    public void test() {
+
+        GroupEntity root = new GroupEntity();
+        root.setId("ROOT");
+        root.setName("ROOT");
+        root.setPath("");
+        groupsDAO.createGroup(root);
+
+        GroupEntity group1 = new GroupEntity();
+        group1.setId("group1");
+        group1.setName("Group1");
+        group1.setPath(group1.getId());
+        groupsDAO.createGroup(group1);
+
+        GroupEntity group2 = new GroupEntity();
+        group2.setId("group2");
+        group2.setName("Group2");
+        group2.setPath(group2.getId());
+        groupsDAO.createGroup(group2);
+
+        Map<String, Permission> groupsPermissions = new HashMap<>();
+        groupsPermissions.put("group1", Permission.VIEW_MEMBERS);
+        groupsPermissions.put("group2", Permission.MANAGE_MEMBERS);
+
+        InvitedRegistration reg = new InvitedRegistration()
+                .setId("id1")
+                .setEmail("test@inaf.it")
+                .setTokenHash("token_hash")
+                .setGroupsPermissions(groupsPermissions);
+
+        dao.addInvitedRegistration(reg);
+
+        InvitedRegistration regFromToken = dao.getInvitedRegistrationFromToken("token_hash").get();
+        assertEquals(reg.getId(), regFromToken.getId());
+        assertEquals(reg.getEmail(), regFromToken.getEmail());
+        assertNotNull(regFromToken.getCreationTime());
+
+        InvitedRegistration regFromGroup = dao.getPendingInvitedRegistrationsForGroup("group1").get(0);
+        assertEquals(reg.getId(), regFromGroup.getId());
+        assertEquals(reg.getEmail(), regFromGroup.getEmail());
+        assertNotNull(regFromGroup.getCreationTime());
+
+        dao.setRegistrationDone(regFromGroup);
+
+        assertTrue(dao.getPendingInvitedRegistrationsForGroup("group1").isEmpty());
+
+        List<String> groupsIds = new ArrayList<>();
+        groupsIds.add(group1.getId());
+        groupsIds.add(group2.getId());
+        dao.deleteAllGroupsInvitedRegistrations(groupsIds);
+
+        groupsDAO.deleteGroup(group1);
+        groupsDAO.deleteGroup(group2);
+    }
+}
-- 
GitLab