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.awaitility.Awaitility.await;
22  import static org.junit.jupiter.api.Assertions.assertEquals;
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.assertTrue;
26  
27  import com.fasterxml.jackson.core.JsonProcessingException;
28  import com.fasterxml.jackson.core.type.TypeReference;
29  import java.io.ByteArrayInputStream;
30  import java.io.IOException;
31  import java.net.URI;
32  import java.util.ArrayList;
33  import java.util.HashMap;
34  import java.util.List;
35  import java.util.UUID;
36  import java.util.concurrent.TimeUnit;
37  import java.util.concurrent.atomic.AtomicReference;
38  import javax.ws.rs.HttpMethod;
39  import javax.ws.rs.core.HttpHeaders;
40  import javax.ws.rs.core.MediaType;
41  import javax.ws.rs.core.Response;
42  import org.apache.cxf.jaxrs.client.Client;
43  import org.apache.cxf.jaxrs.client.WebClient;
44  import org.apache.syncope.client.lib.batch.BatchRequest;
45  import org.apache.syncope.client.lib.batch.BatchResponse;
46  import org.apache.syncope.common.lib.request.GroupCR;
47  import org.apache.syncope.common.lib.request.StringReplacePatchItem;
48  import org.apache.syncope.common.lib.request.UserCR;
49  import org.apache.syncope.common.lib.request.UserUR;
50  import org.apache.syncope.common.lib.to.GroupTO;
51  import org.apache.syncope.common.lib.to.ProvisioningResult;
52  import org.apache.syncope.common.lib.to.UserTO;
53  import org.apache.syncope.common.rest.api.Preference;
54  import org.apache.syncope.common.rest.api.RESTHeaders;
55  import org.apache.syncope.common.rest.api.batch.BatchPayloadGenerator;
56  import org.apache.syncope.common.rest.api.batch.BatchPayloadParser;
57  import org.apache.syncope.common.rest.api.batch.BatchRequestItem;
58  import org.apache.syncope.common.rest.api.batch.BatchResponseItem;
59  import org.apache.syncope.common.rest.api.service.GroupService;
60  import org.apache.syncope.common.rest.api.service.ResourceService;
61  import org.apache.syncope.common.rest.api.service.UserService;
62  import org.apache.syncope.fit.AbstractITCase;
63  import org.junit.jupiter.api.Test;
64  
65  public class BatchITCase extends AbstractITCase {
66  
67      private static String requestBody(final String boundary) throws JsonProcessingException {
68          List<BatchRequestItem> reqItems = new ArrayList<>();
69  
70          // 1. create user as YAML
71          UserCR userCR = UserITCase.getUniqueSample("batch@syncope.apache.org");
72          assertNotEquals("/odd", userCR.getRealm());
73          String createUserPayload = YAML_MAPPER.writeValueAsString(userCR);
74  
75          BatchRequestItem createUser = new BatchRequestItem();
76          createUser.setMethod(HttpMethod.POST);
77          createUser.setRequestURI("/users");
78          createUser.setHeaders(new HashMap<>());
79          createUser.getHeaders().put(HttpHeaders.ACCEPT, List.of(RESTHeaders.APPLICATION_YAML));
80          createUser.getHeaders().put(HttpHeaders.CONTENT_TYPE, List.of(RESTHeaders.APPLICATION_YAML));
81          createUser.getHeaders().put(HttpHeaders.CONTENT_LENGTH, List.of(createUserPayload.length()));
82          createUser.setContent(createUserPayload);
83          reqItems.add(createUser);
84  
85          // 2. create group as XML
86          GroupCR groupCR = GroupITCase.getBasicSample("batch");
87          String createGroupPayload = XML_MAPPER.writeValueAsString(groupCR);
88  
89          BatchRequestItem createGroup = new BatchRequestItem();
90          createGroup.setMethod(HttpMethod.POST);
91          createGroup.setRequestURI("/groups");
92          createGroup.setHeaders(new HashMap<>());
93          createGroup.getHeaders().put(HttpHeaders.ACCEPT, List.of(MediaType.APPLICATION_XML));
94          createGroup.getHeaders().put(HttpHeaders.CONTENT_TYPE, List.of(MediaType.APPLICATION_XML));
95          createGroup.getHeaders().put(HttpHeaders.CONTENT_LENGTH, List.of(createGroupPayload.length()));
96          createGroup.setContent(createGroupPayload);
97          reqItems.add(createGroup);
98  
99          // 3. update the user above as JSON, request for no user data being returned
100         UserUR userUR = new UserUR();
101         userUR.setKey(userCR.getUsername());
102         userUR.setRealm(new StringReplacePatchItem.Builder().value("/odd").build());
103         String updateUserPayload = JSON_MAPPER.writeValueAsString(userUR);
104 
105         BatchRequestItem updateUser = new BatchRequestItem();
106         updateUser.setMethod(HttpMethod.PATCH);
107         updateUser.setRequestURI("/users/" + userCR.getUsername());
108         updateUser.setHeaders(new HashMap<>());
109         updateUser.getHeaders().put(RESTHeaders.PREFER, List.of(Preference.RETURN_NO_CONTENT.toString()));
110         updateUser.getHeaders().put(HttpHeaders.ACCEPT, List.of(MediaType.APPLICATION_JSON));
111         updateUser.getHeaders().put(HttpHeaders.CONTENT_TYPE, List.of(MediaType.APPLICATION_JSON));
112         updateUser.getHeaders().put(HttpHeaders.CONTENT_LENGTH, List.of(updateUserPayload.length()));
113         updateUser.setContent(updateUserPayload);
114         reqItems.add(updateUser);
115 
116         // 4. attempt to invoke an unexisting endpoint
117         BatchRequestItem endpointNotFound = new BatchRequestItem();
118         endpointNotFound.setMethod(HttpMethod.PATCH);
119         endpointNotFound.setRequestURI("/missing");
120         reqItems.add(endpointNotFound);
121 
122         // 5. attempt to delete an unexisting group
123         BatchRequestItem groupNotFound = new BatchRequestItem();
124         groupNotFound.setMethod(HttpMethod.DELETE);
125         groupNotFound.setRequestURI("/groups/" + UUID.randomUUID());
126         reqItems.add(groupNotFound);
127 
128         // 6, delete the group created above, expect deleted group as JSON
129         BatchRequestItem deleteGroup = new BatchRequestItem();
130         deleteGroup.setMethod(HttpMethod.DELETE);
131         deleteGroup.setRequestURI("/groups/" + groupCR.getName());
132         reqItems.add(deleteGroup);
133 
134         String body = BatchPayloadGenerator.generate(reqItems, boundary);
135         LOG.debug("Batch request body:\n{}", body);
136 
137         return body;
138     }
139 
140     private static void check(final List<BatchResponseItem> resItems) throws IOException {
141         assertEquals(6, resItems.size());
142 
143         assertEquals(Response.Status.CREATED.getStatusCode(), resItems.get(0).getStatus());
144         assertNotNull(resItems.get(0).getHeaders().get(HttpHeaders.LOCATION));
145         assertNotNull(resItems.get(0).getHeaders().get(HttpHeaders.ETAG));
146         assertNotNull(resItems.get(0).getHeaders().get(RESTHeaders.DOMAIN));
147         assertNotNull(resItems.get(0).getHeaders().get(RESTHeaders.RESOURCE_KEY));
148         assertEquals(RESTHeaders.APPLICATION_YAML, resItems.get(0).getHeaders().get(HttpHeaders.CONTENT_TYPE).get(0));
149         ProvisioningResult<UserTO> user = YAML_MAPPER.readValue(
150                 resItems.get(0).getContent(), new TypeReference<>() {
151         });
152         assertNotNull(user.getEntity().getKey());
153 
154         assertEquals(Response.Status.CREATED.getStatusCode(), resItems.get(1).getStatus());
155         assertNotNull(resItems.get(1).getHeaders().get(HttpHeaders.LOCATION));
156         assertNotNull(resItems.get(1).getHeaders().get(HttpHeaders.ETAG));
157         assertNotNull(resItems.get(1).getHeaders().get(RESTHeaders.DOMAIN));
158         assertNotNull(resItems.get(1).getHeaders().get(RESTHeaders.RESOURCE_KEY));
159         assertEquals(MediaType.APPLICATION_XML, resItems.get(1).getHeaders().get(HttpHeaders.CONTENT_TYPE).get(0));
160 
161         ProvisioningResult<GroupTO> group = XML_MAPPER.readValue(
162                 resItems.get(1).getContent(), new TypeReference<>() {
163         });
164         assertNotNull(group.getEntity().getKey());
165 
166         assertEquals(Response.Status.NO_CONTENT.getStatusCode(), resItems.get(2).getStatus());
167         assertNotNull(resItems.get(2).getHeaders().get(RESTHeaders.DOMAIN));
168         assertEquals(
169                 Preference.RETURN_NO_CONTENT.toString(),
170                 resItems.get(2).getHeaders().get(RESTHeaders.PREFERENCE_APPLIED).get(0));
171 
172         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resItems.get(3).getStatus());
173 
174         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resItems.get(4).getStatus());
175         assertNotNull(resItems.get(4).getHeaders().get(RESTHeaders.DOMAIN));
176         assertNotNull(resItems.get(4).getHeaders().get(RESTHeaders.ERROR_CODE));
177         assertNotNull(resItems.get(4).getHeaders().get(RESTHeaders.ERROR_INFO));
178         assertEquals(MediaType.APPLICATION_JSON, resItems.get(4).getHeaders().get(HttpHeaders.CONTENT_TYPE).get(0));
179 
180         assertEquals(Response.Status.OK.getStatusCode(), resItems.get(5).getStatus());
181         assertNotNull(resItems.get(5).getHeaders().get(RESTHeaders.DOMAIN));
182         assertEquals(MediaType.APPLICATION_JSON, resItems.get(5).getHeaders().get(HttpHeaders.CONTENT_TYPE).get(0));
183         group = JSON_MAPPER.readValue(
184                 resItems.get(5).getContent(), new TypeReference<>() {
185         });
186         assertNotNull(group);
187     }
188 
189     @Test
190     public void webClientSync() throws IOException {
191         String boundary = "--batch_" + UUID.randomUUID().toString();
192 
193         Response response = WebClient.create(ADDRESS).path("batch").
194                 header(HttpHeaders.AUTHORIZATION, "Bearer " + ADMIN_CLIENT.getJWT()).
195                 type(RESTHeaders.multipartMixedWith(boundary.substring(2))).
196                 post(requestBody(boundary));
197         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
198         assertTrue(response.getMediaType().toString().
199                 startsWith(RESTHeaders.multipartMixedWith(boundary.substring(2))));
200 
201         String body = response.readEntity(String.class);
202         LOG.debug("Batch response body:\n{}", body);
203 
204         check(BatchPayloadParser.parse(
205                 new ByteArrayInputStream(body.getBytes()),
206                 response.getMediaType(),
207                 new BatchResponseItem()));
208     }
209 
210     @Test
211     public void webClientAsync() throws IOException {
212         String boundary = "--batch_" + UUID.randomUUID().toString();
213 
214         // request async processing
215         Response response = WebClient.create(ADDRESS).path("batch").
216                 header(HttpHeaders.AUTHORIZATION, "Bearer " + ADMIN_CLIENT.getJWT()).
217                 header(RESTHeaders.PREFER, Preference.RESPOND_ASYNC).
218                 type(RESTHeaders.multipartMixedWith(boundary.substring(2))).
219                 post(requestBody(boundary));
220         assertEquals(Response.Status.ACCEPTED.getStatusCode(), response.getStatus());
221         assertTrue(response.getMediaType().toString().
222                 startsWith(RESTHeaders.multipartMixedWith(boundary.substring(2))));
223         assertEquals(Preference.RESPOND_ASYNC.toString(), response.getHeaderString(RESTHeaders.PREFERENCE_APPLIED));
224         URI monitor = response.getLocation();
225         assertNotNull(monitor);
226 
227         WebClient client = WebClient.create(monitor).
228                 header(HttpHeaders.AUTHORIZATION, "Bearer " + ADMIN_CLIENT.getJWT()).
229                 type(RESTHeaders.multipartMixedWith(boundary.substring(2)));
230 
231         AtomicReference<Response> holder = new AtomicReference<>();
232         await().atMost(MAX_WAIT_SECONDS, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).until(() -> {
233             try {
234                 holder.set(client.get());
235                 return holder.get().getStatus() != Response.Status.ACCEPTED.getStatusCode();
236             } catch (Exception e) {
237                 return false;
238             }
239         });
240         response = holder.get();
241         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
242         assertTrue(response.getMediaType().toString().
243                 startsWith(RESTHeaders.multipartMixedWith(boundary.substring(2))));
244 
245         String body = response.readEntity(String.class);
246         LOG.debug("Batch response body:\n{}", body);
247 
248         check(BatchPayloadParser.parse(
249                 new ByteArrayInputStream(body.getBytes()),
250                 response.getMediaType(),
251                 new BatchResponseItem()));
252 
253         // check results again: removed since they were returned above
254         response = WebClient.create(monitor).
255                 header(HttpHeaders.AUTHORIZATION, "Bearer " + ADMIN_CLIENT.getJWT()).
256                 type(RESTHeaders.multipartMixedWith(boundary.substring(2))).get();
257         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
258     }
259 
260     private static BatchRequest batchRequest() {
261         BatchRequest batchRequest = ADMIN_CLIENT.batch();
262 
263         // 1. create user as YAML
264         UserService batchUserService = batchRequest.getService(UserService.class);
265         Client client = WebClient.client(batchUserService).reset();
266         client.type(RESTHeaders.APPLICATION_YAML).accept(RESTHeaders.APPLICATION_YAML);
267         UserCR userCR = UserITCase.getUniqueSample("batch@syncope.apache.org");
268         assertNotEquals("/odd", userCR.getRealm());
269         batchUserService.create(userCR);
270 
271         // 2. create group as XML
272         GroupService batchGroupService = batchRequest.getService(GroupService.class);
273         client = WebClient.client(batchGroupService).reset();
274         client.type(MediaType.APPLICATION_XML).accept(MediaType.APPLICATION_XML);
275         GroupCR groupCR = GroupITCase.getBasicSample("batch");
276         batchGroupService.create(groupCR);
277 
278         // 3. update the user above as JSON, request for no user data being returned
279         client = WebClient.client(batchUserService).reset();
280         client.type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
281         client.header(RESTHeaders.PREFER, Preference.RETURN_NO_CONTENT.toString());
282         UserUR userUR = new UserUR();
283         userUR.setKey(userCR.getUsername());
284         userUR.setRealm(new StringReplacePatchItem.Builder().value("/odd").build());
285         batchUserService.update(userUR);
286 
287         // 4. generate not found
288         batchRequest.getService(ResourceService.class).read(UUID.randomUUID().toString());
289 
290         // 5. attempt to delete an unexisting group
291         client = WebClient.client(batchGroupService).reset();
292         client.type(MediaType.APPLICATION_JSON).accept(MediaType.APPLICATION_JSON);
293         batchGroupService.delete(UUID.randomUUID().toString());
294 
295         // 6, delete the group created above, expect deleted group as JSON
296         batchGroupService.delete(groupCR.getName());
297 
298         return batchRequest;
299     }
300 
301     @Test
302     public void syncopeClientSync() throws IOException {
303         BatchResponse batchResponse = batchRequest().commit();
304 
305         Response response = batchResponse.getResponse();
306         assertEquals(Response.Status.OK.getStatusCode(), response.getStatus());
307         assertTrue(response.getMediaType().toString().startsWith(RESTHeaders.MULTIPART_MIXED));
308 
309         check(batchResponse.getItems());
310     }
311 
312     @Test
313     public void syncopeClientAsync() throws IOException {
314         // request async processing
315         BatchResponse batchResponse = batchRequest().commit(true);
316 
317         Response response = batchResponse.getResponse();
318         assertEquals(Response.Status.ACCEPTED.getStatusCode(), response.getStatus());
319         assertTrue(response.getMediaType().toString().startsWith(RESTHeaders.MULTIPART_MIXED));
320 
321         await().atMost(10, TimeUnit.SECONDS).pollInterval(1, TimeUnit.SECONDS).
322                 until(() -> batchResponse.poll().getStatus() == Response.Status.OK.getStatusCode());
323 
324         check(batchResponse.getItems());
325 
326         // check results again: removed since they were returned above
327         response = batchResponse.poll();
328         assertEquals(Response.Status.NOT_FOUND.getStatusCode(), response.getStatus());
329     }
330 }