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}