View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.syncope.fit.core;
20  
21  import static org.junit.jupiter.api.Assertions.assertEquals;
22  import static org.junit.jupiter.api.Assertions.assertFalse;
23  import static org.junit.jupiter.api.Assertions.assertNotEquals;
24  import static org.junit.jupiter.api.Assertions.assertNotNull;
25  import static org.junit.jupiter.api.Assertions.assertNull;
26  import static org.junit.jupiter.api.Assertions.assertTrue;
27  import static org.junit.jupiter.api.Assumptions.assumeTrue;
28  
29  import com.fasterxml.jackson.core.JsonProcessingException;
30  import com.fasterxml.jackson.databind.SerializationFeature;
31  import com.fasterxml.jackson.databind.json.JsonMapper;
32  import com.fasterxml.jackson.databind.node.ArrayNode;
33  import com.fasterxml.jackson.databind.node.ObjectNode;
34  import com.fasterxml.jackson.jaxrs.json.JacksonJsonProvider;
35  import java.io.IOException;
36  import java.time.OffsetDateTime;
37  import java.time.format.DateTimeFormatter;
38  import java.time.temporal.ChronoUnit;
39  import java.util.List;
40  import java.util.UUID;
41  import javax.ws.rs.HttpMethod;
42  import javax.ws.rs.core.GenericType;
43  import javax.ws.rs.core.HttpHeaders;
44  import javax.ws.rs.core.Response;
45  import org.apache.commons.lang3.StringUtils;
46  import org.apache.cxf.jaxrs.client.WebClient;
47  import org.apache.syncope.common.lib.scim.SCIMComplexConf;
48  import org.apache.syncope.common.lib.scim.SCIMConf;
49  import org.apache.syncope.common.lib.scim.SCIMGroupConf;
50  import org.apache.syncope.common.lib.scim.SCIMUserConf;
51  import org.apache.syncope.common.lib.scim.SCIMUserNameConf;
52  import org.apache.syncope.common.lib.scim.types.EmailCanonicalType;
53  import org.apache.syncope.common.lib.to.ProvisioningResult;
54  import org.apache.syncope.common.lib.to.UserTO;
55  import org.apache.syncope.ext.scimv2.api.SCIMConstants;
56  import org.apache.syncope.ext.scimv2.api.data.Group;
57  import org.apache.syncope.ext.scimv2.api.data.ListResponse;
58  import org.apache.syncope.ext.scimv2.api.data.Member;
59  import org.apache.syncope.ext.scimv2.api.data.ResourceType;
60  import org.apache.syncope.ext.scimv2.api.data.SCIMComplexValue;
61  import org.apache.syncope.ext.scimv2.api.data.SCIMError;
62  import org.apache.syncope.ext.scimv2.api.data.SCIMGroup;
63  import org.apache.syncope.ext.scimv2.api.data.SCIMSearchRequest;
64  import org.apache.syncope.ext.scimv2.api.data.SCIMUser;
65  import org.apache.syncope.ext.scimv2.api.data.SCIMUserName;
66  import org.apache.syncope.ext.scimv2.api.data.ServiceProviderConfig;
67  import org.apache.syncope.ext.scimv2.api.data.Value;
68  import org.apache.syncope.ext.scimv2.api.type.ErrorType;
69  import org.apache.syncope.ext.scimv2.api.type.Resource;
70  import org.apache.syncope.fit.AbstractITCase;
71  import org.junit.jupiter.api.BeforeAll;
72  import org.junit.jupiter.api.Test;
73  
74  public class SCIMITCase extends AbstractITCase {
75  
76      public static final String SCIM_ADDRESS = "http://localhost:9080/syncope/rest/scim/v2";
77  
78      private static final SCIMConf CONF;
79  
80      private static Boolean ENABLED;
81  
82      static {
83          CONF = new SCIMConf();
84  
85          CONF.setGroupConf(new SCIMGroupConf());
86  
87          CONF.getGroupConf().setExternalId("originalName");
88  
89          CONF.setUserConf(new SCIMUserConf());
90  
91          CONF.getUserConf().setNickName("ctype");
92          CONF.getUserConf().setDisplayName("cn");
93  
94          CONF.getUserConf().setName(new SCIMUserNameConf());
95          CONF.getUserConf().getName().setGivenName("firstname");
96          CONF.getUserConf().getName().setFamilyName("surname");
97          CONF.getUserConf().getName().setFormatted("fullname");
98  
99          SCIMComplexConf<EmailCanonicalType> email = new SCIMComplexConf<>();
100         email.setValue("userId");
101         email.setType(EmailCanonicalType.work);
102         CONF.getUserConf().getEmails().add(email);
103         email = new SCIMComplexConf<>();
104         email.setValue("email");
105         email.setType(EmailCanonicalType.home);
106         CONF.getUserConf().getEmails().add(email);
107     }
108 
109     private static SCIMUser getSampleUser(final String username) {
110         SCIMUser user = new SCIMUser(null, List.of(Resource.User.schema()), null, username, true);
111         user.setPassword("password123");
112 
113         SCIMUserName name = new SCIMUserName();
114         name.setGivenName(username);
115         name.setFamilyName("surname");
116         name.setFormatted(username);
117         user.setName(name);
118 
119         SCIMComplexValue userId = new SCIMComplexValue();
120         userId.setType(EmailCanonicalType.work.name());
121         userId.setValue(username + "@syncope.apache.org");
122         user.getEmails().add(userId);
123 
124         SCIMComplexValue email = new SCIMComplexValue();
125         email.setType(EmailCanonicalType.home.name());
126         email.setValue(username + "@syncope.apache.org");
127         user.getEmails().add(email);
128 
129         return user;
130     }
131 
132     @BeforeAll
133     public static void isSCIMAvailable() {
134         if (ENABLED == null) {
135             try {
136                 Response response = webClient().path("ServiceProviderConfig").get();
137                 ENABLED = response.getStatus() == 200;
138             } catch (Exception e) {
139                 // ignore
140                 ENABLED = false;
141             }
142         }
143 
144         assumeTrue(ENABLED);
145     }
146 
147     private static WebClient webClient() {
148         return WebClient.create(
149                 SCIM_ADDRESS,
150                 List.of(new JacksonJsonProvider(JsonMapper.builder().
151                         findAndAddModules().disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS).build()))).
152                 accept(SCIMConstants.APPLICATION_SCIM_JSON_TYPE).
153                 type(SCIMConstants.APPLICATION_SCIM_JSON_TYPE).
154                 header(HttpHeaders.AUTHORIZATION, "Bearer " + ADMIN_CLIENT.getJWT());
155     }
156 
157     @Test
158     public void serviceProviderConfig() {
159         Response response = webClient().path("ServiceProviderConfig").get();
160         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
161         assertEquals(
162                 SCIMConstants.APPLICATION_SCIM_JSON,
163                 StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), ";"));
164 
165         ServiceProviderConfig serviceProviderConfig = response.readEntity(ServiceProviderConfig.class);
166         assertNotNull(serviceProviderConfig);
167         assertTrue(serviceProviderConfig.getPatch().isSupported());
168         assertFalse(serviceProviderConfig.getBulk().isSupported());
169         assertTrue(serviceProviderConfig.getChangePassword().isSupported());
170         assertTrue(serviceProviderConfig.getEtag().isSupported());
171         assertTrue(serviceProviderConfig.getSort().isSupported());
172     }
173 
174     @Test
175     public void resourceTypes() {
176         Response response = webClient().path("ResourceTypes").get();
177         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
178         assertEquals(
179                 SCIMConstants.APPLICATION_SCIM_JSON,
180                 StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), ";"));
181 
182         List<ResourceType> resourceTypes = response.readEntity(new GenericType<>() {
183         });
184         assertNotNull(resourceTypes);
185         assertEquals(2, resourceTypes.size());
186 
187         response = webClient().path("ResourceTypes").path("User").get();
188         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
189 
190         ResourceType user = response.readEntity(ResourceType.class);
191         assertNotNull(user);
192         assertEquals(Resource.User.schema(), user.getSchema());
193         assertFalse(user.getSchemaExtensions().isEmpty());
194     }
195 
196     @Test
197     public void schemas() {
198         Response response = webClient().path("Schemas").get();
199         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
200         assertEquals(
201                 SCIMConstants.APPLICATION_SCIM_JSON,
202                 StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), ";"));
203 
204         ArrayNode schemas = response.readEntity(ArrayNode.class);
205         assertNotNull(schemas);
206         assertEquals(3, schemas.size());
207 
208         response = webClient().path("Schemas").path("none").get();
209         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
210 
211         response = webClient().path("Schemas").path(Resource.EnterpriseUser.schema()).get();
212         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
213 
214         ObjectNode enterpriseUser = response.readEntity(ObjectNode.class);
215         assertNotNull(enterpriseUser);
216         assertEquals(Resource.EnterpriseUser.schema(), enterpriseUser.get("id").textValue());
217     }
218 
219     @Test
220     public void read() throws IOException {
221         Response response = webClient().path("Users").path("missing").get();
222         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
223 
224         SCIMError error = response.readEntity(SCIMError.class);
225         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), error.getStatus());
226 
227         response = webClient().path("Users").path("1417acbe-cbf6-4277-9372-e75e04f97000").get();
228         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
229         assertEquals(
230                 SCIMConstants.APPLICATION_SCIM_JSON,
231                 StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), ";"));
232 
233         SCIMUser user = response.readEntity(SCIMUser.class);
234         assertNotNull(user);
235         assertEquals("1417acbe-cbf6-4277-9372-e75e04f97000", user.getId());
236         assertEquals("rossini", user.getUserName());
237         assertFalse(user.getGroups().isEmpty());
238         assertFalse(user.getRoles().isEmpty());
239 
240         response = webClient().path("Users").path("1417acbe-cbf6-4277-9372-e75e04f97000").
241                 query("attributes", "groups").get();
242         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
243         assertEquals(
244                 SCIMConstants.APPLICATION_SCIM_JSON,
245                 StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), ";"));
246 
247         user = response.readEntity(SCIMUser.class);
248         assertNotNull(user);
249         assertEquals("1417acbe-cbf6-4277-9372-e75e04f97000", user.getId());
250         assertNull(user.getUserName());
251         assertFalse(user.getGroups().isEmpty());
252         assertTrue(user.getRoles().isEmpty());
253     }
254 
255     @Test
256     public void conf() {
257         SCIMConf conf = SCIM_CONF_SERVICE.get();
258         assertNotNull(conf);
259 
260         SCIM_CONF_SERVICE.set(CONF);
261 
262         Response response = webClient().path("Users").path("1417acbe-cbf6-4277-9372-e75e04f97000").get();
263         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
264         assertEquals(
265                 SCIMConstants.APPLICATION_SCIM_JSON,
266                 StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), ";"));
267 
268         SCIMUser user = response.readEntity(SCIMUser.class);
269         assertNotNull(user);
270         assertEquals("Rossini, Gioacchino", user.getDisplayName());
271     }
272 
273     @Test
274     public void list() throws IOException {
275         Response response = webClient().path("Groups").query("count", 1100000).get();
276         assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
277         SCIMError error = response.readEntity(SCIMError.class);
278         assertEquals(ErrorType.tooMany, error.getScimType());
279 
280         response = webClient().path("Groups").
281                 query("sortBy", "displayName").
282                 query("count", 11).
283                 get();
284         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
285         assertEquals(
286                 SCIMConstants.APPLICATION_SCIM_JSON,
287                 StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), ";"));
288 
289         ListResponse<SCIMGroup> result = response.readEntity(new GenericType<>() {
290         });
291         assertNotNull(result);
292         assertTrue(result.getTotalResults() > 0);
293         assertEquals(11, result.getItemsPerPage());
294 
295         assertFalse(result.getResources().isEmpty());
296         result.getResources().forEach(group -> {
297             assertNotNull(group.getId());
298             assertNotNull(group.getDisplayName());
299         });
300     }
301 
302     @Test
303     public void search() {
304         // invalid filter
305         Response response = webClient().path("Groups").query("filter", "invalid").get();
306         assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), response.getStatus());
307 
308         SCIMError error = response.readEntity(SCIMError.class);
309         assertEquals(Response.Status.BAD_REQUEST.getStatusCode(), error.getStatus());
310         assertEquals(ErrorType.invalidFilter, error.getScimType());
311 
312         // eq
313         response = webClient().path("Groups").query("filter", "displayName eq \"additional\"").get();
314         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
315         assertEquals(
316                 SCIMConstants.APPLICATION_SCIM_JSON,
317                 StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), ";"));
318 
319         ListResponse<SCIMGroup> groups = response.readEntity(new GenericType<>() {
320         });
321         assertNotNull(groups);
322         assertEquals(1, groups.getTotalResults());
323 
324         SCIMGroup additional = groups.getResources().get(0);
325         assertEquals("additional", additional.getDisplayName());
326 
327         // eq via POST
328         SCIMSearchRequest request = new SCIMSearchRequest("displayName eq \"additional\"", null, null, null, null);
329         response = webClient().path("Groups").path("/.search").post(request);
330         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
331         assertEquals(
332                 SCIMConstants.APPLICATION_SCIM_JSON,
333                 StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), ";"));
334 
335         groups = response.readEntity(new GenericType<>() {
336         });
337         assertNotNull(groups);
338         assertEquals(1, groups.getTotalResults());
339 
340         additional = groups.getResources().get(0);
341         assertEquals("additional", additional.getDisplayName());
342 
343         // gt
344         UserTO newUser = USER_SERVICE.create(UserITCase.getUniqueSample("scimsearch@syncope.apache.org")).
345                 readEntity(new GenericType<ProvisioningResult<UserTO>>() {
346                 }).getEntity();
347 
348         OffsetDateTime value = newUser.getCreationDate().minusSeconds(1).truncatedTo(ChronoUnit.SECONDS);
349         response = webClient().path("Users").query("filter", "meta.created gt \""
350                 + DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(value) + '"').get();
351         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
352         assertEquals(
353                 SCIMConstants.APPLICATION_SCIM_JSON,
354                 StringUtils.substringBefore(response.getHeaderString(HttpHeaders.CONTENT_TYPE), ";"));
355 
356         ListResponse<SCIMUser> users = response.readEntity(new GenericType<>() {
357         });
358         assertNotNull(users);
359         assertEquals(1, users.getTotalResults());
360 
361         SCIMUser newSCIMUser = users.getResources().get(0);
362         assertEquals(newUser.getUsername(), newSCIMUser.getUserName());
363     }
364 
365     @Test
366     public void createUser() throws JsonProcessingException {
367         SCIM_CONF_SERVICE.set(CONF);
368 
369         SCIMUser user = getSampleUser(UUID.randomUUID().toString());
370         user.getRoles().add(new Value("User reviewer"));
371         user.getGroups().add(new Group("37d15e4c-cdc1-460b-a591-8505c8133806", null, null, null));
372 
373         Response response = webClient().path("Users").post(user);
374         assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
375 
376         user = response.readEntity(SCIMUser.class);
377         assertNotNull(user.getId());
378         assertTrue(response.getLocation().toASCIIString().endsWith(user.getId()));
379 
380         UserTO userTO = USER_SERVICE.read(user.getId());
381         assertEquals(user.getUserName(), userTO.getUsername());
382         assertTrue(user.isActive());
383         assertEquals(user.getDisplayName(), userTO.getDerAttr("cn").get().getValues().get(0));
384         assertEquals(user.getName().getGivenName(), userTO.getPlainAttr("firstname").get().getValues().get(0));
385         assertEquals(user.getName().getFamilyName(), userTO.getPlainAttr("surname").get().getValues().get(0));
386         assertEquals(user.getName().getFormatted(), userTO.getPlainAttr("fullname").get().getValues().get(0));
387         assertEquals(user.getEmails().get(0).getValue(), userTO.getPlainAttr("userId").get().getValues().get(0));
388         assertEquals(user.getEmails().get(1).getValue(), userTO.getPlainAttr("email").get().getValues().get(0));
389         assertEquals(user.getRoles().get(0).getValue(), userTO.getRoles().get(0));
390         assertEquals(user.getGroups().get(0).getValue(), userTO.getMemberships().get(0).getGroupKey());
391     }
392 
393     @Test
394     public void updateUser() {
395         SCIM_CONF_SERVICE.set(CONF);
396 
397         SCIMUser user = getSampleUser(UUID.randomUUID().toString());
398 
399         Response response = webClient().path("Users").post(user);
400         assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
401 
402         user = response.readEntity(SCIMUser.class);
403         assertNotNull(user.getId());
404         assertNull(user.getNickName());
405         assertTrue(user.isActive());
406 
407         // 1. update no path, add value and suspend
408         String body =
409                 "{"
410                 + "  \"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
411                 + "  \"Operations\": ["
412                 + "    {"
413                 + "      \"op\": \"add\","
414                 + "      \"value\": {"
415                 + "        \"nickName\": \"" + user.getUserName() + "\","
416                 + "        \"active\": false"
417                 + "      }"
418                 + "    }"
419                 + "  ]"
420                 + "}";
421         response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
422         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
423 
424         user = response.readEntity(SCIMUser.class);
425         assertEquals(user.getUserName(), user.getNickName());
426         assertFalse(user.isActive());
427 
428         // 2. update with path, reactivate
429         body =
430                 "{"
431                 + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
432                 + "\"Operations\":[{"
433                 + "\"op\":\"Replace\","
434                 + "\"path\":\"active\","
435                 + "\"value\":true"
436                 + "}]"
437                 + "}";
438         response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
439         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
440 
441         user = response.readEntity(SCIMUser.class);
442         assertTrue(user.isActive());
443 
444         // 3. update with path, replace simple value
445         assertNotEquals("newSurname", user.getName().getFamilyName());
446         body =
447                 "{"
448                 + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
449                 + "\"Operations\":["
450                 + "{"
451                 + "\"op\":\"Replace\","
452                 + "\"path\":\"name.familyName\","
453                 + "\"value\":\"newSurname\""
454                 + "},"
455                 + "{"
456                 + "\"op\":\"remove\","
457                 + "\"path\":\"nickName\""
458                 + "}"
459                 + "]"
460                 + "}";
461         response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
462         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
463 
464         user = response.readEntity(SCIMUser.class);
465         assertEquals("newSurname", user.getName().getFamilyName());
466         assertNull(user.getNickName());
467 
468         // 4. update with path, replace complex value
469         String newMail = UUID.randomUUID().toString() + "@syncope.apache.org";
470         assertNotEquals(
471                 newMail,
472                 user.getEmails().stream().filter(v -> "work".equals(v.getType())).findFirst().get().getValue());
473         body =
474                 "{"
475                 + "     \"schemas\": [\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
476                 + "     \"Operations\": [{"
477                 + "       \"op\":\"replace\","
478                 + "       \"path\":\"emails[type eq \\\"work\\\"]\","
479                 + "       \"value\":"
480                 + "       {"
481                 + "         \"type\": \"work\","
482                 + "         \"value\": \"" + newMail + "\","
483                 + "         \"primary\": true"
484                 + "       }"
485                 + "     }]"
486                 + "   }";
487         response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
488         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
489 
490         user = response.readEntity(SCIMUser.class);
491         assertEquals(
492                 newMail,
493                 user.getEmails().stream().filter(v -> "work".equals(v.getType())).findFirst().get().getValue());
494 
495         // 5. update with path, filter and sub
496         newMail = "verycomplex" + UUID.randomUUID().toString() + "@syncope.apache.org";
497         body =
498                 "{"
499                 + "     \"schemas\": [\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
500                 + "     \"Operations\": [{"
501                 + "       \"op\":\"replace\","
502                 + "       \"path\":\"emails[type eq \\\"work\\\"].value\","
503                 + "       \"value\":\"" + newMail + "\""
504                 + "     }]"
505                 + "   }";
506         response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
507         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
508 
509         user = response.readEntity(SCIMUser.class);
510         assertEquals(
511                 newMail,
512                 user.getEmails().stream().filter(v -> "work".equals(v.getType())).findFirst().get().getValue());
513 
514         // 6. remove with path and filter
515         body =
516                 "{"
517                 + "     \"schemas\": [\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
518                 + "     \"Operations\": [{"
519                 + "       \"op\":\"remove\","
520                 + "       \"path\":\"emails[type eq \\\"home\\\"]\""
521                 + "     }]"
522                 + "   }";
523         response = webClient().path("Users").path(user.getId()).invoke(HttpMethod.PATCH, body);
524         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
525 
526         user = response.readEntity(SCIMUser.class);
527         assertTrue(user.getEmails().stream().noneMatch(v -> "home".equals(v.getType())));
528     }
529 
530     @Test
531     public void replaceUser() {
532         SCIM_CONF_SERVICE.set(CONF);
533 
534         SCIMUser user = getSampleUser(UUID.randomUUID().toString());
535 
536         Response response = webClient().path("Users").post(user);
537         assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
538 
539         user = response.readEntity(SCIMUser.class);
540         assertNotNull(user.getId());
541 
542         user.getName().setFormatted("new" + user.getUserName());
543 
544         response = webClient().path("Users").path(user.getId()).put(user);
545         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
546 
547         user = response.readEntity(SCIMUser.class);
548         assertTrue(user.getName().getFormatted().startsWith("new"));
549     }
550 
551     @Test
552     public void deleteUser() {
553         SCIM_CONF_SERVICE.set(CONF);
554 
555         SCIMUser user = getSampleUser(UUID.randomUUID().toString());
556 
557         Response response = webClient().path("Users").post(user);
558         assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
559 
560         user = response.readEntity(SCIMUser.class);
561         assertNotNull(user.getId());
562 
563         response = webClient().path("Users").path(user.getId()).get();
564         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
565 
566         response = webClient().path("Users").path(user.getId()).delete();
567         assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
568 
569         response = webClient().path("Users").path(user.getId()).get();
570         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
571     }
572 
573     @Test
574     public void createGroup() {
575         String displayName = UUID.randomUUID().toString();
576 
577         SCIMGroup group = new SCIMGroup(null, null, displayName);
578         group.getMembers().add(new Member("1417acbe-cbf6-4277-9372-e75e04f97000", null, null));
579         assertNull(group.getId());
580         assertEquals(displayName, group.getDisplayName());
581 
582         Response response = webClient().path("Groups").post(group);
583         assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
584 
585         group = response.readEntity(SCIMGroup.class);
586         assertNotNull(group.getId());
587         assertTrue(response.getLocation().toASCIIString().endsWith(group.getId()));
588         assertEquals(1, group.getMembers().size());
589         assertEquals("1417acbe-cbf6-4277-9372-e75e04f97000", group.getMembers().get(0).getValue());
590 
591         response = webClient().path("Users").path("1417acbe-cbf6-4277-9372-e75e04f97000").get();
592         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
593 
594         SCIMUser user = response.readEntity(SCIMUser.class);
595         assertEquals("1417acbe-cbf6-4277-9372-e75e04f97000", user.getId());
596 
597         response = webClient().path("Groups").post(group);
598         assertEquals(Response.Status.CONFLICT.getStatusCode(), response.getStatus());
599 
600         SCIMError error = response.readEntity(SCIMError.class);
601         assertEquals(Response.Status.CONFLICT.getStatusCode(), error.getStatus());
602         assertEquals(ErrorType.uniqueness, error.getScimType());
603     }
604 
605     @Test
606     public void updateGroup() {
607         SCIM_CONF_SERVICE.set(CONF);
608 
609         SCIMGroup group = new SCIMGroup(null, null, UUID.randomUUID().toString());
610         group.getMembers().add(new Member("74cd8ece-715a-44a4-a736-e17b46c4e7e6", null, null));
611         group.getMembers().add(new Member("1417acbe-cbf6-4277-9372-e75e04f97000", null, null));
612         Response response = webClient().path("Groups").post(group);
613         assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
614 
615         group = response.readEntity(SCIMGroup.class);
616         assertNotNull(group.getId());
617         assertNull(group.getExternalId());
618         assertEquals(2, group.getMembers().size());
619 
620         // 1. update with path, add value
621         String body =
622                 "{"
623                 + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
624                 + "\"Operations\":[{"
625                 + "\"op\":\"Add\","
626                 + "\"path\":\"externalId\","
627                 + "\"value\":\"" + group.getId() + "\""
628                 + "}]"
629                 + "}";
630         response = webClient().path("Groups").path(group.getId()).invoke(HttpMethod.PATCH, body);
631         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
632 
633         group = response.readEntity(SCIMGroup.class);
634         assertEquals(group.getId(), group.getExternalId());
635 
636         // 2. add member, remove member, remove attribute
637         body =
638                 "{"
639                 + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
640                 + "\"Operations\":["
641                 + "{"
642                 + "\"op\":\"Add\","
643                 + "\"path\":\"members\","
644                 + "\"value\":[{"
645                 + "\"value\":\"b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee\"}]"
646                 + "},"
647                 + "{"
648                 + "\"op\":\"remove\","
649                 + "\"path\":\"members[value eq \\\"74cd8ece-715a-44a4-a736-e17b46c4e7e6\\\"]\""
650                 + "},"
651                 + "{"
652                 + "\"op\":\"remove\","
653                 + "\"path\":\"externalId\""
654                 + "}"
655                 + "]"
656                 + "}";
657         response = webClient().path("Groups").path(group.getId()).invoke(HttpMethod.PATCH, body);
658         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
659 
660         group = response.readEntity(SCIMGroup.class);
661         assertEquals(2, group.getMembers().size());
662         assertTrue(group.getMembers().stream().
663                 anyMatch(m -> "b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee".equals(m.getValue())));
664         assertTrue(group.getMembers().stream().
665                 anyMatch(m -> "1417acbe-cbf6-4277-9372-e75e04f97000".equals(m.getValue())));
666         assertNull(group.getExternalId());
667 
668         // 3. remove all members
669         body =
670                 "{"
671                 + "\"schemas\":[\"urn:ietf:params:scim:api:messages:2.0:PatchOp\"],"
672                 + "\"Operations\":["
673                 + "{"
674                 + "\"op\":\"remove\","
675                 + "\"path\":\"members\""
676                 + "}"
677                 + "]"
678                 + "}";
679         response = webClient().path("Groups").path(group.getId()).invoke(HttpMethod.PATCH, body);
680         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
681 
682         group = response.readEntity(SCIMGroup.class);
683         assertTrue(group.getMembers().isEmpty());
684     }
685 
686     @Test
687     public void replaceGroup() {
688         SCIMGroup group = new SCIMGroup(null, null, UUID.randomUUID().toString());
689         group.getMembers().add(new Member("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee", null, null));
690         Response response = webClient().path("Groups").post(group);
691         assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
692 
693         group = response.readEntity(SCIMGroup.class);
694         assertNotNull(group.getId());
695         assertEquals(1, group.getMembers().size());
696         assertEquals("b3cbc78d-32e6-4bd4-92e0-bbe07566a2ee", group.getMembers().get(0).getValue());
697 
698         group.setDisplayName("other" + group.getId());
699         group.getMembers().add(new Member("c9b2dec2-00a7-4855-97c0-d854842b4b24", null, null));
700 
701         response = webClient().path("Groups").path(group.getId()).put(group);
702         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
703 
704         group = response.readEntity(SCIMGroup.class);
705         assertTrue(group.getDisplayName().startsWith("other"));
706         assertEquals(2, group.getMembers().size());
707 
708         group.getMembers().clear();
709         group.getMembers().add(new Member("c9b2dec2-00a7-4855-97c0-d854842b4b24", null, null));
710 
711         response = webClient().path("Groups").path(group.getId()).put(group);
712         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
713 
714         group = response.readEntity(SCIMGroup.class);
715         assertEquals(1, group.getMembers().size());
716         assertEquals("c9b2dec2-00a7-4855-97c0-d854842b4b24", group.getMembers().get(0).getValue());
717     }
718 
719     @Test
720     public void deleteGroup() {
721         SCIMGroup group = new SCIMGroup(null, null, UUID.randomUUID().toString());
722         Response response = webClient().path("Groups").post(group);
723         assertEquals(Response.Status.CREATED.getStatusCode(), response.getStatus());
724 
725         group = response.readEntity(SCIMGroup.class);
726         assertNotNull(group.getId());
727 
728         response = webClient().path("Groups").path(group.getId()).get();
729         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
730 
731         response = webClient().path("Groups").path(group.getId()).delete();
732         assertEquals(Response.Status.NO_CONTENT.getStatusCode(), response.getStatus());
733 
734         response = webClient().path("Groups").path(group.getId()).get();
735         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
736     }
737 }