2 * This document is a part of the source code and related artifacts
3 * for CollectionSpace, an open source collections management system
4 * for museums and related institutions:
6 * http://www.collectionspace.org
7 * http://wiki.collectionspace.org
9 * Copyright © 2009 Regents of the University of California
11 * Licensed under the Educational Community License (ECL), Version 2.0.
12 * You may not use this file except in compliance with this License.
14 * You may obtain a copy of the ECL 2.0 License at
16 * https://source.collectionspace.org/collection-space/LICENSE.txt
18 * Unless required by applicable law or agreed to in writing, software
19 * distributed under the License is distributed on an "AS IS" BASIS,
20 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
21 * See the License for the specific language governing permissions and
22 * limitations under the License.
24 package org.collectionspace.services.client.test;
26 import java.io.IOException;
27 import java.io.UnsupportedEncodingException;
28 import java.util.ArrayList;
29 import javax.xml.bind.JAXBContext;
30 import javax.xml.bind.Marshaller;
31 import javax.ws.rs.core.MediaType;
32 import javax.ws.rs.core.MultivaluedMap;
33 import javax.ws.rs.core.Response;
35 import org.apache.commons.httpclient.Header;
36 import org.apache.commons.httpclient.HttpClient;
37 import org.apache.commons.httpclient.HttpException;
38 import org.apache.commons.httpclient.HttpMethod;
39 import org.apache.commons.httpclient.HttpStatus;
40 import org.apache.commons.httpclient.methods.EntityEnclosingMethod;
41 import org.apache.commons.httpclient.methods.GetMethod;
42 import org.apache.commons.httpclient.methods.PostMethod;
43 import org.apache.commons.httpclient.methods.PutMethod;
44 import org.apache.commons.httpclient.methods.RequestEntity;
45 import org.apache.commons.httpclient.methods.StringRequestEntity;
47 import org.collectionspace.services.client.TestServiceClient;
48 import org.collectionspace.services.client.test.ServiceRequestType;
50 import org.jboss.resteasy.client.ClientResponse;
52 import org.slf4j.Logger;
53 import org.slf4j.LoggerFactory;
56 * AbstractServiceTest, abstract base class for the client tests to be performed
57 * to test an entity or relation service.
59 * For Javadoc descriptions of this class's methods, see the ServiceTest interface.
61 public abstract class AbstractServiceTest implements ServiceTest {
63 final Logger logger = LoggerFactory.getLogger(AbstractServiceTest.class);
65 // An HTTP client, used for performing several negative (failure) tests.
67 // @TODO To be replaced with RESTeasy's ClientRequest (a higher-level API
68 // that is based on HttpClient), per Issue CSPACE-386.
69 protected HttpClient httpClient = new HttpClient();
71 // A base-level client, used (only) to obtain the base service URL.
72 private static final TestServiceClient serviceClient = new TestServiceClient();
74 // A resource identifier believed to be non-existent in actual use,
75 // used when testing service calls that reference non-existent resources.
76 protected final String NON_EXISTENT_ID = createNonExistentIdentifier();
78 // The HTTP status code expected to be returned in the response,
79 // from a request made to a service (where relevant).
80 int EXPECTED_STATUS_CODE = 0;
82 // The generic type of service request being tested (e.g. CREATE, UPDATE, DELETE).
84 // This makes it possible to check behavior specific to that type of request,
85 // such as the set of valid status codes that may be returned.
86 ServiceRequestType REQUEST_TYPE = ServiceRequestType.NON_EXISTENT;
88 // ---------------------------------------------------------------
89 // CRUD tests : CREATE tests
90 // ---------------------------------------------------------------
95 public abstract void create();
97 protected void setupCreate() {
99 // Expected status code: 201 Created
100 EXPECTED_STATUS_CODE = Response.Status.CREATED.getStatusCode();
101 // Type of service request being tested
102 REQUEST_TYPE = ServiceRequestType.CREATE;
106 public abstract void createMultiple();
108 // No setup required for createMultiple()
113 public void createNull() {
116 // No setup required for createNull()
119 public abstract void createWithMalformedXml();
121 protected void setupCreateWithMalformedXml() {
123 // Expected status code: 400 Bad Request
124 EXPECTED_STATUS_CODE = Response.Status.BAD_REQUEST.getStatusCode();
125 REQUEST_TYPE = ServiceRequestType.CREATE;
129 public abstract void createWithWrongXmlSchema();
131 protected void setupCreateWithWrongXmlSchema() {
133 // Expected status code: 400 Bad Request
134 EXPECTED_STATUS_CODE = Response.Status.BAD_REQUEST.getStatusCode();
135 REQUEST_TYPE = ServiceRequestType.CREATE;
139 // ---------------------------------------------------------------
140 // CRUD tests : READ tests
141 // ---------------------------------------------------------------
146 public abstract void read();
148 protected void setupRead() {
150 // Expected status code: 200 OK
151 EXPECTED_STATUS_CODE = Response.Status.OK.getStatusCode();
152 REQUEST_TYPE = ServiceRequestType.READ;
158 public abstract void readNonExistent();
160 protected void setupReadNonExistent() {
162 // Expected status code: 404 Not Found
163 EXPECTED_STATUS_CODE = Response.Status.NOT_FOUND.getStatusCode();
164 REQUEST_TYPE = ServiceRequestType.READ;
168 // ---------------------------------------------------------------
169 // CRUD tests : READ (list, or multiple) tests
170 // ---------------------------------------------------------------
175 public abstract void readList();
177 protected void setupReadList() {
179 // Expected status code: 200 OK
180 EXPECTED_STATUS_CODE = Response.Status.OK.getStatusCode();
181 REQUEST_TYPE = ServiceRequestType.READ_MULTIPLE;
186 // None tested at present.
189 // ---------------------------------------------------------------
190 // CRUD tests : UPDATE tests
191 // ---------------------------------------------------------------
197 public abstract void update();
199 protected void setupUpdate() {
201 // Expected status code: 200 OK
202 EXPECTED_STATUS_CODE = Response.Status.OK.getStatusCode();
203 REQUEST_TYPE = ServiceRequestType.UPDATE;
209 public abstract void updateWithMalformedXml();
211 protected void setupUpdateWithMalformedXml() {
213 // Expected status code: 400 Bad Request
214 EXPECTED_STATUS_CODE = Response.Status.BAD_REQUEST.getStatusCode();
215 REQUEST_TYPE = ServiceRequestType.UPDATE;
219 public abstract void updateWithWrongXmlSchema();
221 protected void setupUpdateWithWrongXmlSchema() {
223 // Expected status code: 400 Bad Request
224 EXPECTED_STATUS_CODE = Response.Status.BAD_REQUEST.getStatusCode();
225 REQUEST_TYPE = ServiceRequestType.UPDATE;
229 public abstract void updateNonExistent();
231 protected void setupUpdateNonExistent() {
233 // Expected status code: 404 Not Found
234 EXPECTED_STATUS_CODE = Response.Status.NOT_FOUND.getStatusCode();
235 REQUEST_TYPE = ServiceRequestType.UPDATE;
239 // ---------------------------------------------------------------
240 // CRUD tests : DELETE tests
241 // ---------------------------------------------------------------
246 public abstract void delete();
248 protected void setupDelete() {
250 // Expected status code: 200 OK
251 EXPECTED_STATUS_CODE = Response.Status.OK.getStatusCode();
252 REQUEST_TYPE = ServiceRequestType.DELETE;
258 public abstract void deleteNonExistent();
260 protected void setupDeleteNonExistent() {
262 // Expected status code: 404 Not Found
263 EXPECTED_STATUS_CODE = Response.Status.NOT_FOUND.getStatusCode();
264 REQUEST_TYPE = ServiceRequestType.DELETE;
268 // ---------------------------------------------------------------
269 // Abstract utility methods
271 // Must be implemented by classes that extend
272 // this abstract base class.
273 // ---------------------------------------------------------------
276 * Returns the URL path component of the service.
278 * This component will follow directly after the
281 protected abstract String getServicePathComponent();
284 // ---------------------------------------------------------------
286 // ---------------------------------------------------------------
289 * Reinitializes setup values, to help expose any unintended reuse
290 * of those values between tests.
292 protected void clearSetup() {
293 EXPECTED_STATUS_CODE = 0;
294 REQUEST_TYPE = ServiceRequestType.NON_EXISTENT;
297 // @TODO Add Javadoc comments to all methods requiring them, below.
299 protected String invalidStatusCodeMessage(ServiceRequestType requestType, int statusCode) {
301 "Status code '" + statusCode + "' in response is NOT within the expected set: " +
302 requestType.validStatusCodesAsString();
305 protected String getServiceRootURL() {
306 return serviceClient.getBaseURL() + getServicePathComponent();
309 protected String getResourceURL(String resourceIdentifier) {
310 return getServiceRootURL() + "/" + resourceIdentifier;
313 protected int submitRequest(HttpMethod method) {
316 statusCode = httpClient.executeMethod(method);
317 } catch(HttpException e) {
318 logger.error("Fatal protocol violation: ", e);
319 } catch(IOException e) {
320 logger.error("Fatal transport error: ", e);
321 } catch(Exception e) {
322 logger.error("Unknown exception: ", e);
324 // Release the connection.
325 method.releaseConnection();
330 protected int submitRequest(EntityEnclosingMethod method, RequestEntity entity) {
333 method.setRequestEntity(entity);
334 statusCode = httpClient.executeMethod(method);
335 } catch(HttpException e) {
336 logger.error("Fatal protocol violation: ", e);
337 } catch(IOException e) {
338 logger.error("Fatal transport error: ", e);
339 } catch(Exception e) {
340 logger.error("Unknown exception: ", e);
342 // Release the connection.
343 method.releaseConnection();
348 protected StringRequestEntity getXmlEntity(String contents) {
349 if (contents == null) {
352 StringRequestEntity entity = null;
353 final String XML_DECLARATION =
354 "<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"yes\"?>";
355 final String XML_CONTENT_TYPE=MediaType.APPLICATION_XML;
356 final String UTF8_CHARSET_NAME = "UTF-8";
359 new StringRequestEntity(
360 XML_DECLARATION + contents, XML_CONTENT_TYPE, UTF8_CHARSET_NAME);
361 } catch (UnsupportedEncodingException e) {
362 logger.error("Unsupported character encoding error: ", e);
367 protected String extractId(ClientResponse<Response> res) {
368 MultivaluedMap mvm = res.getMetadata();
369 String uri = (String) ((ArrayList) mvm.get("Location")).get(0);
370 verbose("extractId:uri=" + uri);
371 String[] segments = uri.split("/");
372 String id = segments[segments.length - 1];
377 protected void verbose(String msg) {
378 if (logger.isDebugEnabled()) {
383 protected void verbose(String msg, Object o, Class clazz) {
386 JAXBContext jc = JAXBContext.newInstance(clazz);
387 Marshaller m = jc.createMarshaller();
388 m.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT,
390 m.marshal(o, System.out);
396 protected void verboseMap(MultivaluedMap map) {
397 for(Object entry : map.entrySet()){
398 MultivaluedMap.Entry mentry = (MultivaluedMap.Entry) entry;
399 verbose(" name=" + mentry.getKey() + " value=" + mentry.getValue());
403 protected String createIdentifier() {
404 long identifier = System.currentTimeMillis();
405 return Long.toString(identifier);
408 protected String createNonExistentIdentifier() {
409 return Long.toString(Long.MAX_VALUE);