Merge pull request #130 from apache/it-module

Add Compliance/IT testing module
diff --git a/pom.xml b/pom.xml
index 43e9223..e12c26c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -44,6 +44,10 @@
     <version.slf4j>1.7.36</version.slf4j>
     <version.antlr4>4.5.3</version.antlr4>
     <version.resteasy>6.1.0.Final</version.resteasy>
+
+    <!-- default surefire/failsafe arg lines to empty-->
+    <jacoco.argline></jacoco.argline>
+    <jacoco.it.argline></jacoco.it.argline>
   </properties>
 
   <modules>
@@ -53,6 +57,7 @@
     <module>scim-server-examples/scim-server-jersey</module>
     <module>scim-spec</module>
     <module>scim-tools</module>
+    <module>scim-compliance-tests</module>
     <module>scim-coverage</module>
   </modules>
 
@@ -133,6 +138,11 @@
         <artifactId>scim-client</artifactId>
         <version>2.23-SNAPSHOT</version>
       </dependency>
+      <dependency>
+        <groupId>org.apache.directory.scim</groupId>
+        <artifactId>scim-compliance-tests</artifactId>
+        <version>2.23-SNAPSHOT</version>
+      </dependency>
 
       <dependency>
         <groupId>org.jboss.weld.se</groupId>
@@ -172,6 +182,20 @@
         <type>pom</type>
       </dependency>
       <dependency>
+        <groupId>io.rest-assured</groupId>
+        <artifactId>rest-assured-bom</artifactId>
+        <type>pom</type>
+        <version>5.1.1</version>
+        <scope>import</scope>
+      </dependency>
+      <dependency>
+        <groupId>org.glassfish.jersey</groupId>
+        <artifactId>jersey-bom</artifactId>
+        <version>3.0.6</version>
+        <type>pom</type>
+        <scope>import</scope>
+      </dependency>
+      <dependency>
         <groupId>org.hibernate.validator</groupId>
         <artifactId>hibernate-validator</artifactId>
         <version>7.0.5.Final</version>
@@ -217,6 +241,11 @@
       </dependency>
       <dependency>
         <groupId>org.slf4j</groupId>
+        <artifactId>jcl-over-slf4j</artifactId>
+        <version>${version.slf4j}</version>
+      </dependency>
+      <dependency>
+        <groupId>org.slf4j</groupId>
         <artifactId>slf4j-simple</artifactId>
         <version>${version.slf4j}</version>
         <scope>test</scope>
@@ -243,7 +272,6 @@
         <groupId>org.hamcrest</groupId>
         <artifactId>hamcrest</artifactId>
         <version>2.2</version>
-        <scope>test</scope>
       </dependency>
       <dependency>
         <groupId>eu.codearte.catch-exception</groupId>
@@ -295,15 +323,38 @@
       <plugin>
         <groupId>org.jacoco</groupId>
         <artifactId>jacoco-maven-plugin</artifactId>
-        <configuration>
-          <propertyName>jacoco.argline</propertyName>
-        </configuration>
         <executions>
           <execution>
             <id>prepare-agent</id>
             <goals>
               <goal>prepare-agent</goal>
             </goals>
+            <configuration>
+              <propertyName>jacoco.argline</propertyName>
+            </configuration>
+          </execution>
+          <execution>
+            <id>prepare-agent-integration</id>
+            <phase>pre-integration-test</phase>
+            <goals>
+              <goal>prepare-agent-integration</goal>
+            </goals>
+            <configuration>
+              <propertyName>jacoco.it.argline</propertyName>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-failsafe-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>run-its</id>
+            <goals>
+              <goal>integration-test</goal>
+              <goal>verify</goal>
+            </goals>
           </execution>
         </executions>
       </plugin>
@@ -439,6 +490,26 @@
             <configuration>
               <!-- parent pom overrides the default, so we MUST set the jacoco argline directly -->
               <argLine>@{jacoco.argline}</argLine>
+              <includes>
+                <include>**/Test*.java</include>
+                <include>**/*Test.java</include>
+                <include>**/*Tests.java</include>
+                <include>**/*TestCase.java</include>
+              </includes>
+            </configuration>
+          </plugin>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <version>3.0.0-M7</version>
+            <configuration>
+              <argLine>@{jacoco.it.argline}</argLine>
+              <includes>
+                <include>**/*IT.java</include>
+              </includes>
+              <dependenciesToScan>
+                <dependency>org.apache.directory.scim:scim-compliance-tests</dependency>
+              </dependenciesToScan>
             </configuration>
           </plugin>
           <plugin>
diff --git a/scim-compliance-tests/pom.xml b/scim-compliance-tests/pom.xml
new file mode 100644
index 0000000..bd4459c
--- /dev/null
+++ b/scim-compliance-tests/pom.xml
@@ -0,0 +1,55 @@
+<!--  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. -->
+
+<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>
+
+  <parent>
+    <groupId>org.apache.directory.scim</groupId>
+    <artifactId>scim-parent</artifactId>
+    <version>2.23-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>scim-compliance-tests</artifactId>
+  <name>SCIM - Compliance Tests</name>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>io.rest-assured</groupId>
+      <artifactId>rest-assured</artifactId>
+    </dependency>
+    <dependency>
+      <!-- Required for any libraries that expect to call the commons logging APIs -->
+      <groupId>org.slf4j</groupId>
+      <artifactId>jcl-over-slf4j</artifactId>
+      <scope>runtime</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.mockito</groupId>
+      <artifactId>mockito-junit-jupiter</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.hamcrest</groupId>
+      <artifactId>hamcrest</artifactId>
+    </dependency>
+  </dependencies>
+
+</project>
diff --git a/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/junit/EmbeddedServerExtension.java b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/junit/EmbeddedServerExtension.java
new file mode 100644
index 0000000..2cbb0c2
--- /dev/null
+++ b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/junit/EmbeddedServerExtension.java
@@ -0,0 +1,92 @@
+/*
+ * 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.compliance.junit;
+
+import org.apache.commons.lang3.reflect.FieldUtils;
+import org.junit.jupiter.api.extension.AfterAllCallback;
+import org.junit.jupiter.api.extension.BeforeAllCallback;
+import org.junit.jupiter.api.extension.BeforeEachCallback;
+import org.junit.jupiter.api.extension.ExtensionContext;
+
+import java.io.IOException;
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.reflect.Field;
+import java.net.ServerSocket;
+import java.net.URI;
+import java.util.Arrays;
+import java.util.List;
+import java.util.ServiceLoader;
+
+public class EmbeddedServerExtension implements BeforeAllCallback, BeforeEachCallback, AfterAllCallback {
+
+  private ScimTestServer server;
+  private URI uri;
+
+  @Override
+  public void beforeAll(ExtensionContext context) throws Exception {
+
+    ServiceLoader<ScimTestServer> serviceLoader = ServiceLoader.load(ScimTestServer.class);
+    server = serviceLoader.findFirst().orElseThrow(() -> new RuntimeException("Failed to find implementation of ScimTestServer via ServiceLoader"));
+    uri = server.start(randomPort());
+  }
+
+  @Override
+  public void beforeEach(ExtensionContext context) throws Exception {
+    final List<Object> testInstances = context.getRequiredTestInstances().getAllInstances();
+    testInstances.forEach(test -> {
+        Field[] fields = FieldUtils.getFieldsWithAnnotation(test.getClass(), ScimServerUri.class);
+        Arrays.stream(fields).forEach(field -> {
+          try {
+            field.setAccessible(true);
+            FieldUtils.writeField(field, test, uri);
+          } catch (IllegalAccessException e) {
+            throw new RuntimeException(e);
+          }
+        });
+      }
+    );
+  }
+
+  @Override
+  public void afterAll(ExtensionContext context) throws Exception {
+    server.shutdown();
+  }
+
+  private static int randomPort() {
+    try (ServerSocket socket = new ServerSocket(0)) {
+      return socket.getLocalPort();
+    } catch (IOException e) {
+      throw new RuntimeException("Failed to find a free server port", e);
+    }
+  }
+
+  @Target(ElementType.FIELD)
+  @Retention(RetentionPolicy.RUNTIME)
+  public @interface ScimServerUri {
+  }
+
+  public interface ScimTestServer {
+    URI start(int port);
+    void shutdown();
+  }
+}
diff --git a/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/GroupsIT.java b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/GroupsIT.java
new file mode 100644
index 0000000..1ddf502
--- /dev/null
+++ b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/GroupsIT.java
@@ -0,0 +1,47 @@
+/*
+ * 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.compliance.tests;
+
+import org.apache.directory.scim.compliance.junit.EmbeddedServerExtension;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.Matchers.*;
+import static org.hamcrest.Matchers.emptyString;
+
+@ExtendWith(EmbeddedServerExtension.class)
+public class GroupsIT extends ScimpleITSupport {
+
+  @Test
+  @DisplayName("Verify Groups endpoint")
+  public void groupsEndpoint() {
+    get("/Groups")
+      .statusCode(200)
+      .body(
+        "Resources", not(empty()),
+        "schemas", hasItem(SCHEMA_LIST_RESPONSE),
+        "itemsPerPage", isNumber(),
+        "startIndex", isNumber(),
+        "totalResults", isNumber(),
+        "Resources[0].id", not(emptyString())
+      );
+  }
+}
diff --git a/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/ResourceTypesIT.java b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/ResourceTypesIT.java
new file mode 100644
index 0000000..2c00af1
--- /dev/null
+++ b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/ResourceTypesIT.java
@@ -0,0 +1,55 @@
+/*
+ * 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.compliance.tests;
+
+import org.apache.directory.scim.compliance.junit.EmbeddedServerExtension;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.Matchers.*;
+
+@ExtendWith(EmbeddedServerExtension.class)
+public class ResourceTypesIT extends ScimpleITSupport {
+
+  @Test
+  @DisplayName("ResourceTypes endpoint")
+  public void resourceTypes() {
+    get("/ResourceTypes")
+      .statusCode(200)
+      .body(
+        "Resources", not(empty()),
+        "schemas", hasItem(SCHEMA_LIST_RESPONSE),
+        "itemsPerPage", isNumber(),
+        "startIndex", isNumber(),
+        "totalResults", isNumber(),
+        "Resources[0].name", not(emptyString()),
+        "Resources[0].endpoint", not(emptyString()),
+        "Resources[0].schema", not(emptyString())
+      );
+}
+
+  @Test
+  @DisplayName("Check if ResourceTypes is read-only")
+  public void endpointIsReadOnly() {
+    post("/ResourceTypes", "")
+      .statusCode(405);
+  }
+}
diff --git a/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/SchemasIT.java b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/SchemasIT.java
new file mode 100644
index 0000000..4604935
--- /dev/null
+++ b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/SchemasIT.java
@@ -0,0 +1,54 @@
+/*
+ * 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.compliance.tests;
+
+import org.apache.directory.scim.compliance.junit.EmbeddedServerExtension;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.Matchers.*;
+
+@ExtendWith(EmbeddedServerExtension.class)
+public class SchemasIT extends ScimpleITSupport {
+
+  @Test
+  @DisplayName("ResourceTypes endpoint")
+  public void resourceTypes() {
+    get("/Schemas")
+      .statusCode(200)
+      .body(
+        "Resources", not(empty()),
+        "schemas", hasItem(SCHEMA_LIST_RESPONSE),
+        "itemsPerPage", isNumber(),
+        "startIndex", isNumber(),
+        "totalResults", isNumber(),
+        "Resources[0].id", not(emptyString()),
+        "Resources[0].attributes", not(emptyString())
+      );
+  }
+
+  @Test
+  @DisplayName("Check if Schemas is read-only")
+  public void endpointIsReadOnly() {
+    post("/Schemas", "")
+      .statusCode(405);
+  }
+}
diff --git a/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/ScimpleITSupport.java b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/ScimpleITSupport.java
new file mode 100644
index 0000000..999608c
--- /dev/null
+++ b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/ScimpleITSupport.java
@@ -0,0 +1,145 @@
+/*
+ * 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.compliance.tests;
+
+import io.restassured.filter.Filter;
+import io.restassured.filter.FilterContext;
+import io.restassured.filter.log.LogDetail;
+import io.restassured.filter.log.RequestLoggingFilter;
+import io.restassured.response.Response;
+import io.restassured.response.ValidatableResponse;
+import io.restassured.specification.FilterableRequestSpecification;
+import io.restassured.specification.FilterableResponseSpecification;
+import org.apache.directory.scim.compliance.junit.EmbeddedServerExtension.ScimServerUri;
+import org.hamcrest.Matcher;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static io.restassured.RestAssured.given;
+import static java.util.Collections.emptyMap;
+import static org.hamcrest.Matchers.instanceOf;
+
+public class ScimpleITSupport {
+
+  static final String SCIM_MEDIA_TYPE = "application/scim+json";
+  static final String SCHEMA_LIST_RESPONSE = "urn:ietf:params:scim:api:messages:2.0:ListResponse";
+  static final String SCHEMA_ERROR_RESPONSE = "urn:ietf:params:scim:api:messages:2.0:Error";
+
+  @ScimServerUri
+  private URI uri = URI.create("http://localhost:8080/v2");
+
+  private final boolean loggingEnabled = Boolean.getBoolean("scim.tests.logging.enabled");
+
+  private final Map<String, String> requestHeaders = Map.of(
+    "User-Agent", "Apache SCIMple Compliance Tests",
+    "Accept-Charset", "utf-8",
+    "Authorization", "TODO"
+  );
+
+  protected URI uri() {
+    return uri;
+  }
+
+  protected URI uri(String path) {
+    return uri(path, emptyMap());
+  }
+
+
+  protected URI uri(String path, Map<String, String> query) {
+    URI uri = uri();
+    String queryString = query.isEmpty() ? null :
+      query.keySet()
+        .stream().map(key -> key + "=" + query.get(key))
+        .collect(Collectors.joining("&"));
+    try {
+      return new URI(uri.getScheme(), uri.getUserInfo(), uri.getHost(), uri.getPort(), uri.getPath() + path, queryString, null);
+    } catch (URISyntaxException e) {
+      throw new RuntimeException(e);
+    }
+  }
+
+  protected ValidatableResponse get(String path) {
+    return get(path, emptyMap());
+  }
+
+  protected ValidatableResponse get(String path, Map<String, String> query) {
+    ValidatableResponse responseSpec =
+      given()
+        .urlEncodingEnabled(false) // URL encoding is handled but the URI
+        .redirects().follow(false)
+        .accept(SCIM_MEDIA_TYPE)
+        .headers(requestHeaders)
+      .when()
+        .filter(logging(loggingEnabled))
+        .get(uri(path, query))
+      .then()
+        .contentType(SCIM_MEDIA_TYPE);
+
+      if (loggingEnabled) {
+        responseSpec.log().everything();
+      }
+      return responseSpec;
+  }
+
+  protected ValidatableResponse post(String path, String body) {
+    ValidatableResponse responseSpec =
+      given()
+        .urlEncodingEnabled(false) // URL encoding is handled but the URI
+        .redirects().follow(false)
+        .accept(SCIM_MEDIA_TYPE)
+        .contentType(SCIM_MEDIA_TYPE)
+        .headers(requestHeaders)
+      .when()
+        .filter(logging(loggingEnabled))
+        .body(body)
+        .post(uri(path))
+      .then()
+        .contentType(SCIM_MEDIA_TYPE);
+
+    if (loggingEnabled) {
+      responseSpec.log().everything();
+    }
+    return responseSpec;
+  }
+
+  static Filter logging(boolean enabled) {
+    return enabled
+      ? new RequestLoggingFilter(LogDetail.ALL)
+      : new NoOpFilter();
+  }
+
+  static Matcher<?> isBoolean() {
+    return instanceOf(Boolean.class);
+  }
+
+  static Matcher<?> isNumber() {
+    return instanceOf(Number.class);
+  }
+
+  static class NoOpFilter implements Filter {
+    @Override
+    public Response filter(FilterableRequestSpecification requestSpec, FilterableResponseSpecification responseSpec, FilterContext ctx) {
+      return ctx.next(requestSpec, responseSpec);
+    }
+  }
+}
diff --git a/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/ServiceProviderConfigIT.java b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/ServiceProviderConfigIT.java
new file mode 100644
index 0000000..4d12a0d
--- /dev/null
+++ b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/ServiceProviderConfigIT.java
@@ -0,0 +1,60 @@
+/*
+ * 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.compliance.tests;
+
+import org.apache.directory.scim.compliance.junit.EmbeddedServerExtension;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import static org.hamcrest.Matchers.*;
+
+@ExtendWith(EmbeddedServerExtension.class)
+public class ServiceProviderConfigIT extends ScimpleITSupport {
+
+  @Test
+  @DisplayName("ServiceProviderConfig endpoint")
+  public void config() {
+    get("/ServiceProviderConfig")
+      .statusCode(200)
+      .body(
+        "schemas", hasItem("urn:ietf:params:scim:schemas:core:2.0:ServiceProviderConfig"),
+        "patch.supported", isBoolean(),
+        "bulk.supported", isBoolean(),
+        "bulk.maxOperations", isNumber(),
+        "bulk.maxPayloadSize", isNumber(),
+        "filter.supported", isBoolean(),
+        "filter.maxResults", isNumber(),
+        "changePassword.supported", isBoolean(),
+        "sort.supported", isBoolean(),
+        "etag.supported", isBoolean(),
+        "authenticationSchemes", not(empty()),
+        "authenticationSchemes[0].type", not(emptyString()),
+        "authenticationSchemes[0].description", not(emptyString())
+      );
+  }
+
+  @Test
+  @DisplayName("Check if ServiceProviderConfig is read-only")
+  public void endpoingIsReadOnly() {
+    post("/ServiceProviderConfig", "")
+      .statusCode(405);
+  }
+}
diff --git a/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/UsersIT.java b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/UsersIT.java
new file mode 100644
index 0000000..ff97a47
--- /dev/null
+++ b/scim-compliance-tests/src/main/java/org/apache/directory/scim/compliance/tests/UsersIT.java
@@ -0,0 +1,178 @@
+/*
+ * 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.compliance.tests;
+
+import org.apache.commons.lang3.RandomStringUtils;
+import org.apache.directory.scim.compliance.junit.EmbeddedServerExtension;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+import java.util.Map;
+
+import static org.hamcrest.Matchers.*;
+
+@ExtendWith(EmbeddedServerExtension.class)
+public class UsersIT extends ScimpleITSupport {
+
+  private final String givenName = "Given-" + RandomStringUtils.randomAlphanumeric(10);
+  private final String familyName = "Family-" + RandomStringUtils.randomAlphanumeric(10);
+  private final String displayName = givenName + " " + familyName;
+  private final String email = givenName + "." + familyName + "@example.com";
+
+  @Test
+  @DisplayName("Test Users endpoint")
+  public void userEndpoint() {
+    get("/Users", Map.of("count", "1","startIndex", "1"))
+      .statusCode(200)
+      .body(
+        "Resources", not(empty()),
+        "schemas", hasItem(SCHEMA_LIST_RESPONSE),
+        "itemsPerPage", isNumber(),
+        "startIndex", isNumber(),
+        "totalResults", isNumber(),
+        "Resources[0].id", not(emptyString()),
+        "Resources[0].name.familyName", not(emptyString()),
+        "Resources[0].userName", not(emptyString()),
+        "Resources[0].active", isBoolean(),
+        "Resources[0].name.familyName", not(emptyString()),
+        "Resources[0].emails[0].value", not(emptyString())
+      );
+  }
+
+  @Test
+  @DisplayName("Get Users/{{id}}")
+  public void userById() {
+    String id = get("/Users", Map.of("count", "1","startIndex", "1"))
+      .extract().jsonPath().get("Resources[0].id");
+
+    get("/Users/" + id)
+      .statusCode(200)
+      .body(
+        "id", is(id),
+        "name.familyName", not(emptyString()),
+        "name.givenName", not(emptyString()),
+        "userName", not(emptyString()),
+        "active", isBoolean(),
+        "emails[0].value", not(emptyString())
+      );
+  }
+
+  @Test
+  @DisplayName("Test invalid User by username")
+  public void invalidUserNameFilter() {
+    String invalidUserName = RandomStringUtils.randomAlphanumeric(10);
+
+    get("/Users", Map.of("filter", "userName eq \"" + invalidUserName + "\""))
+      .statusCode(200)
+      .body(
+        "schemas", hasItem(SCHEMA_LIST_RESPONSE),
+        "totalResults", is(0)
+      );
+  }
+
+  @Test
+  @DisplayName("Test invalid User by ID")
+  public void invalidUserId() {
+    String invalidId = RandomStringUtils.randomAlphanumeric(10);
+
+    get("/Users/" + invalidId)
+      .statusCode(404)
+      .body(
+        "schemas", hasItem(SCHEMA_ERROR_RESPONSE),
+        "detail", not(emptyString())
+      );
+  }
+
+  @Test
+  @DisplayName("Create user with realistic values")
+  @Order(10)
+  public void createUser() {
+
+    String body = "{" +
+        "\"schemas\":[\"urn:ietf:params:scim:schemas:core:2.0:User\"]," +
+        "\"userName\":\"" + email + "\"," +
+        "\"name\":{" +
+          "\"givenName\":\"" + givenName + "\"," +
+          "\"familyName\":\"" + familyName + "\"}," +
+        "\"emails\":[{" +
+          "\"primary\":true," +
+          "\"value\":\"" + email + "\"," +
+          "\"type\":\"work\"}]," +
+        "\"displayName\":\"" + displayName + "\"," +
+        "\"active\":true" +
+        "}";
+
+    String id = post("/Users", body)
+      .statusCode(201)
+      .body(
+        "schemas", contains("urn:ietf:params:scim:schemas:core:2.0:User"),
+        "active", is(true),
+        "id", not(emptyString()),
+        "name.givenName", is(givenName),
+        "name.familyName", is(familyName),
+        "userName", is(email)
+      )
+      .extract().jsonPath().get("id");
+
+    // retrieve the user by id
+    get("/Users/" + id)
+      .statusCode(200)
+      .body(
+        "schemas", contains("urn:ietf:params:scim:schemas:core:2.0:User"),
+        "active", is(true),
+        "id", not(emptyString()),
+        "name.givenName", is(givenName),
+        "name.familyName", is(familyName),
+        "userName", is(email)
+      );
+
+    // posting same content again should return a conflict (409)
+    post("/Users", body)
+      .statusCode(409)
+      .body(
+        "schemas", hasItem(SCHEMA_ERROR_RESPONSE),
+        "detail", not(emptyString())
+      );
+  }
+
+  @Test
+  @DisplayName("Username Case Sensitivity Check")
+  public void userNameByFilter() {
+    String userName = get("/Users", Map.of("count", "1","startIndex", "1"))
+      .extract().jsonPath().get("Resources[0].userName");
+
+    get("/Users", Map.of("filter", "userName eq \"" + userName + "\""))
+      .statusCode(200)
+      .contentType("application/scim+json")
+      .body(
+        "schemas", contains(SCHEMA_LIST_RESPONSE),
+        "totalResults", is(1)
+      );
+
+    get("/Users", Map.of("filter", "userName eq \"" + userName.toUpperCase() + "\""))
+      .statusCode(200)
+      .body(
+        "schemas", contains(SCHEMA_LIST_RESPONSE),
+        "totalResults", is(1)
+      );
+  }
+}
diff --git a/scim-compliance-tests/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension b/scim-compliance-tests/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
new file mode 100644
index 0000000..a7704b9
--- /dev/null
+++ b/scim-compliance-tests/src/main/resources/META-INF/services/org.junit.jupiter.api.extension.Extension
@@ -0,0 +1 @@
+org.apache.directory.scim.compliance.junit.EmbeddedServerExtension
diff --git a/scim-server-examples/scim-server-jersey/pom.xml b/scim-server-examples/scim-server-jersey/pom.xml
index f7ff32d..2a6883d 100644
--- a/scim-server-examples/scim-server-jersey/pom.xml
+++ b/scim-server-examples/scim-server-jersey/pom.xml
@@ -27,45 +27,17 @@
   <artifactId>scim-server-jersey</artifactId>
   <name>SCIM - Server - Examples - Jersey</name>
 
-  <properties>
-    <jersey.version>3.0.5</jersey.version>
-  </properties>
-
-  <dependencyManagement>
-    <dependencies>
-      <dependency>
-        <groupId>org.glassfish.jersey</groupId>
-        <artifactId>jersey-bom</artifactId>
-        <version>${jersey.version}</version>
-        <type>pom</type>
-        <scope>import</scope>
-      </dependency>
-    </dependencies>
-  </dependencyManagement>
-
-
-
   <dependencies>
 
     <dependency>
       <groupId>org.glassfish.jersey.containers</groupId>
-      <artifactId>jersey-container-jetty-http</artifactId>
+      <artifactId>jersey-container-grizzly2-http</artifactId>
     </dependency>
     <dependency>
       <groupId>org.glassfish.jersey.inject</groupId>
       <artifactId>jersey-hk2</artifactId>
     </dependency>
     <dependency>
-      <groupId>org.glassfish.jersey.media</groupId>
-      <artifactId>jersey-media-json-jackson</artifactId>
-    </dependency>
-
-    <dependency>
-      <groupId>org.glassfish.jersey.ext.cdi</groupId>
-      <artifactId>jersey-weld2-se</artifactId>
-      <scope>provided</scope>
-    </dependency>
-    <dependency>
       <groupId>org.glassfish.jersey.ext.cdi</groupId>
       <artifactId>jersey-weld2-se</artifactId>
     </dependency>
@@ -73,7 +45,6 @@
       <groupId>org.jboss.weld.se</groupId>
       <artifactId>weld-se-core</artifactId>
     </dependency>
-
     <dependency>
       <groupId>jakarta.servlet</groupId>
       <artifactId>jakarta.servlet-api</artifactId>
diff --git a/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/JerseyApplication.java b/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/JerseyApplication.java
index 5fcd742..1ff38f1 100644
--- a/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/JerseyApplication.java
+++ b/scim-server-examples/scim-server-jersey/src/main/java/org/apache/directory/scim/example/jersey/JerseyApplication.java
@@ -20,6 +20,7 @@
 package org.apache.directory.scim.example.jersey;
 
 import jakarta.inject.Inject;
+import jakarta.ws.rs.core.UriBuilder;
 import org.apache.directory.scim.server.ScimConfiguration;
 import org.apache.directory.scim.server.configuration.ServerConfiguration;
 import org.apache.directory.scim.server.rest.ScimResourceHelper;
@@ -29,15 +30,13 @@
 import java.util.Set;
 
 import jakarta.ws.rs.core.Application;
-import org.eclipse.jetty.server.Server;
-import org.glassfish.jersey.jetty.JettyHttpContainerFactory;
-import org.glassfish.jersey.server.ApplicationHandler;
+import org.glassfish.grizzly.http.server.HttpServer;
+import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
+import org.glassfish.jersey.server.ResourceConfig;
 import org.jboss.weld.environment.se.Weld;
 import org.slf4j.LoggerFactory;
 import org.slf4j.bridge.SLF4JBridgeHandler;
 
-import static org.apache.directory.scim.spec.schema.ServiceProviderConfiguration.AuthenticationSchema.oauthBearer;
-
 // @ApplicationPath("v2")
 // Embedded Jersey + Jetty ignores the ApplicationPath annotation
 // https://github.com/eclipse-ee4j/jersey/issues/3222
@@ -86,13 +85,14 @@
       weld.addPackages(true, JerseyApplication.class.getPackage());
       weld.initialize();
 
-      ApplicationHandler applicationHandler = new ApplicationHandler(JerseyApplication.class);
-      final Server server = JettyHttpContainerFactory.createServer(URI.create(BASE_URI), applicationHandler.getConfiguration());
+      ResourceConfig resourceConfig = ResourceConfig.forApplication(new JerseyApplication());
+      URI uri = UriBuilder.fromUri("http://localhost/").port(8080).build();
+      final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri, resourceConfig);
 
       Runtime.getRuntime().addShutdownHook(new Thread(() -> {
         try {
           System.out.println("Shutting down the application...");
-          server.stop();
+          server.shutdown();
           weld.shutdown();
           System.out.println("Done, exit.");
         } catch (Exception e) {
diff --git a/scim-server/pom.xml b/scim-server/pom.xml
index 8dbd477..583617a 100644
--- a/scim-server/pom.xml
+++ b/scim-server/pom.xml
@@ -108,6 +108,33 @@
       <artifactId>zjsonpatch</artifactId>
       <version>0.4.12</version>
     </dependency>
+
+    <dependency>
+      <groupId>org.apache.directory.scim</groupId>
+      <artifactId>scim-compliance-tests</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.glassfish.jersey.containers</groupId>
+      <artifactId>jersey-container-grizzly2-http</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.glassfish.jersey.inject</groupId>
+      <artifactId>jersey-hk2</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.glassfish.jersey.ext.cdi</groupId>
+      <artifactId>jersey-weld2-se</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.jboss.weld.se</groupId>
+      <artifactId>weld-se-core</artifactId>
+      <scope>test</scope>
+    </dependency>
+
   </dependencies>
 
 </project>
diff --git a/scim-server/src/main/java/org/apache/directory/scim/server/rest/ScimResourceHelper.java b/scim-server/src/main/java/org/apache/directory/scim/server/rest/ScimResourceHelper.java
index aa2cc13..0a46544 100644
--- a/scim-server/src/main/java/org/apache/directory/scim/server/rest/ScimResourceHelper.java
+++ b/scim-server/src/main/java/org/apache/directory/scim/server/rest/ScimResourceHelper.java
@@ -44,22 +44,21 @@
    * @return the JAX-RS annotated classes.
    */
   public static Set<Class<?>> getScimClassesToLoad() {
-    Set<Class<?>> clazzez = new HashSet<>();
 
     // Required scim classes.
-    clazzez.add(BulkResourceImpl.class);
-    clazzez.add(GroupResourceImpl.class);
-    clazzez.add(ResourceTypesResourceImpl.class);
-    clazzez.add(SchemaResourceImpl.class);
-    clazzez.add(SearchResourceImpl.class);
-    clazzez.add(SelfResourceImpl.class);
-    clazzez.add(ServiceProviderConfigResourceImpl.class);
-    clazzez.add(UserResourceImpl.class);
-    clazzez.add(FilterParseExceptionMapper.class);
+    return Set.of(
+      BulkResourceImpl.class,
+      GroupResourceImpl.class,
+      ResourceTypesResourceImpl.class,
+      SchemaResourceImpl.class,
+      SearchResourceImpl.class,
+      SelfResourceImpl.class,
+      ServiceProviderConfigResourceImpl.class,
+      UserResourceImpl.class,
+      FilterParseExceptionMapper.class,
+      WebApplicationExceptionMapper.class,
 
     // handle MediaType of application/scim+json
-    clazzez.add(ScimJacksonXmlBindJsonProvider.class);
-
-    return clazzez;
+    ScimJacksonXmlBindJsonProvider.class);
   }
 }
diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/it/JerseyTestServer.java b/scim-server/src/test/java/org/apache/directory/scim/server/it/JerseyTestServer.java
new file mode 100644
index 0000000..9cd6676
--- /dev/null
+++ b/scim-server/src/test/java/org/apache/directory/scim/server/it/JerseyTestServer.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.server.it;
+
+import jakarta.ws.rs.core.UriBuilder;
+import org.apache.directory.scim.compliance.junit.EmbeddedServerExtension;
+import org.glassfish.grizzly.http.server.HttpServer;
+import org.glassfish.jersey.grizzly2.httpserver.GrizzlyHttpServerFactory;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.jboss.weld.environment.se.Weld;
+
+import org.apache.directory.scim.server.it.testapp.App;
+
+import java.net.URI;
+
+public class JerseyTestServer implements EmbeddedServerExtension.ScimTestServer {
+
+  private HttpServer server;
+  private Weld weld;
+
+  @Override
+  public URI start(int port) {
+    weld = new Weld();
+    // ensure Weld discovers the beans in this project
+    weld.addPackages(true, App.class.getPackage());
+    weld.initialize();
+
+    ResourceConfig rc = ResourceConfig.forApplication(new App());
+    URI uri = UriBuilder.fromUri("http://localhost/").port(port).build();
+    server = GrizzlyHttpServerFactory.createHttpServer(uri, rc);
+
+    return uri;
+  }
+
+  @Override
+  public void shutdown() {
+    if (server != null) {
+      server.shutdown();
+    }
+    if (weld != null) {
+      weld.shutdown();
+    }
+  }
+}
diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/App.java b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/App.java
new file mode 100644
index 0000000..0044689
--- /dev/null
+++ b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/App.java
@@ -0,0 +1,59 @@
+/*
+ * 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.server.it.testapp;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.core.Application;
+import org.apache.directory.scim.server.ScimConfiguration;
+import org.apache.directory.scim.server.configuration.ServerConfiguration;
+import org.apache.directory.scim.server.rest.ScimResourceHelper;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.apache.directory.scim.spec.schema.ServiceProviderConfiguration.AuthenticationSchema.httpBasic;
+
+public class App extends Application {
+
+  @Override
+  public Set<Class<?>> getClasses() {
+    Set<Class<?>> clazzes = new HashSet<>(ScimResourceHelper.getScimClassesToLoad());
+    clazzes.add(ServerConfigInitializer.class);
+    return clazzes;
+  }
+
+  /**
+   * A {@link ScimConfiguration} allow for eager initialization of beans, this class configures the {@link ServerConfiguration}.
+   */
+  public static class ServerConfigInitializer implements ScimConfiguration {
+
+    @Inject
+    private ServerConfiguration serverConfiguration;
+
+    @Override
+    public void configure() {
+
+      // Set any unique configuration bits
+      serverConfiguration
+        .setId("scimple-server-its")
+        .addAuthenticationSchema(httpBasic());
+    }
+  }
+}
diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java
new file mode 100644
index 0000000..7551899
--- /dev/null
+++ b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryGroupService.java
@@ -0,0 +1,136 @@
+/*
+* 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.server.it.testapp;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+import jakarta.ws.rs.core.Response;
+import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
+import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
+import org.apache.directory.scim.server.repository.Repository;
+import org.apache.directory.scim.server.repository.UpdateRequest;
+import org.apache.directory.scim.server.schema.SchemaRegistry;
+import org.apache.directory.scim.spec.protocol.filter.FilterExpressions;
+import org.apache.directory.scim.spec.protocol.filter.FilterResponse;
+import org.apache.directory.scim.spec.protocol.search.Filter;
+import org.apache.directory.scim.spec.protocol.search.PageRequest;
+import org.apache.directory.scim.spec.protocol.search.SortRequest;
+import org.apache.directory.scim.spec.resources.ScimExtension;
+import org.apache.directory.scim.spec.resources.ScimGroup;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Named
+@ApplicationScoped
+public class InMemoryGroupService implements Repository<ScimGroup> {
+
+  private final Map<String, ScimGroup> groups = new HashMap<>();
+
+  private SchemaRegistry schemaRegistry;
+
+  @Inject
+  public InMemoryGroupService(SchemaRegistry schemaRegistry) {
+    this.schemaRegistry = schemaRegistry;
+  }
+
+  protected InMemoryGroupService() {}
+
+  @PostConstruct
+  public void init() {
+    ScimGroup group = new ScimGroup();
+    group.setId("example-group");
+    groups.put(group.getId(), group);
+  }
+
+  @Override
+  public Class<ScimGroup> getResourceClass() {
+    return ScimGroup.class;
+  }
+
+  @Override
+  public ScimGroup create(ScimGroup resource) throws UnableToCreateResourceException {
+    String resourceId = resource.getId();
+    int idCandidate = resource.hashCode();
+    String id = resourceId != null ? resourceId : Integer.toString(idCandidate);
+
+    while (groups.containsKey(id)) {
+      id = Integer.toString(idCandidate);
+      ++idCandidate;
+    }
+
+    // check to make sure the group doesn't already exist
+    boolean existingUserFound = groups.values().stream()
+      .anyMatch(group -> group.getExternalId().equals(resource.getExternalId()));
+    if (existingUserFound) {
+      // HTTP leaking into data layer
+      throw new UnableToCreateResourceException(Response.Status.CONFLICT, "Group '" + resource.getExternalId() + "' already exists.");
+    }
+
+    resource.setId(id);
+    groups.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimGroup update(UpdateRequest<ScimGroup> updateRequest) throws UnableToUpdateResourceException {
+    String id = updateRequest.getId();
+    ScimGroup resource = updateRequest.getResource();
+    groups.put(id, resource);
+    return resource;
+  }
+
+  @Override
+  public ScimGroup get(String id) {
+    return groups.get(id);
+  }
+
+  @Override
+  public void delete(String id) {
+    groups.remove(id);
+  }
+
+  @Override
+  public FilterResponse<ScimGroup> find(Filter filter, PageRequest pageRequest, SortRequest sortRequest) {
+    long count = pageRequest.getCount() != null ? pageRequest.getCount() : groups.size();
+    long startIndex = pageRequest.getStartIndex() != null
+      ? pageRequest.getStartIndex() - 1 // SCIM is 1-based indexed
+      : 0;
+
+    List<ScimGroup> result = groups.values().stream()
+      .skip(startIndex)
+      .limit(count)
+      .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimGroup.SCHEMA_URI)))
+      .collect(Collectors.toList());
+
+    return new FilterResponse<>(result, pageRequest, result.size());
+  }
+
+  @Override
+  public List<Class<? extends ScimExtension>> getExtensionList() {
+    return Collections.emptyList();
+  }
+
+}
diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemorySelfResolverImpl.java b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemorySelfResolverImpl.java
new file mode 100644
index 0000000..4988940
--- /dev/null
+++ b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemorySelfResolverImpl.java
@@ -0,0 +1,36 @@
+/*
+* 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.server.it.testapp;
+
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.ws.rs.core.Response.Status;
+import org.apache.directory.scim.server.exception.UnableToResolveIdResourceException;
+import org.apache.directory.scim.server.repository.SelfIdResolver;
+
+import java.security.Principal;
+
+@ApplicationScoped
+public class InMemorySelfResolverImpl implements SelfIdResolver {
+
+  @Override
+  public String resolveToInternalId(Principal principal) throws UnableToResolveIdResourceException {
+    throw new UnableToResolveIdResourceException(Status.NOT_IMPLEMENTED, "Caller Principal not available");
+  }
+}
diff --git a/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java
new file mode 100644
index 0000000..e597877
--- /dev/null
+++ b/scim-server/src/test/java/org/apache/directory/scim/server/it/testapp/InMemoryUserService.java
@@ -0,0 +1,179 @@
+/*
+* 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.server.it.testapp;
+
+import jakarta.annotation.PostConstruct;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+import jakarta.ws.rs.core.Response;
+import org.apache.directory.scim.server.exception.UnableToCreateResourceException;
+import org.apache.directory.scim.server.exception.UnableToUpdateResourceException;
+import org.apache.directory.scim.server.repository.Repository;
+import org.apache.directory.scim.server.repository.UpdateRequest;
+import org.apache.directory.scim.server.schema.SchemaRegistry;
+import org.apache.directory.scim.spec.protocol.filter.FilterExpressions;
+import org.apache.directory.scim.spec.protocol.filter.FilterResponse;
+import org.apache.directory.scim.spec.protocol.search.Filter;
+import org.apache.directory.scim.spec.protocol.search.PageRequest;
+import org.apache.directory.scim.spec.protocol.search.SortRequest;
+import org.apache.directory.scim.spec.resources.*;
+
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * Creates a singleton (effectively) Repository<ScimUser> with a memory-based
+ * persistence layer.
+ *
+ * @author Chris Harm &lt;crh5255@psu.edu&gt;
+ */
+@Named
+@ApplicationScoped
+public class InMemoryUserService implements Repository<ScimUser> {
+
+  static final String DEFAULT_USER_ID = "1";
+  static final String DEFAULT_USER_EXTERNAL_ID = "e" + DEFAULT_USER_ID;
+  static final String DEFAULT_USER_DISPLAY_NAME = "User " + DEFAULT_USER_ID;
+  static final String DEFAULT_USER_EMAIL_VALUE = "e1@example.com";
+  static final String DEFAULT_USER_EMAIL_TYPE = "work";
+  static final int DEFAULT_USER_LUCKY_NUMBER = 7;
+
+  private final Map<String, ScimUser> users = new HashMap<>();
+
+  private SchemaRegistry schemaRegistry;
+
+  @Inject
+  public InMemoryUserService(SchemaRegistry schemaRegistry) {
+    this.schemaRegistry = schemaRegistry;
+  }
+
+  protected InMemoryUserService() {}
+
+  @PostConstruct
+  public void init() {
+    ScimUser user = new ScimUser();
+    user.setId(DEFAULT_USER_ID);
+    user.setExternalId(DEFAULT_USER_EXTERNAL_ID);
+    user.setUserName(DEFAULT_USER_EXTERNAL_ID);
+    user.setDisplayName(DEFAULT_USER_DISPLAY_NAME);
+    user.setName(new Name()
+      .setGivenName("Tester")
+      .setFamilyName("McTest"));
+    Email email = new Email();
+    email.setDisplay(DEFAULT_USER_EMAIL_VALUE);
+    email.setValue(DEFAULT_USER_EMAIL_VALUE);
+    email.setType(DEFAULT_USER_EMAIL_TYPE);
+    email.setPrimary(true);
+    user.setEmails(List.of(email));
+
+    users.put(user.getId(), user);
+  }
+
+  @Override
+  public Class<ScimUser> getResourceClass() {
+    return ScimUser.class;
+  }
+
+  /**
+   * @see Repository#create(ScimResource)
+   */
+  @Override
+  public ScimUser create(ScimUser resource) throws UnableToCreateResourceException {
+    String resourceId = resource.getId();
+    int idCandidate = resource.hashCode();
+    String id = resourceId != null ? resourceId : Integer.toString(idCandidate);
+
+    while (users.containsKey(id)) {
+      id = Integer.toString(idCandidate);
+      ++idCandidate;
+    }
+
+    // check to make sure the user doesn't already exist
+    boolean existingUserFound = users.values().stream()
+      .anyMatch(user -> user.getUserName().equals(resource.getUserName()));
+    if (existingUserFound) {
+      // HTTP leaking into data layer
+      throw new UnableToCreateResourceException(Response.Status.CONFLICT, "User '" + resource.getUserName() + "' already exists.");
+    }
+
+    resource.setId(id);
+    users.put(id, resource);
+    return resource;
+  }
+
+  /**
+   * @see Repository#update(UpdateRequest)
+   */
+  @Override
+  public ScimUser update(UpdateRequest<ScimUser> updateRequest) throws UnableToUpdateResourceException {
+    String id = updateRequest.getId();
+    ScimUser resource = updateRequest.getResource();
+    users.put(id, resource);
+    return resource;
+  }
+
+  /**
+   * @see Repository#get(String)
+   */
+  @Override
+  public ScimUser get(String id) {
+    return users.get(id);
+  }
+
+  /**
+   * @see Repository#delete(String)
+   */
+  @Override
+  public void delete(String id) {
+    users.remove(id);
+  }
+
+  /**
+   * @see Repository#find(Filter, PageRequest, SortRequest)
+   */
+  @Override
+  public FilterResponse<ScimUser> find(Filter filter, PageRequest pageRequest, SortRequest sortRequest) {
+
+    long count = pageRequest.getCount() != null ? pageRequest.getCount() : users.size();
+    long startIndex = pageRequest.getStartIndex() != null
+      ? pageRequest.getStartIndex() - 1 // SCIM is 1-based indexed
+      : 0;
+
+    List<ScimUser> result = users.values().stream()
+      .skip(startIndex)
+      .limit(count)
+      .filter(FilterExpressions.inMemory(filter, schemaRegistry.getSchema(ScimUser.SCHEMA_URI)))
+      .collect(Collectors.toList());
+
+    return new FilterResponse<>(result, pageRequest, result.size());
+  }
+
+  /**
+   * @see Repository#getExtensionList()
+   */
+  @Override
+  public List<Class<? extends ScimExtension>> getExtensionList() {
+    return Collections.emptyList();
+  }
+}
diff --git a/scim-server/src/test/resources/META-INF/services/org.apache.directory.scim.compliance.junit.EmbeddedServerExtension$ScimTestServer b/scim-server/src/test/resources/META-INF/services/org.apache.directory.scim.compliance.junit.EmbeddedServerExtension$ScimTestServer
new file mode 100644
index 0000000..77cfa7f
--- /dev/null
+++ b/scim-server/src/test/resources/META-INF/services/org.apache.directory.scim.compliance.junit.EmbeddedServerExtension$ScimTestServer
@@ -0,0 +1 @@
+org.apache.directory.scim.server.it.JerseyTestServer
diff --git a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/InMemoryScimFilterMatcher.java b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/InMemoryScimFilterMatcher.java
index 5faec26..1802deb 100644
--- a/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/InMemoryScimFilterMatcher.java
+++ b/scim-spec/scim-spec-protocol/src/main/java/org/apache/directory/scim/spec/protocol/filter/InMemoryScimFilterMatcher.java
@@ -219,9 +219,16 @@
       Object compareValue = expression.getCompareValue();
 
       if (op == CompareOperator.EQ) {
+
+        if (isStringExpression(attribute, compareValue) && !attribute.isCaseExact()) {
+          return actualValue.toString().equalsIgnoreCase(compareValue.toString());
+        }
         return compareValue.equals(actualValue);
       }
       if (op == CompareOperator.NE) {
+        if (isStringExpression(attribute, compareValue) && !attribute.isCaseExact()) {
+          return !actualValue.toString().equalsIgnoreCase(compareValue.toString());
+        }
         return !compareValue.equals(actualValue);
       }
       if (op == CompareOperator.SW) {