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 <crh5255@psu.edu>
+ */
+@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) {