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:34 UTC

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

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();
   }