You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by bd...@apache.org on 2022/07/29 17:05:33 UTC

[directory-scimple] branch client-hacking created (now d5fa41e)

This is an automated email from the ASF dual-hosted git repository.

bdemers pushed a change to branch client-hacking
in repository https://gitbox.apache.org/repos/asf/directory-scimple.git


      at d5fa41e  hacking on client ideas

This branch includes the following new commits:

     new d5fa41e  hacking on client ideas

The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.



[directory-scimple] 01/01: hacking on client ideas

Posted by bd...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

bdemers pushed a commit to branch client-hacking
in repository https://gitbox.apache.org/repos/asf/directory-scimple.git

commit d5fa41ef644368770d5d198f69ada9c36d864b6b
Author: Brian Demers <bd...@apache.org>
AuthorDate: Mon Jul 25 13:29:34 2022 -0400

    hacking on client ideas
---
 scim-client/pom.xml                                |  15 +
 .../apache/directory/scim/client/ScimClient.java   |  61 ++++
 .../directory/scim/client/rest/BaseScimClient.java |  90 ++----
 .../scim/client/rest/ResourceTypesClient.java      |  18 +-
 .../directory/scim/client/rest/ScimSelfClient.java |  28 +-
 .../directory/scim/client/rest/ScimUserClient.java |   1 +
 .../directory/scim/client/ScimClientTest.java      | 106 +++++++
 .../scim/client/rest/ScimUserClientTest.java       | 317 +++++++++++++++++++++
 .../scim/server/rest/SelfResourceImpl.java         |   2 +-
 .../scim/server/rest/SelfResourceImplTest.java     |   4 +-
 .../directory/scim/spec/protocol/SelfResource.java |   4 +-
 11 files changed, 554 insertions(+), 92 deletions(-)

diff --git a/scim-client/pom.xml b/scim-client/pom.xml
index 4f16089..6c9e684 100644
--- a/scim-client/pom.xml
+++ b/scim-client/pom.xml
@@ -49,11 +49,26 @@
       <artifactId>junit-jupiter</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
     <dependency>
       <groupId>org.assertj</groupId>
       <artifactId>assertj-core</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>org.jboss.resteasy</groupId>
+      <artifactId>resteasy-client</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-core</artifactId>
+      <scope>test</scope>
+    </dependency>
   </dependencies>
   
 </project>
diff --git a/scim-client/src/main/java/org/apache/directory/scim/client/ScimClient.java b/scim-client/src/main/java/org/apache/directory/scim/client/ScimClient.java
new file mode 100644
index 0000000..a0776f6
--- /dev/null
+++ b/scim-client/src/main/java/org/apache/directory/scim/client/ScimClient.java
@@ -0,0 +1,61 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.directory.scim.client;
+
+import jakarta.ws.rs.client.Client;
+import org.apache.directory.scim.client.rest.*;
+
+/**
+ * The root client used to access a SCIM server.
+ *
+ */
+public class ScimClient {
+
+  private final Client client;
+  private final String baseUrl;
+
+  private final RestCall restCall;
+
+  public ScimClient(Client client, String baseUrl) {
+    this(client, baseUrl, null);
+  }
+
+  public ScimClient(Client client, String baseUrl, RestCall restCall) {
+    this.client = client;
+    this.baseUrl = baseUrl;
+    this.restCall = restCall;
+  }
+
+  public ScimUserClient userClient() {
+    return new ScimUserClient(client, baseUrl, restCall);
+  }
+
+  public ScimGroupClient groupClient() {
+    return new ScimGroupClient(client, baseUrl, restCall);
+  }
+
+  public ScimSelfClient selfClient() {
+    return new ScimSelfClient(client, baseUrl, restCall);
+  }
+
+  public ResourceTypesClient resourceTypesClient() {
+    return new ResourceTypesClient(client, baseUrl, restCall);
+  }
+}
diff --git a/scim-client/src/main/java/org/apache/directory/scim/client/rest/BaseScimClient.java b/scim-client/src/main/java/org/apache/directory/scim/client/rest/BaseScimClient.java
index 523b662..a1c9044 100644
--- a/scim-client/src/main/java/org/apache/directory/scim/client/rest/BaseScimClient.java
+++ b/scim-client/src/main/java/org/apache/directory/scim/client/rest/BaseScimClient.java
@@ -57,9 +57,13 @@ public abstract class BaseScimClient<T extends ScimResource> implements AutoClos
   private final GenericType<ListResponse<T>> scimResourceListResponseGenericType;
   private final WebTarget target;
   private final InternalScimClient scimClient;
-  private RestCall invoke = Invocation::invoke;
+  private final RestCall invoke;
 
   public BaseScimClient(Client client, String baseUrl, Class<T> scimResourceClass, GenericType<ListResponse<T>> scimResourceListGenericType) {
+    this(client, baseUrl, scimResourceClass, scimResourceListGenericType, null);
+  }
+
+  public BaseScimClient(Client client, String baseUrl, Class<T> scimResourceClass, GenericType<ListResponse<T>> scimResourceListGenericType, RestCall invoke) {
     ScimResourceType scimResourceType = scimResourceClass.getAnnotation(ScimResourceType.class);
     String endpoint = scimResourceType != null ? scimResourceType.endpoint() : null;
 
@@ -71,12 +75,7 @@ public abstract class BaseScimClient<T extends ScimResource> implements AutoClos
     this.scimResourceListResponseGenericType = scimResourceListGenericType;
     this.target = this.client.target(baseUrl).path(endpoint);
     this.scimClient = new InternalScimClient();
-  }
-
-  public BaseScimClient(Client client, String baseUrl, Class<T> scimResourceClass, GenericType<ListResponse<T>> scimResourceListGenericType, RestCall invoke) {
-    this(client, baseUrl, scimResourceClass, scimResourceListGenericType);
-
-    this.invoke = invoke;
+    this.invoke = invoke != null ? invoke : Invocation::invoke;
   }
 
   @Override
@@ -199,14 +198,10 @@ public abstract class BaseScimClient<T extends ScimResource> implements AutoClos
     return new ScimException(restException.getError(), restException.getStatus());
   }
 
-  public RestCall getInvoke() {
+  protected RestCall getInvoke() {
     return this.invoke;
   }
 
-  public void setInvoke(RestCall invoke) {
-    this.invoke = invoke;
-  }
-
   private class InternalScimClient implements BaseResourceTypeResource<T> {
 
     private static final String FILTER_QUERY_PARAM = "filter";
@@ -217,7 +212,6 @@ public abstract class BaseScimClient<T extends ScimResource> implements AutoClos
 
     @Override
     public Response getById(String id, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
-      Response response;
       Invocation request = BaseScimClient.this.target
           .path(id)
           .queryParam(ATTRIBUTES_QUERY_PARAM, nullOutQueryParamIfListIsNullOrEmpty(attributes))
@@ -225,18 +219,11 @@ public abstract class BaseScimClient<T extends ScimResource> implements AutoClos
           .request(getContentType())
           .buildGet();
 
-      try {
-        response = BaseScimClient.this.invoke.apply(request);
-
-        return response;
-      } catch (RestException restException) {
-        throw toScimException(restException);
-      }
+      return executeRequest(request);
     }
 
     @Override
     public Response query(AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes, FilterWrapper filter, AttributeReference sortBy, SortOrder sortOrder, Integer startIndex, Integer count) throws ScimException {
-      Response response;
       Invocation request = BaseScimClient.this.target
           .queryParam(ATTRIBUTES_QUERY_PARAM, nullOutQueryParamIfListIsNullOrEmpty(attributes))
           .queryParam(EXCLUDED_ATTRIBUTES_QUERY_PARAM, nullOutQueryParamIfListIsNullOrEmpty(excludedAttributes))
@@ -248,53 +235,32 @@ public abstract class BaseScimClient<T extends ScimResource> implements AutoClos
           .request(getContentType())
           .buildGet();
 
-      try {
-        response = BaseScimClient.this.invoke.apply(request);
-
-        return response;
-      } catch (RestException restException) {
-        throw toScimException(restException);
-      }
+      return executeRequest(request);
     }
 
     @Override
     public Response create(T resource, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
-      Response response;
       Invocation request = BaseScimClient.this.target
           .queryParam(ATTRIBUTES_QUERY_PARAM, nullOutQueryParamIfListIsNullOrEmpty(attributes))
           .queryParam(EXCLUDED_ATTRIBUTES_QUERY_PARAM, nullOutQueryParamIfListIsNullOrEmpty(excludedAttributes))
           .request(getContentType())
           .buildPost(Entity.entity(resource, getContentType()));
 
-      try {
-        response = BaseScimClient.this.invoke.apply(request);
-
-        return response;
-      } catch (RestException restException) {
-        throw toScimException(restException);
-      }
+      return executeRequest(request);
     }
 
     @Override
     public Response find(SearchRequest searchRequest) throws ScimException {
-      Response response;
       Invocation request = BaseScimClient.this.target
           .path(".search")
           .request(getContentType())
           .buildPost(Entity.entity(searchRequest, getContentType()));
 
-      try {
-        response = BaseScimClient.this.invoke.apply(request);
-
-        return response;
-      } catch (RestException restException) {
-        throw toScimException(restException);
-      }
+      return executeRequest(request);
     }
 
     @Override
     public Response update(T resource, String id, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
-      Response response;
       Invocation request = BaseScimClient.this.target
           .path(id)
           .queryParam(ATTRIBUTES_QUERY_PARAM, nullOutQueryParamIfListIsNullOrEmpty(attributes))
@@ -302,18 +268,11 @@ public abstract class BaseScimClient<T extends ScimResource> implements AutoClos
           .request(getContentType())
           .buildPut(Entity.entity(resource, getContentType()));
 
-      try {
-        response = BaseScimClient.this.invoke.apply(request);
-
-        return response;
-      } catch (RestException restException) {
-        throw toScimException(restException);
-      }
+      return executeRequest(request);
     }
 
     @Override
     public Response patch(PatchRequest patchRequest, String id, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
-      Response response;
       Invocation request = BaseScimClient.this.target
           .path(id)
           .queryParam(ATTRIBUTES_QUERY_PARAM, nullOutQueryParamIfListIsNullOrEmpty(attributes))
@@ -321,30 +280,17 @@ public abstract class BaseScimClient<T extends ScimResource> implements AutoClos
           .request(getContentType())
           .build("PATCH", Entity.entity(patchRequest, getContentType()));
 
-      try {
-        response = BaseScimClient.this.invoke.apply(request);
-
-        return response;
-      } catch (RestException restException) {
-        throw toScimException(restException);
-      }
+      return executeRequest(request);
     }
 
     @Override
     public Response delete(String id) throws ScimException {
-      Response response;
       Invocation request = BaseScimClient.this.target
           .path(id)
           .request(getContentType())
           .buildDelete();
 
-      try {
-        response = BaseScimClient.this.invoke.apply(request);
-
-        return response;
-      } catch (RestException restException) {
-        throw toScimException(restException);
-      }
+      return executeRequest(request);
     }
     
     private AttributeReferenceListWrapper nullOutQueryParamIfListIsNullOrEmpty(AttributeReferenceListWrapper wrapper) {
@@ -358,6 +304,14 @@ public abstract class BaseScimClient<T extends ScimResource> implements AutoClos
       
       return wrapper;
     }
+
+    private Response executeRequest(Invocation invocation) throws ScimException {
+      try {
+        return BaseScimClient.this.invoke.apply(invocation);
+      } catch (RestException restClientException) {
+        throw toScimException(restClientException);
+      }
+    }
   }
 
   protected String getContentType() {
diff --git a/scim-client/src/main/java/org/apache/directory/scim/client/rest/ResourceTypesClient.java b/scim-client/src/main/java/org/apache/directory/scim/client/rest/ResourceTypesClient.java
index a1f247f..deb1ab3 100644
--- a/scim-client/src/main/java/org/apache/directory/scim/client/rest/ResourceTypesClient.java
+++ b/scim-client/src/main/java/org/apache/directory/scim/client/rest/ResourceTypesClient.java
@@ -24,6 +24,7 @@ import java.util.Optional;
 
 import jakarta.ws.rs.ProcessingException;
 import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.Invocation;
 import jakarta.ws.rs.client.WebTarget;
 import jakarta.ws.rs.core.GenericType;
 import jakarta.ws.rs.core.Response;
@@ -37,14 +38,19 @@ public class ResourceTypesClient implements AutoCloseable {
 
   private final Client client;
   private final WebTarget target;
-  private final ResourceTypesResourceClient resourceTypesResourceClient = new ResourceTypesResourceClient();
+  private final ResourceTypesResourceClient resourceTypesResourceClient;
 
   public ResourceTypesClient(Client client, String baseUrl) {
+    this(client, baseUrl, null);
+  }
+
+  public ResourceTypesClient(Client client, String baseUrl, RestCall invoke) {
     this.client = client;
     this.target = this.client.target(baseUrl).path("ResourceTypes");
+    this.resourceTypesResourceClient = new ResourceTypesResourceClient(invoke);
   }
 
-  public List<ResourceType> getAllResourceTypes(String filter) throws RestException {
+  public List<ResourceType> query(String filter) throws RestException {
     List<ResourceType> resourceTypes;
     Response response = this.resourceTypesResourceClient.getAllResourceTypes(filter);
 
@@ -58,7 +64,7 @@ public class ResourceTypesClient implements AutoCloseable {
     return resourceTypes;
   }
 
-  public Optional<ResourceType> getResourceType(String name) throws RestException, ProcessingException, IllegalStateException {
+  public Optional<ResourceType> get(String name) throws RestException, ProcessingException, IllegalStateException {
     Optional<ResourceType> resourceType;
     Response response = this.resourceTypesResourceClient.getResourceType(name);
 
@@ -77,6 +83,12 @@ public class ResourceTypesClient implements AutoCloseable {
 
   private class ResourceTypesResourceClient implements ResourceTypesResource {
 
+    private final RestCall invoke;
+
+    private ResourceTypesResourceClient(RestCall invoke) {
+      this.invoke = invoke != null ? null : Invocation::invoke;
+    }
+
     @Override
     public Response getAllResourceTypes(String filter) throws RestException {
       Response response = ResourceTypesClient.this.target
diff --git a/scim-client/src/main/java/org/apache/directory/scim/client/rest/ScimSelfClient.java b/scim-client/src/main/java/org/apache/directory/scim/client/rest/ScimSelfClient.java
index 8580f12..6418f10 100644
--- a/scim-client/src/main/java/org/apache/directory/scim/client/rest/ScimSelfClient.java
+++ b/scim-client/src/main/java/org/apache/directory/scim/client/rest/ScimSelfClient.java
@@ -53,41 +53,41 @@ public class ScimSelfClient implements AutoCloseable {
     this.invoke = invoke;
   }
 
-  public ScimUser getSelf(AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
+  public ScimUser get(AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
     ScimUser self;
-    Response response = this.selfResourceClient.getSelf(attributes, excludedAttributes);
+    Response response = this.selfResourceClient.get(attributes, excludedAttributes);
     self = BaseScimClient.handleResponse(response, ScimUser.class, response::readEntity);
 
     return self;
   }
 
-  public ScimUser getSelf() throws ScimException {
-    ScimUser self = this.getSelf(null, null);
+  public ScimUser get() throws ScimException {
+    ScimUser self = this.get(null, null);
 
     return self;
   }
 
-  public void updateSelf(ScimUser scimUser, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
+  public void update(ScimUser scimUser, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
     Response response = this.selfResourceClient.update(scimUser, attributes, excludedAttributes);
 
     BaseScimClient.handleResponse(response);
   }
 
-  public void updateSelf(ScimUser scimUser) throws ScimException {
-    this.updateSelf(scimUser, null, null);
+  public void update(ScimUser scimUser) throws ScimException {
+    this.update(scimUser, null, null);
   }
 
-  public void patchSelf(PatchRequest patchRequest, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
+  public void patch(PatchRequest patchRequest, AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
     Response response = this.selfResourceClient.patch(patchRequest, attributes, excludedAttributes);
 
     BaseScimClient.handleResponse(response);
   }
 
-  public void patchSelf(PatchRequest patchRequest) throws ScimException {
-    this.patchSelf(patchRequest, null, null);
+  public void patch(PatchRequest patchRequest) throws ScimException {
+    this.patch(patchRequest, null, null);
   }
 
-  public void deleteSelf() throws ScimException {
+  public void delete() throws ScimException {
     Response response = this.selfResourceClient.delete();
 
     BaseScimClient.handleResponse(response);
@@ -97,10 +97,6 @@ public class ScimSelfClient implements AutoCloseable {
     return this.invoke;
   }
 
-  public void setInvoke(RestCall invoke) {
-    this.invoke = invoke;
-  }
-
   @Override
   public void close() throws Exception {
     this.client.close();
@@ -109,7 +105,7 @@ public class ScimSelfClient implements AutoCloseable {
   private class SelfResourceClient implements SelfResource {
 
     @Override
-    public Response getSelf(AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
+    public Response get(AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) throws ScimException {
       Response response;
       Invocation request = ScimSelfClient.this.target
           .queryParam(BaseScimClient.ATTRIBUTES_QUERY_PARAM, attributes)
diff --git a/scim-client/src/main/java/org/apache/directory/scim/client/rest/ScimUserClient.java b/scim-client/src/main/java/org/apache/directory/scim/client/rest/ScimUserClient.java
index d5554e5..8defd5f 100644
--- a/scim-client/src/main/java/org/apache/directory/scim/client/rest/ScimUserClient.java
+++ b/scim-client/src/main/java/org/apache/directory/scim/client/rest/ScimUserClient.java
@@ -20,6 +20,7 @@
 package org.apache.directory.scim.client.rest;
 
 import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.WebTarget;
 import jakarta.ws.rs.core.GenericType;
 
 import org.apache.directory.scim.spec.protocol.data.ListResponse;
diff --git a/scim-client/src/test/java/org/apache/directory/scim/client/ScimClientTest.java b/scim-client/src/test/java/org/apache/directory/scim/client/ScimClientTest.java
new file mode 100644
index 0000000..faefe34
--- /dev/null
+++ b/scim-client/src/test/java/org/apache/directory/scim/client/ScimClientTest.java
@@ -0,0 +1,106 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.directory.scim.client;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.WebTarget;
+import org.apache.directory.scim.client.rest.ResourceTypesClient;
+import org.apache.directory.scim.client.rest.ScimGroupClient;
+import org.apache.directory.scim.client.rest.ScimSelfClient;
+import org.apache.directory.scim.client.rest.ScimUserClient;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+public class ScimClientTest {
+
+  private static final String BASE_URL = "https://scim.example.com/test";
+
+  @Test
+  public void userClient() {
+    Client client = mock(Client.class);
+    ScimClient scimClient = new ScimClient(client, BASE_URL);
+    WebTarget webTarget = mock(WebTarget.class);
+    when(client.target(BASE_URL)).thenReturn(webTarget);
+    WebTarget usersTarget = mock(WebTarget.class);
+    ArgumentCaptor<String> pathCapture = ArgumentCaptor.forClass(String.class);
+    when(webTarget.path(pathCapture.capture())).thenReturn(usersTarget);
+
+    ScimUserClient userClient = scimClient.userClient();
+
+    assertThat(userClient).extracting("client").isSameAs(client);
+    assertThat(userClient).extracting("target").isSameAs(usersTarget);
+    assertThat(pathCapture.getValue()).isEqualTo("/Users");
+  }
+
+  @Test
+  public void groupClient() {
+    Client client = mock(Client.class);
+    ScimClient scimClient = new ScimClient(client, BASE_URL);
+    WebTarget webTarget = mock(WebTarget.class);
+    when(client.target(BASE_URL)).thenReturn(webTarget);
+    WebTarget usersTarget = mock(WebTarget.class);
+    ArgumentCaptor<String> pathCapture = ArgumentCaptor.forClass(String.class);
+    when(webTarget.path(pathCapture.capture())).thenReturn(usersTarget);
+
+    ScimGroupClient groupClient = scimClient.groupClient();
+
+    assertThat(groupClient).extracting("client").isSameAs(client);
+    assertThat(groupClient).extracting("target").isSameAs(usersTarget);
+    assertThat(pathCapture.getValue()).isEqualTo("/Groups");
+  }
+
+  @Test
+  public void typesClient() {
+    Client client = mock(Client.class);
+    ScimClient scimClient = new ScimClient(client, BASE_URL);
+    WebTarget webTarget = mock(WebTarget.class);
+    when(client.target(BASE_URL)).thenReturn(webTarget);
+    WebTarget usersTarget = mock(WebTarget.class);
+    ArgumentCaptor<String> pathCapture = ArgumentCaptor.forClass(String.class);
+    when(webTarget.path(pathCapture.capture())).thenReturn(usersTarget);
+
+    ResourceTypesClient typesClient = scimClient.resourceTypesClient();
+
+    assertThat(typesClient).extracting("client").isSameAs(client);
+    assertThat(typesClient).extracting("target").isSameAs(usersTarget);
+    assertThat(pathCapture.getValue()).isEqualTo("ResourceTypes");
+  }
+
+  @Test
+  public void selfClient() {
+    Client client = mock(Client.class);
+    ScimClient scimClient = new ScimClient(client, BASE_URL);
+    WebTarget webTarget = mock(WebTarget.class);
+    when(client.target(BASE_URL)).thenReturn(webTarget);
+    WebTarget usersTarget = mock(WebTarget.class);
+    ArgumentCaptor<String> pathCapture = ArgumentCaptor.forClass(String.class);
+    when(webTarget.path(pathCapture.capture())).thenReturn(usersTarget);
+
+    ScimSelfClient selfClient = scimClient.selfClient();
+
+    assertThat(selfClient).extracting("client").isSameAs(client);
+    assertThat(selfClient).extracting("target").isSameAs(usersTarget);
+    assertThat(pathCapture.getValue()).isEqualTo("Me");
+  }
+}
diff --git a/scim-client/src/test/java/org/apache/directory/scim/client/rest/ScimUserClientTest.java b/scim-client/src/test/java/org/apache/directory/scim/client/rest/ScimUserClientTest.java
new file mode 100644
index 0000000..30df2fc
--- /dev/null
+++ b/scim-client/src/test/java/org/apache/directory/scim/client/rest/ScimUserClientTest.java
@@ -0,0 +1,317 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+
+ * http://www.apache.org/licenses/LICENSE-2.0
+
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.directory.scim.client.rest;
+
+import jakarta.ws.rs.client.Client;
+import jakarta.ws.rs.client.Entity;
+import jakarta.ws.rs.client.Invocation;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.GenericType;
+import jakarta.ws.rs.core.Response;
+import org.apache.directory.scim.spec.protocol.Constants;
+import org.apache.directory.scim.spec.protocol.attribute.AttributeReference;
+import org.apache.directory.scim.spec.protocol.attribute.AttributeReferenceListWrapper;
+import org.apache.directory.scim.spec.protocol.data.ErrorResponse;
+import org.apache.directory.scim.spec.protocol.data.ListResponse;
+import org.apache.directory.scim.spec.protocol.data.PatchRequest;
+import org.apache.directory.scim.spec.protocol.data.SearchRequest;
+import org.apache.directory.scim.spec.protocol.exception.ScimException;
+import org.apache.directory.scim.spec.protocol.search.Filter;
+import org.apache.directory.scim.spec.protocol.search.SortOrder;
+import org.apache.directory.scim.spec.resources.ScimUser;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+import java.util.*;
+import java.util.stream.IntStream;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.*;
+
+public class ScimUserClientTest {
+
+  private static final String BASE_URL = "https://scim.example.com/test";
+
+  @Test
+  public void findTest() throws Exception {
+
+    ListResponse<ScimUser> mockListResponse = mock(ListResponse.class);
+    MockClient mockClient = new MockClient(BASE_URL, "POST", ok(mockListResponse));
+    SearchRequest searchRequest = new SearchRequest();
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    ListResponse<ScimUser> listResponse = userClient.find(searchRequest);
+
+    assertThat(listResponse).isSameAs(mockListResponse);
+    assertThat(mockClient.requestPath()).isEqualTo("/Users/.search");
+    assertThat(mockClient.requestEntity()).isSameAs(searchRequest);
+  }
+
+  @Test
+  public void queryTest() throws Exception {
+
+    ListResponse<ScimUser> mockListResponse = mock(ListResponse.class);
+    MockClient mockClient = new MockClient(BASE_URL, "GET", ok(mockListResponse));
+    AttributeReferenceListWrapper attributes = mock(AttributeReferenceListWrapper.class);
+    AttributeReferenceListWrapper excludedAttributes = mock(AttributeReferenceListWrapper.class);
+    Filter filter = mock(Filter.class);
+    AttributeReference sortBy = mock(AttributeReference.class);
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    ListResponse<ScimUser> listResponse = userClient.query(attributes, excludedAttributes, filter, sortBy, SortOrder.ASCENDING, 5, 42);
+
+    Map<String, Object> expectedQueryParams = new HashMap<>();
+    expectedQueryParams.put("attributes", null);
+    expectedQueryParams.put("count", 42);
+    expectedQueryParams.put("excludedAttributes", null);
+    expectedQueryParams.put("filter", filter);
+    expectedQueryParams.put("sortBy", sortBy);
+    expectedQueryParams.put("sortOrder", "ASCENDING");
+    expectedQueryParams.put("startIndex", 5);
+
+    assertThat(listResponse).isSameAs(mockListResponse);
+    assertThat(mockClient.requestPath()).isEqualTo("/Users");
+    assertThat(mockClient.queryParams()).isEqualTo(expectedQueryParams);
+  }
+
+  @Test
+  public void getById_found() throws Exception {
+    ScimUser entity = mock(ScimUser.class);
+    MockClient mockClient = new MockClient(BASE_URL, "GET", ok(entity));
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    Optional<ScimUser> optionalResponse = userClient.getById("test-id");
+
+    assertThat(optionalResponse).isNotNull();
+    assertThat(optionalResponse.get()).isSameAs(entity);
+    assertThat(mockClient.requestPath()).isEqualTo("/Users/test-id");
+  }
+
+  @Test
+  public void getById_notFound() throws Exception {
+    MockClient mockClient = new MockClient(BASE_URL, "GET", notFound());
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    Optional<ScimUser> optionalResponse = userClient.getById("test-id-not-found");
+
+    assertThat(optionalResponse).isEmpty();
+    assertThat(mockClient.requestPath()).isEqualTo("/Users/test-id-not-found");
+  }
+
+  @Test
+  public void getById_serverError() throws Exception {
+    MockClient mockClient = new MockClient(BASE_URL, "GET", error(Response.Status.INTERNAL_SERVER_ERROR));
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    ScimException exception = expect(ScimException.class, () -> userClient.getById("test-id-error"));
+
+    assertThat(exception.getStatus()).isEqualTo(Response.Status.INTERNAL_SERVER_ERROR);
+    assertThat(mockClient.requestPath()).isEqualTo("/Users/test-id-error");
+  }
+
+  @Test
+  public void create_success() throws Exception {
+    ScimUser entity = mock(ScimUser.class);
+    ScimUser user = mock(ScimUser.class);
+    MockClient mockClient = new MockClient(BASE_URL, "POST", ok(entity));
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    ScimUser response = userClient.create(user);
+
+    assertThat(response).isSameAs(entity);
+    assertThat(mockClient.requestEntity()).isSameAs(user);
+    assertThat(mockClient.requestPath()).isEqualTo("/Users");
+  }
+
+  @Test
+  public void create_fail() throws Exception {
+    ScimUser entity = mock(ScimUser.class);
+    MockClient mockClient = new MockClient(BASE_URL, "POST", error(Response.Status.INTERNAL_SERVER_ERROR));
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    ScimException exception = expect(ScimException.class, () -> userClient.create(entity));
+
+    assertThat(exception.getStatus()).isEqualTo(Response.Status.INTERNAL_SERVER_ERROR);
+    assertThat(mockClient.requestEntity()).isSameAs(entity);
+    assertThat(mockClient.requestPath()).isEqualTo("/Users");
+  }
+
+  @Test
+  public void create_withRestCallFail() throws Exception {
+    ScimUser entity = mock(ScimUser.class);
+    MockClient mockClient = new MockClient(BASE_URL, "POST", error(Response.Status.INTERNAL_SERVER_ERROR));
+    ErrorResponse error = new ErrorResponse(Response.Status.CONFLICT, "expected test exception in create_withRestCall");
+    RestCall restCall = (invocation) -> { throw new RestException(409, error); };
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL, restCall);
+    ScimException exception = expect(ScimException.class, () -> userClient.create(entity));
+
+    assertThat(exception.getStatus()).isEqualTo(Response.Status.CONFLICT);
+    assertThat(mockClient.requestEntity()).isSameAs(entity);
+    assertThat(mockClient.requestPath()).isEqualTo("/Users");
+  }
+
+  @Test
+  public void update_success() throws Exception {
+    ScimUser entity = mock(ScimUser.class);
+    ScimUser user = mock(ScimUser.class);
+    MockClient mockClient = new MockClient(BASE_URL, "PUT", ok(entity));
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    ScimUser response = userClient.update("update-test", user);
+
+    assertThat(response).isSameAs(entity);
+    assertThat(mockClient.requestEntity()).isSameAs(user);
+    assertThat(mockClient.requestPath()).isEqualTo("/Users/update-test");
+  }
+
+  @Test
+  public void patch_success() throws Exception {
+    PatchRequest patch = mock(PatchRequest.class);
+    ScimUser entity = mock(ScimUser.class);
+    MockClient mockClient = new MockClient(BASE_URL, "PATCH", ok(entity));
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    ScimUser response = userClient.patch("update-test", patch);
+
+    assertThat(response).isSameAs(entity);
+    assertThat(mockClient.requestEntity()).isSameAs(patch);
+    assertThat(mockClient.requestPath()).isEqualTo("/Users/update-test");
+  }
+
+  @Test
+  public void delete_success() throws Exception {
+    ScimUser entity = mock(ScimUser.class);
+    MockClient mockClient = new MockClient(BASE_URL, "DELETE", ok(entity));
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    userClient.delete("test-delete");
+
+    assertThat(mockClient.requestPath()).isEqualTo("/Users/test-delete");
+  }
+
+  @Test
+  public void delete_error() throws Exception {
+    MockClient mockClient = new MockClient(BASE_URL, "DELETE", notFound());
+
+    ScimUserClient userClient = new ScimUserClient(mockClient.client, BASE_URL);
+    ScimException exception = expect(ScimException.class, () -> userClient.delete("test-delete-404"));
+
+    assertThat(exception.getStatus()).isEqualTo(Response.Status.NOT_FOUND);
+    assertThat(mockClient.requestPath()).isEqualTo("/Users/test-delete-404");
+  }
+
+  static Response ok(Object entity) {
+    Response response = mock(Response.class);
+    when(response.readEntity(any(GenericType.class))).thenReturn(entity); // list response
+    when(response.readEntity(any(Class.class))).thenReturn(entity); // single entity
+    when(response.getStatusInfo()).thenReturn(Response.Status.OK);
+    when(response.getStatus()).thenReturn(Response.Status.OK.getStatusCode());
+    return response;
+  }
+
+  static Response notFound() {
+    return error(Response.Status.NOT_FOUND);
+  }
+
+  static Response error(Response.Status status) {
+    Response response = mock(Response.class);
+    when(response.getStatusInfo()).thenReturn(status);
+    when(response.getStatus()).thenReturn(status.getStatusCode());
+    return response;
+  }
+
+  @FunctionalInterface
+  interface ThrowingRunnable {
+    void run() throws Exception;
+  }
+
+  <T extends Exception> T expect(Class<T> exceptionType, ThrowingRunnable runnable) {
+    try {
+      runnable.run();
+    } catch (Exception e) {
+      if (e.getClass().equals(exceptionType)) {
+        return (T) e;
+      }
+      throw new RuntimeException("Expected block to throw exception of type: " + exceptionType + " but was: " + e.getClass(), e);
+    }
+    throw new RuntimeException("Expected block to throw exception of type " + exceptionType);
+  }
+
+  static class MockClient {
+
+    Client client = mock(Client.class);
+    WebTarget webTarget = mock(WebTarget.class);
+    Invocation.Builder builder = mock(Invocation.Builder.class);
+    ArgumentCaptor<Entity> entityCaptor = ArgumentCaptor.forClass(Entity.class);
+    Invocation invocation = mock(Invocation.class);
+    ArgumentCaptor<String> pathCapture = ArgumentCaptor.forClass(String.class);
+    ArgumentCaptor<String> queryKeysCapture = ArgumentCaptor.forClass(String.class);
+    ArgumentCaptor<Object> queryValuesCapture = ArgumentCaptor.forClass(Object.class);
+
+    MockClient(String baseUrl, String method, Response response) {
+      when(client.target(baseUrl)).thenReturn(webTarget);
+      when(webTarget.path(pathCapture.capture())).thenReturn(webTarget);
+      when(webTarget.request(Constants.SCIM_CONTENT_TYPE)).thenReturn(builder);
+      when(webTarget.queryParam(queryKeysCapture.capture(), queryValuesCapture.capture())).thenReturn(webTarget);
+      when(invocation.invoke()).thenReturn(response);
+
+      switch(method){
+        case "POST":
+          when(builder.buildPost(entityCaptor.capture())).thenReturn(invocation);
+          break;
+        case "PUT":
+          when(builder.buildPut(entityCaptor.capture())).thenReturn(invocation);
+          break;
+        case "PATCH":
+          when(builder.build(eq("PATCH"), entityCaptor.capture())).thenReturn(invocation);
+          break;
+        case "GET":
+          when(builder.buildGet()).thenReturn(invocation);
+          break;
+        case "DELETE":
+          when(builder.buildDelete()).thenReturn(invocation);
+          break;
+        default:
+          throw new IllegalStateException("Unsupported method type '" + method + "', an update to the `MockClient` is needed");
+      }
+    }
+
+    String requestPath() {
+      return String.join("/", pathCapture.getAllValues());
+    }
+
+    Map<String, Object> queryParams() {
+      List<String> keys = queryKeysCapture.getAllValues();
+      List<Object> values = queryValuesCapture.getAllValues();
+
+      Map<String, Object> params = new LinkedHashMap<>(keys.size());
+      IntStream
+        .range(0, keys.size())
+        .forEach(i -> params.put(keys.get(i), values.get(i)));
+
+      return params;
+    }
+
+    Object requestEntity() {
+      return entityCaptor.getValue().getEntity();
+    }
+  }
+}
diff --git a/scim-server/src/main/java/org/apache/directory/scim/server/rest/SelfResourceImpl.java b/scim-server/src/main/java/org/apache/directory/scim/server/rest/SelfResourceImpl.java
index 32c789e..51cfeb2 100644
--- a/scim-server/src/main/java/org/apache/directory/scim/server/rest/SelfResourceImpl.java
+++ b/scim-server/src/main/java/org/apache/directory/scim/server/rest/SelfResourceImpl.java
@@ -54,7 +54,7 @@ public class SelfResourceImpl implements SelfResource {
   SecurityContext securityContext;
 
   @Override
-  public Response getSelf(AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) {
+  public Response get(AttributeReferenceListWrapper attributes, AttributeReferenceListWrapper excludedAttributes) {
     try {
       String internalId = getInternalId();
       return userResource.getById(internalId, attributes, excludedAttributes);
diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/rest/SelfResourceImplTest.java b/scim-server/src/test/java/org/apache/directory/scim/server/rest/SelfResourceImplTest.java
index b5e8143..5b22241 100644
--- a/scim-server/src/test/java/org/apache/directory/scim/server/rest/SelfResourceImplTest.java
+++ b/scim-server/src/test/java/org/apache/directory/scim/server/rest/SelfResourceImplTest.java
@@ -58,7 +58,7 @@ public class SelfResourceImplTest {
     selfResource.selfIdResolver = selfIdResolverInstance;
     selfResource.securityContext = securityContext;
 
-    Response response = selfResource.getSelf(null, null);
+    Response response = selfResource.get(null, null);
     assertThat(response.getEntity(), instanceOf(ErrorResponse.class));
     List<String> messages = ((ErrorResponse)response.getEntity()).getErrorMessageList();
     assertThat(messages, hasItem("Caller SelfIdResolver not available"));
@@ -90,6 +90,6 @@ public class SelfResourceImplTest {
     selfResource.userResource = userResource;
 
     // the response is just a passed along from the UserResource, so just validate it is the same instance.
-    assertThat(selfResource.getSelf(null, null), sameInstance(mockResponse));
+    assertThat(selfResource.get(null, null), sameInstance(mockResponse));
   }
 }
diff --git a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/SelfResource.java b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/SelfResource.java
index d040b41..1a40d48 100644
--- a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/SelfResource.java
+++ b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/SelfResource.java
@@ -90,8 +90,8 @@ public interface SelfResource {
     @ApiResponse(responseCode="500", description="Internal Server Error"),
     @ApiResponse(responseCode="501", description="Not Implemented")
   })
-    default Response getSelf(@Parameter(name="attributes") @QueryParam("attributes") AttributeReferenceListWrapper attributes,
-                             @Parameter(name="excludedAttributes") @QueryParam("excludedAttributes") AttributeReferenceListWrapper excludedAttributes) throws Exception {
+    default Response get(@Parameter(name="attributes") @QueryParam("attributes") AttributeReferenceListWrapper attributes,
+                         @Parameter(name="excludedAttributes") @QueryParam("excludedAttributes") AttributeReferenceListWrapper excludedAttributes) throws Exception {
     return Response.status(Status.NOT_IMPLEMENTED).build();
   }