001/* 002 * (C) Copyright 2013-2019 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * dmetzler 018 */ 019package org.nuxeo.ecm.restapi.test; 020 021import static org.junit.Assert.assertEquals; 022import static org.junit.Assert.assertTrue; 023 024import java.io.IOException; 025import java.io.InputStream; 026import java.util.ArrayList; 027import java.util.Iterator; 028import java.util.List; 029import java.util.Map; 030import java.util.Map.Entry; 031 032import javax.inject.Inject; 033import javax.ws.rs.core.MediaType; 034import javax.ws.rs.core.MultivaluedMap; 035import javax.ws.rs.core.Response; 036 037import org.junit.After; 038import org.junit.Before; 039import org.nuxeo.ecm.core.api.CoreSession; 040import org.nuxeo.ecm.core.api.DocumentModel; 041import org.nuxeo.jaxrs.test.CloseableClientResponse; 042import org.nuxeo.jaxrs.test.JerseyClientHelper; 043import org.nuxeo.runtime.test.runner.ServletContainerFeature; 044import org.nuxeo.runtime.transaction.TransactionHelper; 045 046import com.fasterxml.jackson.core.JsonProcessingException; 047import com.fasterxml.jackson.databind.JsonNode; 048import com.fasterxml.jackson.databind.ObjectMapper; 049import com.sun.jersey.api.client.Client; 050import com.sun.jersey.api.client.ClientResponse; 051import com.sun.jersey.api.client.WebResource; 052import com.sun.jersey.api.client.WebResource.Builder; 053import com.sun.jersey.core.util.MultivaluedMapImpl; 054import com.sun.jersey.multipart.MultiPart; 055import com.sun.jersey.multipart.MultiPartMediaTypes; 056 057/** 058 * @since 5.7.2 059 */ 060public class BaseTest { 061 062 @Inject 063 protected ServletContainerFeature servletContainerFeature; 064 065 protected enum RequestType { 066 GET, POST, DELETE, PUT, POSTREQUEST 067 } 068 069 protected ObjectMapper mapper; 070 071 protected Client client; 072 073 protected WebResource service; 074 075 @Before 076 public void doBefore() throws Exception { 077 service = getServiceFor("Administrator", "Administrator"); 078 mapper = new ObjectMapper(); 079 } 080 081 @After 082 public void doAfter() throws Exception { 083 client.destroy(); 084 } 085 086 protected String getBaseURL() { 087 int port = servletContainerFeature.getPort(); 088 return "http://localhost:" + port; 089 } 090 091 protected String getRestApiUrl() { 092 return getBaseURL() + "/api/v1/"; 093 } 094 095 /** 096 * Returns a {@link WebResource} to perform REST API calls with the given credentials. 097 * <p> 098 * Since 9.3, uses the Apache HTTP client, more reliable and much more configurable than the one from the JDK. 099 * 100 * @since 5.7.3 101 */ 102 protected WebResource getServiceFor(String username, String password) { 103 return getServiceFor(getRestApiUrl(), username, password); 104 } 105 106 /** 107 * Returns a {@link WebResource} to perform calls on the given resource with the given credentials. 108 * <p> 109 * Uses the Apache HTTP client, more reliable and much more configurable than the one from the JDK. 110 * 111 * @since 9.3 112 */ 113 protected WebResource getServiceFor(String resource, String username, String password) { 114 if (client != null) { 115 client.destroy(); 116 } 117 client = JerseyClientHelper.clientBuilder().setCredentials(username, password).build(); 118 return client.resource(resource); 119 } 120 121 @Inject 122 public CoreSession session; 123 124 protected CloseableClientResponse getResponse(RequestType requestType, String path) { 125 return getResponse(requestType, path, null, null, null, null); 126 } 127 128 protected CloseableClientResponse getResponse(RequestType requestType, String path, Map<String, String> headers) { 129 return getResponse(requestType, path, null, null, null, headers); 130 } 131 132 protected CloseableClientResponse getResponse(RequestType requestType, String path, MultiPart mp) { 133 return getResponse(requestType, path, null, null, mp, null); 134 } 135 136 protected CloseableClientResponse getResponse(RequestType requestType, String path, MultiPart mp, 137 Map<String, String> headers) { 138 return getResponse(requestType, path, null, null, mp, headers); 139 } 140 141 protected CloseableClientResponse getResponse(RequestType requestType, String path, 142 MultivaluedMap<String, String> queryParams) { 143 return getResponse(requestType, path, null, queryParams, null, null); 144 } 145 146 protected CloseableClientResponse getResponse(RequestType requestType, String path, String data) { 147 return getResponse(requestType, path, data, null, null, null); 148 } 149 150 protected CloseableClientResponse getResponse(RequestType requestType, String path, String data, 151 Map<String, String> headers) { 152 return getResponse(requestType, path, data, null, null, headers); 153 } 154 155 protected CloseableClientResponse getResponse(RequestType requestType, String path, String data, 156 MultivaluedMap<String, String> queryParams, MultiPart mp, Map<String, String> headers, 157 String... acceptedTypes) { 158 159 WebResource wr = service.path(path); 160 161 if (queryParams != null && !queryParams.isEmpty()) { 162 wr = wr.queryParams(queryParams); 163 } 164 Builder builder; 165 166 String[] types = acceptedTypes.length > 0 ? acceptedTypes : new String[] { MediaType.APPLICATION_JSON }; 167 builder = wr.accept(types).header("X-NXDocumentProperties", "dublincore"); 168 169 // Adding some headers if needed 170 if (headers != null && !headers.isEmpty()) { 171 for (Entry<String, String> header : headers.entrySet()) { 172 builder.header(header.getKey(), header.getValue()); 173 } 174 } 175 ClientResponse response; 176 switch (requestType) { 177 case GET: 178 response = builder.get(ClientResponse.class); 179 break; 180 case POST: 181 case POSTREQUEST: 182 if (mp != null) { 183 response = builder.type(MultiPartMediaTypes.createFormData()).post(ClientResponse.class, mp); 184 } else if (data != null) { 185 setJSONContentTypeIfAbsent(builder, headers); 186 response = builder.post(ClientResponse.class, data); 187 } else { 188 response = builder.post(ClientResponse.class); 189 } 190 break; 191 case PUT: 192 if (mp != null) { 193 response = builder.type(MultiPartMediaTypes.createFormData()).put(ClientResponse.class, mp); 194 } else if (data != null) { 195 setJSONContentTypeIfAbsent(builder, headers); 196 response = builder.put(ClientResponse.class, data); 197 } else { 198 response = builder.put(ClientResponse.class); 199 } 200 break; 201 case DELETE: 202 response = builder.delete(ClientResponse.class, data); 203 break; 204 default: 205 throw new UnsupportedOperationException("Type: " + requestType + " is not handled"); 206 } 207 208 // Make the ClientResponse AutoCloseable by wrapping it in a CloseableClientResponse. 209 // This is to strongly encourage the caller to use a try-with-resources block to make sure the response is 210 // closed and avoid leaking connections. 211 return CloseableClientResponse.of(response); 212 } 213 214 /** @since 9.3 */ 215 protected void setJSONContentTypeIfAbsent(Builder builder, Map<String, String> headers) { 216 if (headers == null || !(headers.containsKey("Content-Type"))) { 217 builder.type(MediaType.APPLICATION_JSON); 218 } 219 } 220 221 protected JsonNode getResponseAsJson(RequestType responseType, String url) 222 throws IOException, JsonProcessingException { 223 return getResponseAsJson(responseType, url, null); 224 } 225 226 /** 227 * @since 5.8 228 */ 229 protected JsonNode getResponseAsJson(RequestType responseType, String url, 230 MultivaluedMap<String, String> queryParams) throws JsonProcessingException, IOException { 231 try (CloseableClientResponse response = getResponse(responseType, url, queryParams)) { 232 assertEquals(Response.Status.OK.getStatusCode(), response.getStatus()); 233 return mapper.readTree(response.getEntityInputStream()); 234 } 235 } 236 237 /** 238 * Fetch session invalidations. 239 * 240 * @since 5.9.3 241 */ 242 protected void fetchInvalidations() { 243 session.save(); 244 if (TransactionHelper.isTransactionActiveOrMarkedRollback()) { 245 TransactionHelper.commitOrRollbackTransaction(); 246 TransactionHelper.startTransaction(); 247 } 248 } 249 250 protected void assertNodeEqualsDoc(JsonNode node, DocumentModel note) throws Exception { 251 assertEquals("document", node.get("entity-type").asText()); 252 assertEquals(note.getPathAsString(), node.get("path").asText()); 253 assertEquals(note.getId(), node.get("uid").asText()); 254 assertEquals(note.getTitle(), node.get("title").asText()); 255 } 256 257 protected List<JsonNode> getLogEntries(JsonNode node) { 258 assertEquals("documents", node.get("entity-type").asText()); 259 assertTrue(node.get("entries").isArray()); 260 List<JsonNode> result = new ArrayList<>(); 261 Iterator<JsonNode> elements = node.get("entries").elements(); 262 while (elements.hasNext()) { 263 result.add(elements.next()); 264 } 265 return result; 266 } 267 268 /** 269 * @since 7.1 270 */ 271 protected String getErrorMessage(JsonNode node) { 272 assertTrue(hasErrorMessage(node)); 273 assertTrue("Exception message is not present in response", node.has("message")); 274 assertTrue("Exception message is not textual", node.get("message").isTextual()); 275 return node.get("message").asText(); 276 } 277 278 protected void assertEntityEqualsDoc(InputStream in, DocumentModel doc) throws Exception { 279 280 JsonNode node = mapper.readTree(in); 281 assertNodeEqualsDoc(node, doc); 282 283 } 284 285 /** 286 * @since 11.1 287 */ 288 protected boolean hasErrorMessage(JsonNode node) { 289 return node.get("entity-type").asText().equals("exception"); 290 } 291 292 /** 293 * Builds and returns a {@link MultivaluedMap} filled with the given simple values. 294 * 295 * @since 11.1 296 */ 297 protected MultivaluedMap<String, String> multiOf(String k1, String v1) { 298 return multiOf(Map.of(k1, v1)); 299 } 300 301 /** 302 * Builds and returns a {@link MultivaluedMap} filled with the given simple values. 303 * 304 * @since 11.1 305 */ 306 protected MultivaluedMap<String, String> multiOf(String k1, String v1, String k2, String v2) { 307 return multiOf(Map.of(k1, v1, k2, v2)); 308 } 309 310 /** 311 * Builds and returns a {@link MultivaluedMap} filled with the given simple values. 312 * 313 * @since 11.1 314 */ 315 protected MultivaluedMap<String, String> multiOf(Map<String, String> map) { 316 MultivaluedMap<String, String> queryParams = new MultivaluedMapImpl(); 317 map.forEach(queryParams::putSingle); 318 return queryParams; 319 } 320}