From d313d3e9a328e457bbb25bc2b8a6c748b5802267 Mon Sep 17 00:00:00 2001
From: Sonia Zorba <sonia.zorba@inaf.it>
Date: Mon, 26 Oct 2020 18:05:56 +0100
Subject: [PATCH] GMS client improvements

---
 .gitignore                                    |   4 +-
 gms-client/gms-cli/gms.properties             |   6 +-
 gms-client/gms-cli/pom.xml                    |   2 +-
 .../main/java/it/inaf/ia2/gms/cli/CLI.java    |  28 +-
 gms-client/gms-client-lib/pom.xml             |  32 ---
 .../it/inaf/ia2/gms/client/GmsClient.java     |  91 -------
 .../inaf/ia2/gms/client/GmsClientBuilder.java |  40 ---
 .../ia2/gms/client/call/AddMemberCall.java    |  35 ---
 .../gms/client/call/AddPermissionCall.java    |  40 ---
 .../inaf/ia2/gms/client/call/BaseGmsCall.java |  44 ---
 .../ia2/gms/client/call/CreateGroupCall.java  |  30 ---
 .../ia2/gms/client/call/DeleteGroupCall.java  |  28 --
 .../gms/client/call/HttpClientWrapper.java    | 105 --------
 .../ia2/gms/client/call/RemoveMemberCall.java |  34 ---
 .../gms/client/call/RemovePermissionCall.java |  35 ---
 .../gms/client/call/SetPermissionCall.java    |  39 ---
 .../it/inaf/ia2/gms/client/GmsClientTest.java | 255 ------------------
 .../client/call/HttpClientWrapperTest.java    |  15 --
 .../client/call/MockedHttpClientWrapper.java  |  18 --
 gms-client/gms-client/pom.xml                 |  85 ++++++
 .../it/inaf/ia2/gms/client/GmsClient.java     | 159 +++++++++++
 .../call/AddInvitedRegistrationCall.java      |  24 +-
 .../ia2/gms/client/call/AddMemberCall.java    |  30 +++
 .../gms/client/call/AddPermissionCall.java    |  35 +++
 .../ia2/gms/client/call/CreateGroupCall.java  |  25 ++
 .../ia2/gms/client/call/DeleteGroupCall.java  |  23 ++
 .../client/call/GetGroupPermissionsCall.java  |  27 +-
 .../client/call/GetMemberEmailAddresses.java  |  27 +-
 .../gms/client/call/GetUserGroupsCall.java    |  44 ++-
 .../client/call/GetUserPermissionsCall.java   |  27 +-
 .../ia2/gms/client/call/ListGroupsCall.java   |  27 +-
 .../ia2/gms/client/call/RemoveMemberCall.java |  29 ++
 .../gms/client/call/RemovePermissionCall.java |  29 ++
 .../gms/client/call/SetPermissionCall.java    |  33 +++
 .../ia2/gms/client/model/GroupPermission.java |   0
 .../inaf/ia2/gms/client/model/Permission.java |   0
 .../ia2/gms/client/model/UserPermission.java  |   0
 .../ia2/gms/client/BaseGmsClientTest.java     | 101 +++++++
 .../ia2/gms/client/call/AddMemberTest.java    |  33 +++
 .../gms/client/call/AddPermissionTest.java    |  34 +++
 .../ia2/gms/client/call/CreateGroupTest.java  |  33 +++
 .../ia2/gms/client/call/DeleteGroupTest.java  |  33 +++
 .../gms/client/call/GetUserGroupsTest.java    |  61 +++++
 .../client/call/InvitedRegistrationTest.java  |  98 +++++++
 .../ia2/gms/client/call/RemoveMemberTest.java |  33 +++
 .../gms/client/call/RemovePermissionTest.java |  33 +++
 .../java/it/inaf/ia2/gms/model/Identity.java  |  59 ----
 .../it/inaf/ia2/gms/model/IdentityType.java   |  38 ---
 .../java/it/inaf/ia2/gms/model/RapUser.java   |  63 -----
 49 files changed, 999 insertions(+), 1125 deletions(-)
 delete mode 100644 gms-client/gms-client-lib/pom.xml
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
 delete mode 100644 gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java
 delete mode 100644 gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
 delete mode 100644 gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java
 delete mode 100644 gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java
 create mode 100644 gms-client/gms-client/pom.xml
 create mode 100644 gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
 rename gms-client/{gms-client-lib => gms-client}/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java (66%)
 create mode 100644 gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
 create mode 100644 gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
 create mode 100644 gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
 create mode 100644 gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
 rename gms-client/{gms-client-lib => gms-client}/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java (60%)
 rename gms-client/{gms-client-lib => gms-client}/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java (53%)
 rename gms-client/{gms-client-lib => gms-client}/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java (57%)
 rename gms-client/{gms-client-lib => gms-client}/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java (60%)
 rename gms-client/{gms-client-lib => gms-client}/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java (55%)
 create mode 100644 gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
 create mode 100644 gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
 create mode 100644 gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java
 rename gms-client/{gms-client-lib => gms-client}/src/main/java/it/inaf/ia2/gms/client/model/GroupPermission.java (100%)
 rename gms-client/{gms-client-lib => gms-client}/src/main/java/it/inaf/ia2/gms/client/model/Permission.java (100%)
 rename gms-client/{gms-client-lib => gms-client}/src/main/java/it/inaf/ia2/gms/client/model/UserPermission.java (100%)
 create mode 100644 gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/BaseGmsClientTest.java
 create mode 100644 gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddMemberTest.java
 create mode 100644 gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddPermissionTest.java
 create mode 100644 gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/CreateGroupTest.java
 create mode 100644 gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/DeleteGroupTest.java
 create mode 100644 gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/GetUserGroupsTest.java
 create mode 100644 gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/InvitedRegistrationTest.java
 create mode 100644 gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemoveMemberTest.java
 create mode 100644 gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemovePermissionTest.java
 delete mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/Identity.java
 delete mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/IdentityType.java
 delete mode 100644 gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java

diff --git a/.gitignore b/.gitignore
index 410aba1..793bc23 100644
--- a/.gitignore
+++ b/.gitignore
@@ -54,12 +54,10 @@ nbactions.xml
 ### VS Code ###
 .vscode/
 
-/gms-ui/target/
 /gms/nbactions-release-profile.xml
 
-/gms-client/gms-client-lib/target/
-/gms-client/gms-cli/target/
 /gms/node/
+**/target/*
 
 nb-configuration.xml
 dependency-reduced-pom.xml
diff --git a/gms-client/gms-cli/gms.properties b/gms-client/gms-cli/gms.properties
index facdd47..b7f4928 100644
--- a/gms-client/gms-cli/gms.properties
+++ b/gms-client/gms-cli/gms.properties
@@ -1,2 +1,4 @@
-base_url=http://localhost:8082/gms/ws/jwt
-token=eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IjM0ZmU4MDcwMDVhNTcxMTYifQ.eyJpc3MiOiJzc28uaWEyLmluYWYuaXQiLCJzdWIiOiIyMzg2IiwiaWF0IjoxNTg3NjU5NzYxLCJleHAiOjE1ODc3NDYxNjEsImF1ZCI6ImdtcyJ9.KcXRAciG3ApqlE8MFM8VYW9WAX3hEZb7Vk8jB9uJtWsOMU48ha_Ybb4k_f0nrD2jhOxwaNn2QMxWZuflwCf1N-KiCj5Ff9f8xKOrrXZrl-w1H3_dwtMlIS8t2b0-w0WwRJ7UIhrwVBzmCcWinD3qJhFPzyO2pi-A4aXV57RpJ68VXfALQXeHK0sslrf-RgAU3xWYOgjGTUoGB5BQYC9huA_bZ0eV1HFcancs9pDdoTusqZs8OkPFCJbo7-L5eibsuykqnLHztYdCcP2Vtvtwb0pww-ofWZblIHzoMI8i-ipnfLJETG8Dpc7FrhjCYLw3AEGZg4U1wYTeqG3HRbPXSQ
+gms_url=http://localhost:8082/gms/ws/jwt
+client_id=gms_cli
+client_secret=gms
+rap_url=http://localhost/rap-ia2
diff --git a/gms-client/gms-cli/pom.xml b/gms-client/gms-cli/pom.xml
index cfaad41..2bfb5e9 100644
--- a/gms-client/gms-cli/pom.xml
+++ b/gms-client/gms-cli/pom.xml
@@ -17,7 +17,7 @@
     <dependencies>
         <dependency>
             <groupId>${project.groupId}</groupId>
-            <artifactId>gms-client-lib</artifactId>
+            <artifactId>gms-client</artifactId>
             <version>1.0-SNAPSHOT</version>
         </dependency>
     </dependencies>
diff --git a/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java b/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
index 4d4ff78..118e2be 100644
--- a/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
+++ b/gms-client/gms-cli/src/main/java/it/inaf/ia2/gms/cli/CLI.java
@@ -1,8 +1,9 @@
 package it.inaf.ia2.gms.cli;
 
+import it.inaf.ia2.client.ClientException;
 import it.inaf.ia2.gms.client.GmsClient;
-import it.inaf.ia2.gms.client.GmsClientBuilder;
 import it.inaf.ia2.gms.client.model.Permission;
+import it.inaf.ia2.rap.client.RapClient;
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.IOException;
@@ -61,7 +62,11 @@ public class CLI {
                 default:
                     verifyConfigLoaded();
                     createClient();
-                    parseCommand();
+                    try {
+                        parseCommand();
+                    } catch (ClientException ex) {
+                        System.err.println(ex.getMessage());
+                    }
                     commandParsed = true;
                     break;
             }
@@ -96,17 +101,16 @@ public class CLI {
     }
 
     private void createClient() {
-        GmsClientBuilder clientBuilder = new GmsClientBuilder()
-                .setGmsBaseUrl(gmsBaseUrl);
+
+        client = new GmsClient(gmsBaseUrl);
 
         if (token != null) {
-            client = clientBuilder.build();
             client.setAccessToken(token);
-        } else {
-            client = clientBuilder.setClientId(clientId)
-                    .setClientSecret(clientSecret)
-                    .setRapBaseUrl(rapBaseUrl)
-                    .build();
+        } else {            
+            RapClient rapClient = new RapClient(rapBaseUrl)
+                    .setClientId(clientId)
+                    .setClientSecret(clientSecret);            
+            client.setAccessToken(rapClient.getAccessTokenFromClientCredentials());
         }
     }
 
@@ -118,7 +122,7 @@ public class CLI {
         }
 
         Properties properties = new Properties();
-        try (InputStream in = new FileInputStream(config)) {
+        try ( InputStream in = new FileInputStream(config)) {
             properties.load(in);
         } catch (IOException ex) {
             throw new UncheckedIOException(ex);
@@ -141,7 +145,7 @@ public class CLI {
             System.exit(1);
         }
 
-        try (InputStream in = new FileInputStream(tokenFile)) {
+        try ( InputStream in = new FileInputStream(tokenFile)) {
             java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\A");
             token = s.next().trim();
         } catch (IOException ex) {
diff --git a/gms-client/gms-client-lib/pom.xml b/gms-client/gms-client-lib/pom.xml
deleted file mode 100644
index 571ed7f..0000000
--- a/gms-client/gms-client-lib/pom.xml
+++ /dev/null
@@ -1,32 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
-    <modelVersion>4.0.0</modelVersion>
-    <groupId>it.inaf.ia2</groupId>
-    <artifactId>gms-client-lib</artifactId>
-    <version>1.0-SNAPSHOT</version>
-    <packaging>jar</packaging>
-    <properties>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <maven.compiler.source>12</maven.compiler.source>
-        <maven.compiler.target>12</maven.compiler.target>
-    </properties>
-    <dependencies>
-        <dependency>
-            <groupId>org.apache.logging.log4j</groupId>
-            <artifactId>log4j-to-slf4j</artifactId>
-            <version>2.12.1</version>
-        </dependency>
-        <dependency>
-            <groupId>junit</groupId>
-            <artifactId>junit</artifactId>
-            <version>4.12</version>
-            <scope>test</scope>
-        </dependency>
-        <dependency>
-            <groupId>org.mockito</groupId>
-            <artifactId>mockito-core</artifactId>
-            <version>2.23.4</version>
-            <scope>test</scope>
-        </dependency>
-    </dependencies>
-</project>
\ No newline at end of file
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
deleted file mode 100644
index 8dcf041..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
+++ /dev/null
@@ -1,91 +0,0 @@
-package it.inaf.ia2.gms.client;
-
-import it.inaf.ia2.gms.client.call.AddInvitedRegistrationCall;
-import it.inaf.ia2.gms.client.call.HttpClientWrapper;
-import it.inaf.ia2.gms.client.call.AddMemberCall;
-import it.inaf.ia2.gms.client.call.AddPermissionCall;
-import it.inaf.ia2.gms.client.call.CreateGroupCall;
-import it.inaf.ia2.gms.client.call.DeleteGroupCall;
-import it.inaf.ia2.gms.client.call.GetGroupPermissionsCall;
-import it.inaf.ia2.gms.client.call.GetMemberEmailAddresses;
-import it.inaf.ia2.gms.client.call.GetUserGroupsCall;
-import it.inaf.ia2.gms.client.call.GetUserPermissionsCall;
-import it.inaf.ia2.gms.client.call.ListGroupsCall;
-import it.inaf.ia2.gms.client.call.RemoveMemberCall;
-import it.inaf.ia2.gms.client.call.RemovePermissionCall;
-import it.inaf.ia2.gms.client.call.SetPermissionCall;
-import it.inaf.ia2.gms.client.model.GroupPermission;
-import it.inaf.ia2.gms.client.model.Permission;
-import it.inaf.ia2.gms.client.model.UserPermission;
-import java.util.List;
-import java.util.Map;
-
-public class GmsClient {
-
-    private final HttpClientWrapper httpClientWrapper;
-
-    GmsClient(HttpClientWrapper httpClientWrapper) {
-        this.httpClientWrapper = httpClientWrapper;
-    }
-
-    public GmsClient setAccessToken(String accessToken) {
-        httpClientWrapper.setAccessToken(accessToken);
-        return this;
-    }
-
-    public List<String> getMyGroups(String prefix) {
-        return new GetUserGroupsCall(httpClientWrapper).getUserGroups(prefix);
-    }
-
-    public List<String> listGroups(String prefix) {
-        return new ListGroupsCall(httpClientWrapper).listGroups(prefix);
-    }
-
-    public List<String> getUserGroups(String userId, String prefix) {
-        return new GetUserGroupsCall(httpClientWrapper).getUserGroups(userId, prefix);
-    }
-
-    public void createGroup(String completeGroupName, boolean leaf) {
-        new CreateGroupCall(httpClientWrapper).createGroup(completeGroupName, leaf);
-    }
-
-    public void deleteGroup(String completeGroupName) {
-        new DeleteGroupCall(httpClientWrapper).deleteGroup(completeGroupName);
-    }
-
-    public void addMember(String completeGroupName, String userId) {
-        new AddMemberCall(httpClientWrapper).addMember(completeGroupName, userId);
-    }
-
-    public void removeMember(String completeGroupName, String userId) {
-        new RemoveMemberCall(httpClientWrapper).removeMember(completeGroupName, userId);
-    }
-
-    public void addPermission(String completeGroupName, String userId, Permission permission) {
-        new AddPermissionCall(httpClientWrapper).addPermission(completeGroupName, userId, permission);
-    }
-
-    public void setPermission(String completeGroupName, String userId, Permission permission) {
-        new SetPermissionCall(httpClientWrapper).setPermission(completeGroupName, userId, permission);
-    }
-
-    public void removePermission(String completeGroupName, String userId) {
-        new RemovePermissionCall(httpClientWrapper).removePermission(completeGroupName, userId);
-    }
-
-    public List<UserPermission> getUserPermissions(String userId) {
-        return new GetUserPermissionsCall(httpClientWrapper).getUserPermissions(userId);
-    }
-
-    public List<GroupPermission> getGroupPermissions(String groupId) {
-        return new GetGroupPermissionsCall(httpClientWrapper).getGroupPermissions(groupId);
-    }
-
-    public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) {
-        new AddInvitedRegistrationCall(httpClientWrapper).addInvitedRegistration(token, email, groupsPermissions);
-    }
-
-    public List<String> getMemberEmailAddresses(String groupId, Permission permission) {
-        return new GetMemberEmailAddresses(httpClientWrapper).getMemberEmailAddresses(groupId, permission);
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java
deleted file mode 100644
index 002081d..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/GmsClientBuilder.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package it.inaf.ia2.gms.client;
-
-import it.inaf.ia2.gms.client.call.HttpClientWrapper;
-
-public class GmsClientBuilder {
-
-    private String gmsBaseUrl;
-    private String rapBaseUrl;
-    private String clientId;
-    private String clientSecret;
-
-    public GmsClientBuilder setGmsBaseUrl(String gmsBaseUrl) {
-        this.gmsBaseUrl = gmsBaseUrl;
-        return this;
-    }
-
-    public GmsClientBuilder setRapBaseUrl(String rapBaseUrl) {
-        this.rapBaseUrl = rapBaseUrl;
-        return this;
-    }
-
-    public GmsClientBuilder setClientId(String clientId) {
-        this.clientId = clientId;
-        return this;
-    }
-
-    public GmsClientBuilder setClientSecret(String clientSecret) {
-        this.clientSecret = clientSecret;
-        return this;
-    }
-
-    public GmsClient build() {
-        HttpClientWrapper clientWrapper = new HttpClientWrapper(gmsBaseUrl);
-        if (rapBaseUrl != null && clientId != null && clientSecret != null) {
-            clientWrapper.setRapBaseUrl(rapBaseUrl)
-                    .setClientId(clientId).setClientSecret(clientSecret);
-        }
-        return new GmsClient(clientWrapper);
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
deleted file mode 100644
index e76242d..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublishers;
-import java.net.http.HttpResponse;
-
-public class AddMemberCall extends BaseGmsCall {
-
-    public AddMemberCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean addMember(String completeGroupName, String userId) {
-
-        String endpoint = "membership";
-        if (completeGroupName != null && !completeGroupName.isBlank()) {
-            endpoint += "/" + completeGroupName;
-        }
-
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
-                .header("Accept", "text/plain")
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .POST(BodyPublishers.ofString("user_id=" + userId))
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to add member to group");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
deleted file mode 100644
index 42acad9..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import it.inaf.ia2.gms.client.model.Permission;
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublisher;
-import java.net.http.HttpRequest.BodyPublishers;
-import java.net.http.HttpResponse;
-
-public class AddPermissionCall extends BaseGmsCall {
-
-    public AddPermissionCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean addPermission(String completeGroupName, String userId, Permission permission) {
-
-        String endpoint = "permission";
-        if (completeGroupName != null && !completeGroupName.isBlank()) {
-            endpoint += "/" + completeGroupName;
-        }
-
-        BodyPublisher requestBody = BodyPublishers.ofString(
-                "user_id=" + userId + "&permission=" + permission);
-
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
-                .header("Accept", "text/plain")
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .POST(requestBody)
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to add permission");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java
deleted file mode 100644
index f5304b7..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/BaseGmsCall.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.io.InputStream;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.Builder;
-import java.net.http.HttpResponse;
-import java.util.Scanner;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-public abstract class BaseGmsCall {
-
-    private static final Logger LOGGER = LoggerFactory.getLogger(BaseGmsCall.class);
-
-    protected final HttpClientWrapper clientWrapper;
-
-    public BaseGmsCall(HttpClientWrapper clientWrapper) {
-        this.clientWrapper = clientWrapper;
-    }
-
-    protected HttpClient getClient() {
-        return clientWrapper.getClient();
-    }
-
-    protected Builder newHttpRequest(String endpoint) {
-        return clientWrapper.newHttpRequest(endpoint);
-    }
-
-    protected static void logServerError(HttpRequest request, HttpResponse<String> response) {
-        LOGGER.error("Error while reading " + request.uri()
-                + "\nServer response status code is " + response.statusCode()
-                + "\nServer response text is " + response.body());
-    }
-
-    protected static void logServerErrorInputStream(HttpRequest request, HttpResponse<InputStream> response) {
-        Scanner s = new Scanner(response.body()).useDelimiter("\\A");
-        String responseBody = s.hasNext() ? s.next() : "";
-        String error = "Error while reading " + request.uri()
-                + "\nServer response status code is " + response.statusCode()
-                + "\nServer response text is " + responseBody;
-        LOGGER.error(error);
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
deleted file mode 100644
index 1cbb2e1..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.BodyPublishers;
-import java.net.http.HttpResponse;
-
-public class CreateGroupCall extends BaseGmsCall {
-
-    public CreateGroupCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean createGroup(String completeGroupName, boolean leaf) {
-
-        HttpRequest groupsRequest = newHttpRequest(completeGroupName)
-                .header("Accept", "text/plain")
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .POST(BodyPublishers.ofString("leaf=" + leaf))
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 201) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to create group");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
deleted file mode 100644
index 56b7b98..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
+++ /dev/null
@@ -1,28 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class DeleteGroupCall extends BaseGmsCall {
-
-    public DeleteGroupCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean deleteGroup(String completeGroupName) {
-
-        HttpRequest groupsRequest = newHttpRequest(completeGroupName)
-                .header("Accept", "text/plain")
-                .DELETE()
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 204) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to delete group");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java
deleted file mode 100644
index 7d90d72..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/HttpClientWrapper.java
+++ /dev/null
@@ -1,105 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.URI;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpRequest.Builder;
-import java.net.http.HttpResponse;
-import java.util.Base64;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-
-public class HttpClientWrapper {
-
-    private final String baseGmsUri;
-    private final HttpClient client;
-
-    private String rapBaseUrl;
-    private String clientId;
-    private String clientSecret;
-
-    private String accessToken;
-
-    public HttpClientWrapper(String baseGmsUri) {
-        String uri = baseGmsUri;
-        if (!uri.endsWith("/")) {
-            uri += "/";
-        }
-        this.baseGmsUri = uri;
-
-        this.client = HttpClient.newBuilder()
-                .followRedirects(HttpClient.Redirect.ALWAYS)
-                .build();
-    }
-
-    public HttpClientWrapper setAccessToken(String accessToken) {
-        this.accessToken = accessToken;
-        return this;
-    }
-
-    public HttpClientWrapper setRapBaseUrl(String rapBaseUrl) {
-        if (!rapBaseUrl.endsWith("/")) {
-            rapBaseUrl += "/";
-        }
-        this.rapBaseUrl = rapBaseUrl;
-        return this;
-    }
-
-    public HttpClientWrapper setClientId(String clientId) {
-        this.clientId = clientId;
-        return this;
-    }
-
-    public HttpClientWrapper setClientSecret(String clientSecret) {
-        this.clientSecret = clientSecret;
-        return this;
-    }
-
-    Builder newHttpRequest(String endpoint) {
-        if (accessToken == null) {
-            accessToken = getAccessTokenFromClientCredentials();
-        }
-        return HttpRequest.newBuilder()
-                .uri(URI.create(baseGmsUri + endpoint))
-                .header("Authorization", "Bearer " + accessToken);
-    }
-
-    private String getAccessTokenFromClientCredentials() {
-        if (rapBaseUrl == null || clientId == null || clientSecret == null) {
-            throw new IllegalStateException("Access token is null and client credentials are not configured");
-        }
-
-        String basicAuthHeader = clientId + ":" + clientSecret;
-
-        HttpRequest tokenRequest = HttpRequest.newBuilder()
-                .uri(URI.create(rapBaseUrl + "auth/oauth2/token"))
-                .header("Authorization", "Basic " + Base64.getEncoder().encodeToString(basicAuthHeader.getBytes()))
-                .header("Accept", "application/json")
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .POST(HttpRequest.BodyPublishers.ofString("grant_type=client_credentials"))
-                .build();
-
-        return client.sendAsync(tokenRequest, HttpResponse.BodyHandlers.ofString())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return getAccessTokenFromResponse(response.body());
-                    }
-                    BaseGmsCall.logServerError(tokenRequest, response);
-
-                    throw new IllegalStateException("Unable to retrieve access token");
-                }).join();
-    }
-
-    protected String getAccessTokenFromResponse(String body) {
-        Pattern codePattern = Pattern.compile(".*\"access_token\":\\s*\"([^\"]+).*");
-        Matcher matcher = codePattern.matcher(body);
-        if (matcher.find()) {
-            return matcher.group(1);
-        }
-        throw new IllegalStateException("Unable to extract access token from body");
-    }
-
-    HttpClient getClient() {
-        return client;
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
deleted file mode 100644
index cc5b01a..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class RemoveMemberCall extends BaseGmsCall {
-
-    public RemoveMemberCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean removeMember(String completeGroupName, String userId) {
-
-        String endpoint = "membership";
-        if (completeGroupName != null && !completeGroupName.isBlank()) {
-            endpoint += "/" + completeGroupName;
-        }
-        endpoint += "?user_id=" + userId;
-
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
-                .header("Accept", "text/plain")
-                .DELETE()
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 204) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to remove member from group");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
deleted file mode 100644
index 7f17e76..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class RemovePermissionCall extends BaseGmsCall {
-
-    public RemovePermissionCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean removePermission(String completeGroupName, String userId) {
-
-        String endpoint = "permission";
-        if (completeGroupName != null && !completeGroupName.isBlank()) {
-            endpoint += "/" + completeGroupName;
-        }
-        endpoint += "?user_id=" + userId;
-
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
-                .header("Accept", "text/plain")
-                .DELETE()
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 204) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to remove permission");
-                }).join();
-    }
-
-}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java b/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java
deleted file mode 100644
index c7eee10..0000000
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import static it.inaf.ia2.gms.client.call.BaseGmsCall.logServerErrorInputStream;
-import it.inaf.ia2.gms.client.model.Permission;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-
-public class SetPermissionCall extends BaseGmsCall {
-
-    public SetPermissionCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
-    }
-
-    public boolean setPermission(String completeGroupName, String userId, Permission permission) {
-
-        String endpoint = "permission";
-        if (completeGroupName != null && !completeGroupName.isBlank()) {
-            endpoint += "/" + completeGroupName;
-        }
-
-        HttpRequest.BodyPublisher requestBody = HttpRequest.BodyPublishers.ofString(
-                "user_id=" + userId + "&permission=" + permission);
-
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
-                .header("Accept", "text/plain")
-                .header("Content-Type", "application/x-www-form-urlencoded")
-                .PUT(requestBody)
-                .build();
-
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to set permission");
-                }).join();
-    }
-}
diff --git a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
deleted file mode 100644
index ce84caa..0000000
--- a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/GmsClientTest.java
+++ /dev/null
@@ -1,255 +0,0 @@
-package it.inaf.ia2.gms.client;
-
-import it.inaf.ia2.gms.client.call.HttpClientWrapper;
-import it.inaf.ia2.gms.client.call.MockedHttpClientWrapper;
-import it.inaf.ia2.gms.client.model.Permission;
-import java.io.ByteArrayInputStream;
-import java.io.InputStream;
-import java.net.http.HttpClient;
-import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
-import java.net.http.HttpResponse.BodySubscriber;
-import java.net.http.HttpResponse.BodySubscribers;
-import java.nio.ByteBuffer;
-import java.nio.charset.StandardCharsets;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.Flow;
-import static org.junit.Assert.assertEquals;
-import org.junit.Before;
-import org.junit.Test;
-import org.junit.runner.RunWith;
-import org.mockito.AdditionalMatchers;
-import org.mockito.ArgumentMatcher;
-import org.mockito.ArgumentMatchers;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.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;
-
-@RunWith(MockitoJUnitRunner.class)
-public class GmsClientTest {
-
-    private static final String BASE_URL = "http://base-url";
-
-    private HttpClient httpClient;
-    private GmsClient client;
-
-    @Before
-    public void setUp() {
-
-        httpClient = mock(HttpClient.class);
-
-        HttpClientWrapper clientWrapper = new MockedHttpClientWrapper(BASE_URL, httpClient);
-        clientWrapper.setAccessToken("foo");
-
-        client = new GmsClient(clientWrapper);
-    }
-
-    @Test
-    public void testGetMyGroups() {
-
-        String body = "LBT.INAF\n"
-                + "LBT.AZ";
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200, body));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        List<String> groups = client.getMyGroups("LBT.");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("GET", "search"), any());
-
-        assertEquals(2, groups.size());
-        assertEquals("INAF", groups.get(0));
-        assertEquals("AZ", groups.get(1));
-    }
-
-    @Test
-    public void testListGroups() {
-
-        String body = "INAF\n"
-                + "AZ";
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200, body));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        List<String> groups = client.listGroups("LBT.");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("GET", "list/LBT."), any());
-
-        assertEquals(2, groups.size());
-        assertEquals("INAF", groups.get(0));
-        assertEquals("AZ", groups.get(1));
-    }
-
-    @Test
-    public void testCreateGroup() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(201));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.createGroup("LBT.INAF", false);
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "LBT.INAF"), any());
-    }
-
-    @Test
-    public void testDeleteGroup() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.deleteGroup("LBT.INAF");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "LBT.INAF"), any());
-    }
-
-    @Test
-    public void testAddMember() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.addMember("LBT.INAF", "user");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "membership/LBT.INAF"), any());
-    }
-
-    @Test
-    public void testRemoveMember() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.removeMember("LBT.INAF", "user");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "membership/LBT.INAF?user_id=user"), any());
-    }
-
-    @Test
-    public void testAddPermission() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.addPermission("LBT.INAF", "user", Permission.ADMIN);
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "permission/LBT.INAF"), any());
-    }
-
-    @Test
-    public void testRemovePermission() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        client.removePermission("LBT.INAF", "user");
-
-        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "permission/LBT.INAF?user_id=user"), any());
-    }
-
-    @Test
-    public void testInvitedRegistration() {
-
-        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(201));
-
-        when(httpClient.sendAsync(any(), any())).thenReturn(response);
-        Map<String, Permission> permissionsMap = new HashMap<>();
-        permissionsMap.put("group1", Permission.MANAGE_MEMBERS);
-        permissionsMap.put("group2", Permission.MANAGE_MEMBERS);
-        client.addInvitedRegistration("bvjsgqu423", "email", permissionsMap);
-        // hash = AOyojiwaRR7BHPde6Tomg3+BMoQQggNM3wUHEarXuNQ=
-
-        verify(httpClient, times(1)).sendAsync(
-                AdditionalMatchers.and(
-                        endpointEq("POST", "invited-registration"),
-                        ArgumentMatchers.argThat(req -> {
-                            String reqbody = req.bodyPublisher().map(p -> {
-                                var bodySubscriber = BodySubscribers.ofString(StandardCharsets.UTF_8);
-                                var flowSubscriber = new StringSubscriber(bodySubscriber);
-                                p.subscribe(flowSubscriber);
-                                return bodySubscriber.getBody().toCompletableFuture().join();
-                            }).get();
-
-                            // If the resulting hash contains a + symbol it has to be encoded to %2B,
-                            // otherwise it will be interpreted as a space and wrong value will be
-                            // stored into the database
-                            String expectedBody = "token_hash=AOyojiwaRR7BHPde6Tomg3%2BBMoQQggNM3wUHEarXuNQ="
-                                    + "&email=email&groups=group2 MANAGE_MEMBERS\n"
-                                    + "group1 MANAGE_MEMBERS";
-
-                            return reqbody.equals(expectedBody);
-                        })), any());
-    }
-
-    /**
-     * Credit: https://stackoverflow.com/a/55816685/771431
-     */
-    static final class StringSubscriber implements Flow.Subscriber<ByteBuffer> {
-
-        final BodySubscriber<String> wrapped;
-
-        StringSubscriber(BodySubscriber<String> wrapped) {
-            this.wrapped = wrapped;
-        }
-
-        @Override
-        public void onSubscribe(Flow.Subscription subscription) {
-            wrapped.onSubscribe(subscription);
-        }
-
-        @Override
-        public void onNext(ByteBuffer item) {
-            wrapped.onNext(List.of(item));
-        }
-
-        @Override
-        public void onError(Throwable throwable) {
-            wrapped.onError(throwable);
-        }
-
-        @Override
-        public void onComplete() {
-            wrapped.onComplete();
-        }
-    }
-
-    private HttpResponse getMockedResponse(int statusCode, String body) {
-        HttpResponse response = getMockedResponse(statusCode);
-        InputStream in = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
-        when(response.body()).thenReturn(in);
-        return response;
-    }
-
-    private HttpResponse getMockedResponse(int statusCode) {
-        HttpResponse response = mock(HttpResponse.class);
-        when(response.statusCode()).thenReturn(statusCode);
-        return response;
-    }
-
-    private HttpRequest endpointEq(String expectedMethod, String expectedEndpoint) {
-        return ArgumentMatchers.argThat(endpointEqArgumentMatcher(expectedMethod, expectedEndpoint));
-    }
-
-    private ArgumentMatcher<HttpRequest> endpointEqArgumentMatcher(String expectedMethod, String expectedEndpoint) {
-
-        return new ArgumentMatcher<HttpRequest>() {
-
-            private final String expectedUri = BASE_URL + "/" + expectedEndpoint;
-
-            @Override
-            public boolean matches(HttpRequest request) {
-                return expectedMethod.equals(request.method()) && expectedUri.equals(request.uri().toString());
-            }
-
-            @Override
-            public String toString() {
-                return expectedMethod + " " + expectedUri;
-            }
-        };
-    }
-}
diff --git a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java
deleted file mode 100644
index 4bc0924..0000000
--- a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/HttpClientWrapperTest.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import static org.junit.Assert.assertEquals;
-import org.junit.Test;
-
-public class HttpClientWrapperTest {
-
-    @Test
-    public void testExtractAccessToken() {
-        String response = "{\"access_token\":\"TEST_TOKEN\",\"token_type\":\"Bearer\",\"expires_in\":3600}";
-
-        HttpClientWrapper clientWrapper = new HttpClientWrapper("http://localhost");
-        assertEquals("TEST_TOKEN", clientWrapper.getAccessTokenFromResponse(response));
-    }
-}
diff --git a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java b/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java
deleted file mode 100644
index d287f90..0000000
--- a/gms-client/gms-client-lib/src/test/java/it/inaf/ia2/gms/client/call/MockedHttpClientWrapper.java
+++ /dev/null
@@ -1,18 +0,0 @@
-package it.inaf.ia2.gms.client.call;
-
-import java.net.http.HttpClient;
-
-public class MockedHttpClientWrapper extends HttpClientWrapper {
-
-    private final HttpClient mockedClient;
-
-    public MockedHttpClientWrapper(String baseGmsUri, HttpClient mockedClient) {
-        super(baseGmsUri);
-        this.mockedClient = mockedClient;
-    }
-
-    @Override
-    HttpClient getClient() {
-        return mockedClient;
-    }
-}
diff --git a/gms-client/gms-client/pom.xml b/gms-client/gms-client/pom.xml
new file mode 100644
index 0000000..4edf264
--- /dev/null
+++ b/gms-client/gms-client/pom.xml
@@ -0,0 +1,85 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <groupId>it.inaf.ia2</groupId>
+    <artifactId>gms-client</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <maven.compiler.source>14</maven.compiler.source>
+        <maven.compiler.target>14</maven.compiler.target>
+        <junit-jupiter.version>5.6.0</junit-jupiter.version>
+        <mockito.version>3.5.13</mockito.version>
+    </properties>
+    <dependencies>
+        <dependency>
+            <groupId>${project.groupId}</groupId>
+            <artifactId>rap-client</artifactId>
+            <version>1.0-SNAPSHOT</version>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-api</artifactId>
+            <version>${junit-jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-params</artifactId>
+            <version>${junit-jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter-engine</artifactId>
+            <version>${junit-jupiter.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-inline</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-junit-jupiter</artifactId>
+            <version>${mockito.version}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.jacoco</groupId>
+                <artifactId>jacoco-maven-plugin</artifactId>
+                <version>0.8.6</version>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>prepare-agent</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>report</id>
+                        <phase>test</phase>
+                        <goals>
+                            <goal>report</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <version>2.22.2</version>
+            </plugin>
+        </plugins>
+    </build>
+</project>
\ No newline at end of file
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/GmsClient.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
new file mode 100644
index 0000000..cfe275a
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/GmsClient.java
@@ -0,0 +1,159 @@
+package it.inaf.ia2.gms.client;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import it.inaf.ia2.client.BaseClient;
+import it.inaf.ia2.gms.client.call.AddInvitedRegistrationCall;
+import it.inaf.ia2.gms.client.call.AddMemberCall;
+import it.inaf.ia2.gms.client.call.AddPermissionCall;
+import it.inaf.ia2.gms.client.call.CreateGroupCall;
+import it.inaf.ia2.gms.client.call.DeleteGroupCall;
+import it.inaf.ia2.gms.client.call.GetGroupPermissionsCall;
+import it.inaf.ia2.gms.client.call.GetMemberEmailAddresses;
+import it.inaf.ia2.gms.client.call.GetUserGroupsCall;
+import it.inaf.ia2.gms.client.call.GetUserPermissionsCall;
+import it.inaf.ia2.gms.client.call.ListGroupsCall;
+import it.inaf.ia2.gms.client.call.RemoveMemberCall;
+import it.inaf.ia2.gms.client.call.RemovePermissionCall;
+import it.inaf.ia2.gms.client.call.SetPermissionCall;
+import it.inaf.ia2.gms.client.model.GroupPermission;
+import it.inaf.ia2.gms.client.model.Permission;
+import it.inaf.ia2.gms.client.model.UserPermission;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Map;
+
+public class GmsClient extends BaseClient {
+
+    private static final ObjectMapper MAPPER = new ObjectMapper();
+
+    private String accessToken;
+
+    public GmsClient(String gmsBaseUri) {
+        super(gmsBaseUri);
+    }
+
+    public GmsClient setAccessToken(String accessToken) {
+        this.accessToken = accessToken;
+        return this;
+    }
+
+    public List<String> getMyGroups(String prefix) {
+        return new GetUserGroupsCall(this).getUserGroups(prefix);
+    }
+
+    @Override
+    public HttpRequest.Builder newRequest(String endpoint) {
+        if (accessToken == null) {
+            throw new IllegalStateException("Access token is null");
+        }
+        return super.newRequest(getUri(endpoint))
+                .setHeader("Authorization", "Bearer " + accessToken);
+    }
+
+    @Override
+    public HttpRequest.Builder newRequest(URI uri) {
+        if (accessToken == null) {
+            throw new IllegalStateException("Access token is null");
+        }
+        return super.newRequest(uri)
+                .setHeader("Authorization", "Bearer " + accessToken);
+    }
+
+    @Override
+    protected <T> String getInvalidStatusCodeExceptionMessage(HttpRequest request, HttpResponse<T> response) {
+        return response.headers().firstValue("Content-Type")
+                .map(contentType -> {
+                    try {
+                        if (contentType.startsWith("text/plain")) {
+                            String errorResponseBody = null;
+                            if (response.body() instanceof String) {
+                                errorResponseBody = (String) response.body();
+                            } else if (response.body() instanceof InputStream) {
+                                errorResponseBody = new String(((InputStream) response.body()).readAllBytes(), StandardCharsets.UTF_8);
+                            }
+                            if (errorResponseBody != null && !errorResponseBody.isBlank()) {
+                                return errorResponseBody;
+                            }
+                        } else if (contentType.startsWith("application/json")
+                                || contentType.startsWith("text/json")) {
+                            Map<String, Object> errorResponseBody = null;
+                            if (response.body() instanceof String) {
+                                errorResponseBody = MAPPER.readValue((String) response.body(), Map.class);
+                            } else if (response.body() instanceof InputStream) {
+                                errorResponseBody = MAPPER.readValue((InputStream) response.body(), Map.class);
+                            }
+                            if (errorResponseBody != null && errorResponseBody.containsKey("error")) {
+                                return (String) errorResponseBody.get("error");
+                            }
+                        }
+                    } catch (IOException ex) {
+                    }
+                    return null;
+                }).orElse(super.getInvalidStatusCodeExceptionMessage(request, response));
+    }
+
+    public List<String> listGroups(String prefix) {
+        return new ListGroupsCall(this).listGroups(prefix);
+    }
+
+    public List<String> getUserGroups(String userId) {
+        return new GetUserGroupsCall(this).getUserGroups(userId);
+    }
+
+    public List<String> getUserGroups(String userId, String prefix) {
+        return new GetUserGroupsCall(this).getUserGroups(userId, prefix);
+    }
+
+    public void createGroup(String completeGroupName, boolean leaf) {
+        new CreateGroupCall(this).createGroup(completeGroupName, leaf);
+    }
+
+    public void deleteGroup(String completeGroupName) {
+        new DeleteGroupCall(this).deleteGroup(completeGroupName);
+    }
+
+    public void addMember(String completeGroupName, String userId) {
+        new AddMemberCall(this).addMember(completeGroupName, userId);
+    }
+
+    public void removeMember(String completeGroupName, String userId) {
+        new RemoveMemberCall(this).removeMember(completeGroupName, userId);
+    }
+
+    public String addPermission(String completeGroupName, String userId, Permission permission) {
+        return new AddPermissionCall(this).addPermission(completeGroupName, userId, permission);
+    }
+
+    public String setPermission(String completeGroupName, String userId, Permission permission) {
+        return new SetPermissionCall(this).setPermission(completeGroupName, userId, permission);
+    }
+
+    public void removePermission(String completeGroupName, String userId) {
+        new RemovePermissionCall(this).removePermission(completeGroupName, userId);
+    }
+
+    public List<UserPermission> getUserPermissions(String userId) {
+        return new GetUserPermissionsCall(this).getUserPermissions(userId);
+    }
+
+    public List<GroupPermission> getGroupPermissions(String groupId) {
+        return new GetGroupPermissionsCall(this).getGroupPermissions(groupId);
+    }
+
+    public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) {
+        new AddInvitedRegistrationCall(this).addInvitedRegistration(token, email, groupsPermissions);
+    }
+
+    public List<String> getMemberEmailAddresses(String groupId, Permission permission) {
+        return new GetMemberEmailAddresses(this).getMemberEmailAddresses(groupId, permission);
+    }
+
+    public Object call(HttpRequest groupsRequest, HttpResponse.BodyHandler<InputStream> ofInputStream) {
+        throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates.
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
similarity index 66%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
index d0a1ad4..533eea8 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddInvitedRegistrationCall.java
@@ -1,8 +1,11 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import it.inaf.ia2.gms.client.model.Permission;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
@@ -10,10 +13,10 @@ import java.util.Base64;
 import java.util.Map;
 import java.util.stream.Collectors;
 
-public class AddInvitedRegistrationCall extends BaseGmsCall {
+public class AddInvitedRegistrationCall extends BaseCall<GmsClient> {
 
-    public AddInvitedRegistrationCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public AddInvitedRegistrationCall(GmsClient client) {
+        super(client);
     }
 
     public void addInvitedRegistration(String token, String email, Map<String, Permission> groupsPermissions) {
@@ -29,20 +32,13 @@ public class AddInvitedRegistrationCall extends BaseGmsCall {
                         .stream().map(e -> e.getKey() + " " + e.getValue())
                         .collect(Collectors.toList()));
 
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
+        HttpRequest groupsRequest = client.newRequest(endpoint)
                 .header("Accept", "text/plain")
                 .header("Content-Type", "application/x-www-form-urlencoded")
-                .POST(HttpRequest.BodyPublishers.ofString(bodyParams))
+                .POST(BodyPublishers.ofString(bodyParams))
                 .build();
 
-        getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 201) {
-                        return true;
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to create invited registration");
-                }).join();
+        client.call(groupsRequest, BodyHandlers.ofInputStream(), 201, res -> true);
     }
 
     private String getTokenHash(String token) {
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
new file mode 100644
index 0000000..1a414cd
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddMemberCall.java
@@ -0,0 +1,30 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class AddMemberCall extends BaseCall<GmsClient> {
+
+    public AddMemberCall(GmsClient client) {
+        super(client);
+    }
+
+    public boolean addMember(String completeGroupName, String userId) {
+
+        String endpoint = "membership";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+
+        HttpRequest groupsRequest = client.newRequest(endpoint)
+                .header("Accept", "text/plain")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .POST(BodyPublishers.ofString("user_id=" + userId))
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200, res -> true);
+    }
+}
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
new file mode 100644
index 0000000..01562e6
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/AddPermissionCall.java
@@ -0,0 +1,35 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import it.inaf.ia2.gms.client.model.Permission;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublisher;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class AddPermissionCall extends BaseCall<GmsClient> {
+
+    public AddPermissionCall(GmsClient client) {
+        super(client);
+    }
+
+    public String addPermission(String completeGroupName, String userId, Permission permission) {
+
+        String endpoint = "permission";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+
+        BodyPublisher requestBody = BodyPublishers.ofString(
+                "user_id=" + userId + "&permission=" + permission);
+
+        HttpRequest groupsRequest = client.newRequest(endpoint)
+                .header("Accept", "text/plain")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .POST(requestBody)
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofString(), 200, res -> res);
+    }
+}
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
new file mode 100644
index 0000000..1d7a6db
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/CreateGroupCall.java
@@ -0,0 +1,25 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpRequest.BodyPublishers;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class CreateGroupCall extends BaseCall<GmsClient> {
+
+    public CreateGroupCall(GmsClient client) {
+        super(client);
+    }
+
+    public boolean createGroup(String completeGroupName, boolean leaf) {
+
+        HttpRequest groupsRequest = client.newRequest(completeGroupName)
+                .header("Accept", "text/plain")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .POST(BodyPublishers.ofString("leaf=" + leaf))
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 201, res -> true);
+    }
+}
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
new file mode 100644
index 0000000..4b9f0d4
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/DeleteGroupCall.java
@@ -0,0 +1,23 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class DeleteGroupCall extends BaseCall<GmsClient> {
+
+    public DeleteGroupCall(GmsClient client) {
+        super(client);
+    }
+
+    public boolean deleteGroup(String completeGroupName) {
+
+        HttpRequest groupsRequest = client.newRequest(completeGroupName)
+                .header("Accept", "text/plain")
+                .DELETE()
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 204, res -> true);
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java
similarity index 60%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java
index 6c0b996..d4c6838 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetGroupPermissionsCall.java
@@ -1,17 +1,19 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import it.inaf.ia2.gms.client.model.GroupPermission;
 import it.inaf.ia2.gms.client.model.Permission;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Scanner;
 
-public class GetGroupPermissionsCall extends BaseGmsCall {
+public class GetGroupPermissionsCall extends BaseCall<GmsClient> {
 
-    public GetGroupPermissionsCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public GetGroupPermissionsCall(GmsClient client) {
+        super(client);
     }
 
     public List<GroupPermission> getGroupPermissions(String groupId) {
@@ -21,21 +23,14 @@ public class GetGroupPermissionsCall extends BaseGmsCall {
         String endpoint = "permission";
         endpoint += "/" + groupId;
 
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
+        HttpRequest groupsRequest = client.newRequest(endpoint)
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to retrieve groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -48,6 +43,6 @@ public class GetGroupPermissionsCall extends BaseGmsCall {
                         }
                     }
                     return groupPermissions;
-                }).join();
+                });
     }
 }
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
similarity index 53%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
index a69913b..c240bcc 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetMemberEmailAddresses.java
@@ -1,16 +1,18 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import it.inaf.ia2.gms.client.model.Permission;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Scanner;
 
-public class GetMemberEmailAddresses extends BaseGmsCall {
+public class GetMemberEmailAddresses extends BaseCall<GmsClient> {
 
-    public GetMemberEmailAddresses(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public GetMemberEmailAddresses(GmsClient client) {
+        super(client);
     }
 
     public List<String> getMemberEmailAddresses(String group, Permission permission) {
@@ -22,21 +24,14 @@ public class GetMemberEmailAddresses extends BaseGmsCall {
             endpoint += "?permission=" + permission;
         }
 
-        HttpRequest request = newHttpRequest(endpoint)
+        HttpRequest request = client.newRequest(endpoint)
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(request, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(request, response);
-                    throw new IllegalStateException("Unable to retrieve groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(request, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -45,6 +40,6 @@ public class GetMemberEmailAddresses extends BaseGmsCall {
                         }
                     }
                     return emailAddresses;
-                }).join();
+                });
     }
 }
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
similarity index 57%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
index 57ab0ff..a2c4934 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserGroupsCall.java
@@ -1,15 +1,17 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Scanner;
 
-public class GetUserGroupsCall extends BaseGmsCall {
+public class GetUserGroupsCall extends BaseCall<GmsClient> {
 
-    public GetUserGroupsCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public GetUserGroupsCall(GmsClient client) {
+        super(client);
     }
 
     /**
@@ -20,21 +22,14 @@ public class GetUserGroupsCall extends BaseGmsCall {
 
         List<String> groups = new ArrayList<>();
 
-        HttpRequest groupsRequest = newHttpRequest("search")
+        HttpRequest groupsRequest = client.newRequest("search")
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to retrieve groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -50,7 +45,7 @@ public class GetUserGroupsCall extends BaseGmsCall {
                         }
                     }
                     return groups;
-                }).join();
+                });
     }
 
     public List<String> getUserGroups(String userId, String prefix) {
@@ -63,21 +58,14 @@ public class GetUserGroupsCall extends BaseGmsCall {
         }
         endpoint += "?user_id=" + userId;
 
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
+        HttpRequest groupsRequest = client.newRequest(endpoint)
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to retrieve groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -86,6 +74,6 @@ public class GetUserGroupsCall extends BaseGmsCall {
                         }
                     }
                     return groups;
-                }).join();
+                });
     }
 }
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java
similarity index 60%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java
index 34db2f1..67aa39a 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/GetUserPermissionsCall.java
@@ -1,17 +1,19 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import it.inaf.ia2.gms.client.model.Permission;
 import it.inaf.ia2.gms.client.model.UserPermission;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Scanner;
 
-public class GetUserPermissionsCall extends BaseGmsCall {
+public class GetUserPermissionsCall extends BaseCall<GmsClient> {
 
-    public GetUserPermissionsCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public GetUserPermissionsCall(GmsClient client) {
+        super(client);
     }
 
     public List<UserPermission> getUserPermissions(String userId) {
@@ -21,21 +23,14 @@ public class GetUserPermissionsCall extends BaseGmsCall {
         String endpoint = "permission";
         endpoint += "?user_id=" + userId;
 
-        HttpRequest groupsRequest = newHttpRequest(endpoint)
+        HttpRequest groupsRequest = client.newRequest(endpoint)
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to retrieve groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -48,6 +43,6 @@ public class GetUserPermissionsCall extends BaseGmsCall {
                         }
                     }
                     return userPermissions;
-                }).join();
+                });
     }
 }
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
similarity index 55%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
index 7170eba..4b3411c 100644
--- a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/ListGroupsCall.java
@@ -1,15 +1,17 @@
 package it.inaf.ia2.gms.client.call;
 
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
 import java.net.http.HttpRequest;
-import java.net.http.HttpResponse;
+import java.net.http.HttpResponse.BodyHandlers;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Scanner;
 
-public class ListGroupsCall extends BaseGmsCall {
+public class ListGroupsCall extends BaseCall<GmsClient> {
 
-    public ListGroupsCall(HttpClientWrapper clientWrapper) {
-        super(clientWrapper);
+    public ListGroupsCall(GmsClient client) {
+        super(client);
     }
 
     /**
@@ -26,21 +28,14 @@ public class ListGroupsCall extends BaseGmsCall {
             uri += "/" + prefix;
         }
 
-        HttpRequest groupsRequest = newHttpRequest(uri)
+        HttpRequest groupsRequest = client.newRequest(uri)
                 .header("Accept", "text/plain")
                 .GET()
                 .build();
 
-        return getClient().sendAsync(groupsRequest, HttpResponse.BodyHandlers.ofInputStream())
-                .thenApply(response -> {
-                    if (response.statusCode() == 200) {
-                        return response.body();
-                    }
-                    logServerErrorInputStream(groupsRequest, response);
-                    throw new IllegalStateException("Unable to list groups");
-                })
-                .thenApply(inputStream -> {
-                    try (Scanner scan = new Scanner(inputStream)) {
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 200,
+                inputStream -> {
+                    try ( Scanner scan = new Scanner(inputStream)) {
                         while (scan.hasNextLine()) {
                             String line = scan.nextLine();
                             if (!line.isEmpty()) {
@@ -49,6 +44,6 @@ public class ListGroupsCall extends BaseGmsCall {
                         }
                     }
                     return groups;
-                }).join();
+                });
     }
 }
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
new file mode 100644
index 0000000..546ccfe
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemoveMemberCall.java
@@ -0,0 +1,29 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class RemoveMemberCall extends BaseCall<GmsClient> {
+
+    public RemoveMemberCall(GmsClient client) {
+        super(client);
+    }
+
+    public boolean removeMember(String completeGroupName, String userId) {
+
+        String endpoint = "membership";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+        endpoint += "?user_id=" + userId;
+
+        HttpRequest groupsRequest = client.newRequest(endpoint)
+                .header("Accept", "text/plain")
+                .DELETE()
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 204, res -> true);
+    }
+}
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
new file mode 100644
index 0000000..f950614
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/RemovePermissionCall.java
@@ -0,0 +1,29 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class RemovePermissionCall extends BaseCall<GmsClient> {
+
+    public RemovePermissionCall(GmsClient client) {
+        super(client);
+    }
+
+    public boolean removePermission(String completeGroupName, String userId) {
+
+        String endpoint = "permission";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+        endpoint += "?user_id=" + userId;
+
+        HttpRequest groupsRequest = client.newRequest(endpoint)
+                .header("Accept", "text/plain")
+                .DELETE()
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofInputStream(), 204, res -> true);
+    }
+}
diff --git a/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java
new file mode 100644
index 0000000..9f62ebe
--- /dev/null
+++ b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/call/SetPermissionCall.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.client.BaseCall;
+import it.inaf.ia2.gms.client.GmsClient;
+import it.inaf.ia2.gms.client.model.Permission;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse.BodyHandlers;
+
+public class SetPermissionCall extends BaseCall<GmsClient> {
+
+    public SetPermissionCall(GmsClient client) {
+        super(client);
+    }
+
+    public String setPermission(String completeGroupName, String userId, Permission permission) {
+
+        String endpoint = "permission";
+        if (completeGroupName != null && !completeGroupName.isBlank()) {
+            endpoint += "/" + completeGroupName;
+        }
+
+        HttpRequest.BodyPublisher requestBody = HttpRequest.BodyPublishers.ofString(
+                "user_id=" + userId + "&permission=" + permission);
+
+        HttpRequest groupsRequest = client.newRequest(endpoint)
+                .header("Accept", "text/plain")
+                .header("Content-Type", "application/x-www-form-urlencoded")
+                .PUT(requestBody)
+                .build();
+
+        return client.call(groupsRequest, BodyHandlers.ofString(), 200, res -> res);
+    }
+}
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/GroupPermission.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/GroupPermission.java
similarity index 100%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/GroupPermission.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/GroupPermission.java
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Permission.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/Permission.java
similarity index 100%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/Permission.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/Permission.java
diff --git a/gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/UserPermission.java b/gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/UserPermission.java
similarity index 100%
rename from gms-client/gms-client-lib/src/main/java/it/inaf/ia2/gms/client/model/UserPermission.java
rename to gms-client/gms-client/src/main/java/it/inaf/ia2/gms/client/model/UserPermission.java
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/BaseGmsClientTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/BaseGmsClientTest.java
new file mode 100644
index 0000000..69e22fe
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/BaseGmsClientTest.java
@@ -0,0 +1,101 @@
+package it.inaf.ia2.gms.client;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UncheckedIOException;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.nio.charset.StandardCharsets;
+import java.util.concurrent.CompletableFuture;
+import org.mockito.ArgumentMatcher;
+import org.mockito.ArgumentMatchers;
+import static org.mockito.ArgumentMatchers.any;
+import org.mockito.MockedStatic;
+import org.mockito.Mockito;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class BaseGmsClientTest {
+
+    private static final String BASE_URL = "http://base-url";
+
+    protected HttpClient httpClient;
+    protected GmsClient gmsClient;
+
+    public void init() {
+        httpClient = mock(HttpClient.class);
+        gmsClient = getMockedGmsClient(httpClient);
+    }
+
+    protected static String getResourceFileContent(String fileName) {
+        try ( InputStream in = BaseGmsClientTest.class.getClassLoader().getResourceAsStream(fileName)) {
+            return new String(in.readAllBytes(), StandardCharsets.UTF_8);
+        } catch (IOException ex) {
+            throw new UncheckedIOException(ex);
+        }
+    }
+
+    protected static GmsClient getMockedGmsClient(HttpClient mockedHttpClient) {
+
+        HttpClient.Builder builder = mock(HttpClient.Builder.class);
+        when(builder.followRedirects(any())).thenReturn(builder);
+        when(builder.version(any())).thenReturn(builder);
+        when(builder.build()).thenReturn(mockedHttpClient);
+
+        try ( MockedStatic<HttpClient> staticMock = Mockito.mockStatic(HttpClient.class)) {
+            staticMock.when(HttpClient::newBuilder).thenReturn(builder);
+            return new GmsClient(BASE_URL).setAccessToken("foo");
+        }
+    }
+
+    protected static CompletableFuture<HttpResponse<InputStream>> getMockedStreamResponseFuture(int statusCode, String body) {
+        return CompletableFuture.completedFuture(getMockedStreamResponse(200, body));
+    }
+
+    protected static CompletableFuture<HttpResponse<String>> getMockedStringResponseFuture(int statusCode, String body) {
+        return CompletableFuture.completedFuture(getMockedStringResponse(200, body));
+    }
+
+    protected static HttpResponse<InputStream> getMockedStreamResponse(int statusCode, String body) {
+        HttpResponse response = getMockedResponse(statusCode);
+        InputStream in = new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
+        when(response.body()).thenReturn(in);
+        return response;
+    }
+
+    protected static HttpResponse<String> getMockedStringResponse(int statusCode, String body) {
+        HttpResponse response = getMockedResponse(statusCode);
+        when(response.body()).thenReturn(body);
+        return response;
+    }
+
+    protected static HttpResponse getMockedResponse(int statusCode) {
+        HttpResponse response = mock(HttpResponse.class);
+        when(response.statusCode()).thenReturn(statusCode);
+        return response;
+    }
+
+    protected static HttpRequest endpointEq(String expectedMethod, String expectedEndpoint) {
+        return ArgumentMatchers.argThat(endpointEqArgumentMatcher(expectedMethod, expectedEndpoint));
+    }
+
+    protected static ArgumentMatcher<HttpRequest> endpointEqArgumentMatcher(String expectedMethod, String expectedEndpoint) {
+
+        return new ArgumentMatcher<HttpRequest>() {
+
+            private final String expectedUri = BASE_URL + "/" + expectedEndpoint;
+
+            @Override
+            public boolean matches(HttpRequest request) {
+                return expectedMethod.equals(request.method()) && expectedUri.equals(request.uri().toString());
+            }
+
+            @Override
+            public String toString() {
+                return expectedMethod + " " + expectedUri;
+            }
+        };
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddMemberTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddMemberTest.java
new file mode 100644
index 0000000..250eec2
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddMemberTest.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class AddMemberTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testAddMember() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.addMember("LBT.INAF", "user");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "membership/LBT.INAF"), any());
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddPermissionTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddPermissionTest.java
new file mode 100644
index 0000000..04131a8
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/AddPermissionTest.java
@@ -0,0 +1,34 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import it.inaf.ia2.gms.client.model.Permission;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class AddPermissionTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testAddPermission() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(200));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.addPermission("LBT.INAF", "user", Permission.ADMIN);
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "permission/LBT.INAF"), any());
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/CreateGroupTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/CreateGroupTest.java
new file mode 100644
index 0000000..dfd3fd0
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/CreateGroupTest.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class CreateGroupTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testCreateGroup() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(201));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.createGroup("LBT.INAF", false);
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("POST", "LBT.INAF"), any());
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/DeleteGroupTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/DeleteGroupTest.java
new file mode 100644
index 0000000..dd87b4e
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/DeleteGroupTest.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class DeleteGroupTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testDeleteGroup() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.deleteGroup("LBT.INAF");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "LBT.INAF"), any());
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/GetUserGroupsTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/GetUserGroupsTest.java
new file mode 100644
index 0000000..7741eb4
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/GetUserGroupsTest.java
@@ -0,0 +1,61 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class GetUserGroupsTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testGetMyGroups() {
+
+        String body = "LBT.INAF\n"
+                + "LBT.AZ\n"
+                + "";
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedStreamResponse(200, body));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        List<String> groups = gmsClient.getMyGroups("LBT.");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("GET", "search"), any());
+
+        assertEquals(2, groups.size());
+        assertEquals("INAF", groups.get(0));
+        assertEquals("AZ", groups.get(1));
+    }
+
+    @Test
+    public void testListGroups() {
+
+        String body = "INAF\n"
+                + "AZ";
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedStreamResponse(200, body));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        List<String> groups = gmsClient.listGroups("LBT.");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("GET", "list/LBT."), any());
+
+        assertEquals(2, groups.size());
+        assertEquals("INAF", groups.get(0));
+        assertEquals("AZ", groups.get(1));
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/InvitedRegistrationTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/InvitedRegistrationTest.java
new file mode 100644
index 0000000..b234777
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/InvitedRegistrationTest.java
@@ -0,0 +1,98 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import it.inaf.ia2.gms.client.model.Permission;
+import java.net.http.HttpResponse;
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Flow;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.AdditionalMatchers;
+import org.mockito.ArgumentMatchers;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class InvitedRegistrationTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testInvitedRegistration() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(201));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        Map<String, Permission> permissionsMap = new HashMap<>();
+        permissionsMap.put("group1", Permission.MANAGE_MEMBERS);
+        permissionsMap.put("group2", Permission.MANAGE_MEMBERS);
+        gmsClient.addInvitedRegistration("bvjsgqu423", "email", permissionsMap);
+        // hash = AOyojiwaRR7BHPde6Tomg3+BMoQQggNM3wUHEarXuNQ=
+
+        verify(httpClient, times(1)).sendAsync(
+                AdditionalMatchers.and(
+                        endpointEq("POST", "invited-registration"),
+                        ArgumentMatchers.argThat(req -> {
+                            String reqbody = req.bodyPublisher().map(p -> {
+                                var bodySubscriber = HttpResponse.BodySubscribers.ofString(StandardCharsets.UTF_8);
+                                var flowSubscriber = new StringSubscriber(bodySubscriber);
+                                p.subscribe(flowSubscriber);
+                                return bodySubscriber.getBody().toCompletableFuture().join();
+                            }).get();
+
+                            // If the resulting hash contains a + symbol it has to be encoded to %2B,
+                            // otherwise it will be interpreted as a space and wrong value will be
+                            // stored into the database
+                            String expectedBody = "token_hash=AOyojiwaRR7BHPde6Tomg3%2BBMoQQggNM3wUHEarXuNQ="
+                                    + "&email=email&groups=group2 MANAGE_MEMBERS\n"
+                                    + "group1 MANAGE_MEMBERS";
+
+                            return reqbody.equals(expectedBody);
+                        })), any());
+    }
+
+    /**
+     * Credit: https://stackoverflow.com/a/55816685/771431
+     */
+    static final class StringSubscriber implements Flow.Subscriber<ByteBuffer> {
+
+        final HttpResponse.BodySubscriber<String> wrapped;
+
+        StringSubscriber(HttpResponse.BodySubscriber<String> wrapped) {
+            this.wrapped = wrapped;
+        }
+
+        @Override
+        public void onSubscribe(Flow.Subscription subscription) {
+            wrapped.onSubscribe(subscription);
+        }
+
+        @Override
+        public void onNext(ByteBuffer item) {
+            wrapped.onNext(List.of(item));
+        }
+
+        @Override
+        public void onError(Throwable throwable) {
+            wrapped.onError(throwable);
+        }
+
+        @Override
+        public void onComplete() {
+            wrapped.onComplete();
+        }
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemoveMemberTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemoveMemberTest.java
new file mode 100644
index 0000000..298909e
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemoveMemberTest.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class RemoveMemberTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testRemoveMember() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.removeMember("LBT.INAF", "user");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "membership/LBT.INAF?user_id=user"), any());
+    }
+}
diff --git a/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemovePermissionTest.java b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemovePermissionTest.java
new file mode 100644
index 0000000..d48577c
--- /dev/null
+++ b/gms-client/gms-client/src/test/java/it/inaf/ia2/gms/client/call/RemovePermissionTest.java
@@ -0,0 +1,33 @@
+package it.inaf.ia2.gms.client.call;
+
+import it.inaf.ia2.gms.client.BaseGmsClientTest;
+import java.util.concurrent.CompletableFuture;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+@ExtendWith(MockitoExtension.class)
+public class RemovePermissionTest extends BaseGmsClientTest {
+
+    @BeforeEach
+    @Override
+    public void init() {
+        super.init();
+    }
+
+    @Test
+    public void testRemovePermission() {
+
+        CompletableFuture response = CompletableFuture.completedFuture(getMockedResponse(204));
+
+        when(httpClient.sendAsync(any(), any())).thenReturn(response);
+        gmsClient.removePermission("LBT.INAF", "user");
+
+        verify(httpClient, times(1)).sendAsync(endpointEq("DELETE", "permission/LBT.INAF?user_id=user"), any());
+    }
+}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/Identity.java b/gms/src/main/java/it/inaf/ia2/gms/model/Identity.java
deleted file mode 100644
index c879da9..0000000
--- a/gms/src/main/java/it/inaf/ia2/gms/model/Identity.java
+++ /dev/null
@@ -1,59 +0,0 @@
-package it.inaf.ia2.gms.model;
-
-public class Identity {
-
-    private IdentityType type;
-    private String typedId;
-    private String email;
-    private String name;
-    private String surname;
-    private boolean primary;
-
-    public IdentityType getType() {
-        return type;
-    }
-
-    public void setType(IdentityType type) {
-        this.type = type;
-    }
-
-    public String getTypedId() {
-        return typedId;
-    }
-
-    public void setTypedId(String typedId) {
-        this.typedId = typedId;
-    }
-
-    public String getEmail() {
-        return email;
-    }
-
-    public void setEmail(String email) {
-        this.email = email;
-    }
-
-    public String getName() {
-        return name;
-    }
-
-    public void setName(String name) {
-        this.name = name;
-    }
-
-    public String getSurname() {
-        return surname;
-    }
-
-    public void setSurname(String surname) {
-        this.surname = surname;
-    }
-
-    public boolean isPrimary() {
-        return primary;
-    }
-
-    public void setPrimary(boolean primary) {
-        this.primary = primary;
-    }
-}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/IdentityType.java b/gms/src/main/java/it/inaf/ia2/gms/model/IdentityType.java
deleted file mode 100644
index f5c80c8..0000000
--- a/gms/src/main/java/it/inaf/ia2/gms/model/IdentityType.java
+++ /dev/null
@@ -1,38 +0,0 @@
-package it.inaf.ia2.gms.model;
-
-import com.fasterxml.jackson.annotation.JsonCreator;
-import com.fasterxml.jackson.annotation.JsonValue;
-import java.util.Arrays;
-
-public enum IdentityType {
-
-    EDU_GAIN("eduGAIN"),
-    X509("X.509"),
-    ORCID("OrcID"),
-    GOOGLE("Google"),
-    LINKEDIN("LinkedIn"),
-    FACEBOOK("Facebook"),
-    LOCAL_IDP("LocalIdP");
-
-    private final String value;
-
-    IdentityType(String value) {
-        this.value = value;
-    }
-
-    public String getValue() {
-        return value;
-    }
-
-    @JsonCreator
-    public static IdentityType forValue(String value) {
-        return Arrays.stream(IdentityType.values())
-                .filter(it -> value.equals(it.value)).findFirst()
-                .orElseThrow(() -> new IllegalArgumentException("Unsupported IdentityType " + value));
-    }
-
-    @JsonValue
-    public String toValue() {
-        return value;
-    }
-}
diff --git a/gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java b/gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java
deleted file mode 100644
index 846b317..0000000
--- a/gms/src/main/java/it/inaf/ia2/gms/model/RapUser.java
+++ /dev/null
@@ -1,63 +0,0 @@
-package it.inaf.ia2.gms.model;
-
-import java.util.List;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-public class RapUser {
-
-    private String id;
-    private List<Identity> identities;
-
-    public String getId() {
-        return id;
-    }
-
-    public void setId(String id) {
-        this.id = id;
-    }
-
-    public List<Identity> getIdentities() {
-        return identities;
-    }
-
-    public void setIdentities(List<Identity> identities) {
-        this.identities = identities;
-    }
-
-    public String getDisplayName() {
-
-        String displayName = null;
-
-        // trying to extract name and surname
-        for (Identity identity : identities) {
-            if (identity.getName() != null && identity.getSurname() != null) {
-                displayName = String.format("%s %s", identity.getName(), identity.getSurname());
-                if (identity.isPrimary()) { // prefer always primary
-                    break;
-                }
-            }
-        }
-
-        if (displayName == null) { // No name and surname --> using primary email
-            displayName = getPrimaryEmail();
-        }
-
-        // Adding types
-        Set<String> types = identities.stream().map(i -> {
-            if (i.getType() == IdentityType.EDU_GAIN && i.getTypedId().endsWith("@ia2.inaf.it")) {
-                return "IA2";
-            }
-            return i.getType().getValue();
-        }).collect(Collectors.toSet());
-        displayName += String.format(" (%s)", String.join(", ", types));
-
-        return displayName;
-    }
-
-    public String getPrimaryEmail() {
-        Identity primaryIdentity = identities.stream().filter(i -> i.isPrimary()).findFirst()
-                .orElseThrow(() -> new IllegalStateException("No primary identity for user " + id));
-        return primaryIdentity.getEmail();
-    }
-}
-- 
GitLab