View Javadoc
1   /*
2    * ====================================================================
3    * Licensed to the Apache Software Foundation (ASF) under one
4    * or more contributor license agreements.  See the NOTICE file
5    * distributed with this work for additional information
6    * regarding copyright ownership.  The ASF licenses this file
7    * to you under the Apache License, Version 2.0 (the
8    * "License"); you may not use this file except in compliance
9    * with the License.  You may obtain a copy of the License at
10   *
11   *   http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing,
14   * software distributed under the License is distributed on an
15   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16   * KIND, either express or implied.  See the License for the
17   * specific language governing permissions and limitations
18   * under the License.
19   * ====================================================================
20   *
21   * This software consists of voluntary contributions made by many
22   * individuals on behalf of the Apache Software Foundation.  For more
23   * information on the Apache Software Foundation, please see
24   * <http://www.apache.org/>.
25   *
26   */
27  
28  package org.apache.hc.core5.testing.framework;
29  
30  import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.BODY;
31  import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.CONTENT_TYPE;
32  import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.HEADERS;
33  import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.METHOD;
34  import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.REQUEST;
35  import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.RESPONSE;
36  import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.STATUS;
37  
38  import java.io.ByteArrayInputStream;
39  import java.io.ByteArrayOutputStream;
40  import java.io.IOException;
41  import java.io.ObjectInputStream;
42  import java.io.ObjectOutputStream;
43  import java.util.ArrayList;
44  import java.util.Arrays;
45  import java.util.Collections;
46  import java.util.HashMap;
47  import java.util.List;
48  import java.util.Map;
49  import java.util.concurrent.TimeUnit;
50  
51  import org.apache.hc.core5.http.HttpVersion;
52  import org.apache.hc.core5.http.ProtocolVersion;
53  import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
54  import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
55  import org.apache.hc.core5.http.io.HttpRequestHandler;
56  import org.apache.hc.core5.http.io.SocketConfig;
57  import org.apache.hc.core5.http.protocol.UriPatternMatcher;
58  import org.apache.hc.core5.io.CloseMode;
59  
60  public class TestingFramework {
61  
62      /**
63       * Use the ALL_METHODS list to conveniently cycle through all HTTP methods.
64       */
65      public static final List<String> ALL_METHODS = Arrays.asList("HEAD", "GET", "DELETE", "POST", "PUT", "PATCH");
66  
67      /**
68       * If an {@link ClassicTestClientTestingAdapter} is unable to return a response in
69       * the format this testing framework is needing, then it will need to check the
70       * item in the response (such as body, status, headers, or contentType) itself and set
71       * the returned value of the item as ALREADY_CHECKED.
72       */
73      public static final Object ALREADY_CHECKED = new Object();
74  
75      /**
76       * If a test does not specify a path, this one is used.
77       */
78      public static final String DEFAULT_REQUEST_PATH = "a/path";
79  
80      /**
81       * If a test does not specify a body, this one is used.
82       */
83      public static final String DEFAULT_REQUEST_BODY = "{\"location\":\"home\"}";
84  
85      /**
86       * If a test does not specify a request contentType, this one is used.
87       */
88      public static final String DEFAULT_REQUEST_CONTENT_TYPE = "application/json";
89  
90      /**
91       * If a test does not specify query parameters, these are used.
92       */
93      public static final Map<String, String> DEFAULT_REQUEST_QUERY;
94  
95      /**
96       * If a test does not specify a request headers, these are used.
97       */
98      public static final Map<String, String> DEFAULT_REQUEST_HEADERS;
99  
100     /**
101      * If a test does not specify a protocol version, this one is used.
102      */
103     public static final ProtocolVersion DEFAULT_REQUEST_PROTOCOL_VERSION = HttpVersion.HTTP_1_1;
104 
105     /**
106      * If a test does not specify an expected response status, this one is used.
107      */
108     public static final int DEFAULT_RESPONSE_STATUS = 200;
109 
110     /**
111      * If a test does not specify an expected response body, this one is used.
112      */
113     public static final String DEFAULT_RESPONSE_BODY = "{\"location\":\"work\"}";
114 
115     /**
116      * If a test does not specify an expected response contentType, this one is used.
117      */
118     public static final String DEFAULT_RESPONSE_CONTENT_TYPE = "application/json";
119 
120     /**
121      * If a test does not specify expected response headers, these are used.
122      */
123     public static final Map<String, String> DEFAULT_RESPONSE_HEADERS;
124 
125     static {
126         final Map<String, String> request = new HashMap<>();
127         request.put("p1", "this");
128         request.put("p2", "that");
129         DEFAULT_REQUEST_QUERY = Collections.unmodifiableMap(request);
130 
131         Map<String, String> headers = new HashMap<>();
132         headers.put("header1", "stuff");
133         headers.put("header2", "more stuff");
134         DEFAULT_REQUEST_HEADERS = Collections.unmodifiableMap(headers);
135 
136         headers = new HashMap<>();
137         headers.put("header3", "header_three");
138         headers.put("header4", "header_four");
139         DEFAULT_RESPONSE_HEADERS = Collections.unmodifiableMap(headers);
140     }
141 
142     private ClientTestingAdapter adapter;
143     private TestingFrameworkRequestHandlerRequestHandler.html#TestingFrameworkRequestHandler">TestingFrameworkRequestHandler requestHandler = new TestingFrameworkRequestHandler();
144     private List<FrameworkTest> tests = new ArrayList<>();
145 
146     private HttpServer server;
147     private int port;
148 
149     public TestingFramework() throws TestingFrameworkException {
150         this(null);
151     }
152 
153     public TestingFramework(final ClientTestingAdapter adapter) throws TestingFrameworkException {
154         this.adapter = adapter;
155 
156         /*
157          * By default, a set of tests that will exercise each HTTP method are pre-loaded.
158          */
159         for (final String method : ALL_METHODS) {
160             final List<Integer> statusList = Arrays.asList(200, 201);
161             for (final Integer status : statusList) {
162                 final Map<String, Object> request = new HashMap<>();
163                 request.put(METHOD, method);
164 
165                 final Map<String, Object> response = new HashMap<>();
166                 response.put(STATUS, status);
167 
168                 final Map<String, Object> test = new HashMap<>();
169                 test.put(REQUEST, request);
170                 test.put(RESPONSE, response);
171 
172                 addTest(test);
173             }
174         }
175     }
176 
177     /**
178      * This is not likely to be used except during the testing of this class.
179      * It is used to inject a mocked request handler.
180      *
181      * @param requestHandler
182      */
183     public void setRequestHandler(final TestingFrameworkRequestHandler requestHandler) {
184         this.requestHandler = requestHandler;
185     }
186 
187     /**
188      * Run the tests that have been previously added.  First, an in-process {@link HttpServer} is
189      * started.  Then, all the tests are completed by passing each test to the adapter
190      * which will make the HTTP request.
191      *
192      * @throws TestingFrameworkException if there is a test failure or unexpected problem.
193      */
194     public void runTests() throws TestingFrameworkException {
195         if (adapter == null) {
196             throw new TestingFrameworkException("adapter should not be null");
197         }
198 
199         startServer();
200 
201         try {
202             for (final FrameworkTest test : tests) {
203                 try {
204                     callAdapter(test);
205                 } catch (final Throwable t) {
206                     processThrowable(t, test);
207                 }
208             }
209         } finally {
210             stopServer();
211         }
212     }
213 
214     private void processThrowable(final Throwable t, final FrameworkTest test) throws TestingFrameworkException {
215         final TestingFrameworkException e;
216         if (t instanceof TestingFrameworkException) {
217             e = (TestingFrameworkException) t;
218         } else {
219             e = new TestingFrameworkException(t);
220         }
221         e.setAdapter(adapter);
222         e.setTest(test);
223         throw e;
224     }
225 
226     private void startServer() throws TestingFrameworkException {
227         /*
228          * Start an in-process server and handle all HTTP requests
229          * with the requestHandler.
230          */
231         final SocketConfig socketConfig = SocketConfig.custom()
232                                           .setSoTimeout(15000, TimeUnit.MILLISECONDS)
233                                           .build();
234 
235         final ServerBootstrap serverBootstrap = ServerBootstrap.bootstrap()
236                                           .setLookupRegistry(new UriPatternMatcher<HttpRequestHandler>())
237                                           .setSocketConfig(socketConfig)
238                                           .register("/*", requestHandler);
239 
240         server = serverBootstrap.create();
241         try {
242             server.start();
243         } catch (final IOException e) {
244             throw new TestingFrameworkException(e);
245         }
246 
247         port = server.getLocalPort();
248     }
249 
250     private void stopServer() {
251         final HttpServer local = this.server;
252         this.server = null;
253         if (local != null) {
254             local.close(CloseMode.IMMEDIATE);
255         }
256     }
257 
258     private void callAdapter(final FrameworkTest test) throws TestingFrameworkException {
259         Map<String, Object> request = test.initRequest();
260 
261         /*
262          * If the adapter does not support the particular request, skip the test.
263          */
264         if (! adapter.isRequestSupported(request)) {
265             return;
266         }
267 
268         /*
269          * Allow the adapter to modify the request before the request expectations
270          * are given to the requestHandler.  Typically, adapters should not have
271          * to modify the request.
272          */
273         request = adapter.modifyRequest(request);
274 
275         // Tell the request handler what to expect in the request.
276         requestHandler.setRequestExpectations(request);
277 
278         Map<String, Object> responseExpectations = test.initResponseExpectations();
279         /*
280          * Allow the adapter to modify the response expectations before the handler
281          * is told what to return.  Typically, adapters should not have to modify
282          * the response expectations.
283          */
284         responseExpectations = adapter.modifyResponseExpectations(request, responseExpectations);
285 
286         // Tell the request handler what response to return.
287         requestHandler.setDesiredResponse(responseExpectations);
288 
289         /*
290          * Use the adapter to make the HTTP call.  Make sure the responseExpectations are not changed
291          * since they have already been sent to the request handler and they will later be used
292          * to check the response.
293          */
294         final String defaultURI = getDefaultURI();
295         final Map<String, Object> response = adapter.execute(
296                                                 defaultURI,
297                                                 request,
298                                                 requestHandler,
299                                                 Collections.unmodifiableMap(responseExpectations));
300         /*
301          * The adapter is welcome to call assertNothingThrown() earlier, but we will
302          * do it here to make sure it is done.  If the handler threw any exception
303          * while checking the request it received, it will be re-thrown here.
304          */
305         requestHandler.assertNothingThrown();
306 
307         assertResponseMatchesExpectation(request.get(METHOD), response, responseExpectations);
308     }
309 
310     @SuppressWarnings("unchecked")
311     private void assertResponseMatchesExpectation(final Object method, final Map<String, Object> actualResponse,
312                                                   final Map<String, Object> expectedResponse)
313                                                   throws TestingFrameworkException {
314         if (actualResponse == null) {
315             throw new TestingFrameworkException("response should not be null");
316         }
317         /*
318          * Now check the items in the response unless the adapter says they
319          * already checked something.
320          */
321         if (actualResponse.get(STATUS) != TestingFramework.ALREADY_CHECKED) {
322             assertStatusMatchesExpectation(actualResponse.get(STATUS), expectedResponse.get(STATUS));
323         }
324         if (! method.equals("HEAD")) {
325             if (actualResponse.get(BODY) != TestingFramework.ALREADY_CHECKED) {
326                 assertBodyMatchesExpectation(actualResponse.get(BODY), expectedResponse.get(BODY));
327             }
328             if (actualResponse.get(CONTENT_TYPE) != TestingFramework.ALREADY_CHECKED) {
329                 assertContentTypeMatchesExpectation(actualResponse.get(CONTENT_TYPE), expectedResponse.get(CONTENT_TYPE));
330             }
331         }
332         if (actualResponse.get(HEADERS) != TestingFramework.ALREADY_CHECKED) {
333             assertHeadersMatchExpectation((Map<String, String>) actualResponse.get(HEADERS),
334                                           (Map<String, String>) expectedResponse.get(HEADERS));
335         }
336     }
337 
338     private void assertStatusMatchesExpectation(final Object actualStatus, final Object expectedStatus)
339             throws TestingFrameworkException {
340         if (actualStatus == null) {
341             throw new TestingFrameworkException("Returned status is null.");
342         }
343         if ((expectedStatus != null) && (! actualStatus.equals(expectedStatus))) {
344             throw new TestingFrameworkException("Expected status not found. expected="
345                                                   + expectedStatus + "; actual=" + actualStatus);
346         }
347     }
348 
349     private void assertBodyMatchesExpectation(final Object actualBody, final Object expectedBody)
350         throws TestingFrameworkException {
351         if (actualBody == null) {
352             throw new TestingFrameworkException("Returned body is null.");
353         }
354         if ((expectedBody != null) && (! actualBody.equals(expectedBody))) {
355             throw new TestingFrameworkException("Expected body not found. expected="
356                                     + expectedBody + "; actual=" + actualBody);
357         }
358     }
359 
360     private void assertContentTypeMatchesExpectation(final Object actualContentType, final Object expectedContentType)
361         throws TestingFrameworkException {
362         if (expectedContentType != null) {
363             if (actualContentType == null) {
364                 throw new TestingFrameworkException("Returned contentType is null.");
365             }
366             if (! actualContentType.equals(expectedContentType)) {
367                 throw new TestingFrameworkException("Expected content type not found.  expected="
368                                     + expectedContentType + "; actual=" + actualContentType);
369             }
370         }
371     }
372 
373     private void assertHeadersMatchExpectation(final Map<String, String> actualHeaders,
374                                                final Map<String, String>  expectedHeaders)
375             throws TestingFrameworkException {
376         if (expectedHeaders == null) {
377             return;
378         }
379         for (final Map.Entry<String, String> expectedHeader : expectedHeaders.entrySet()) {
380             final String expectedHeaderName = expectedHeader.getKey();
381             if (! actualHeaders.containsKey(expectedHeaderName)) {
382                 throw new TestingFrameworkException("Expected header not found: name=" + expectedHeaderName);
383             }
384             if (! actualHeaders.get(expectedHeaderName).equals(expectedHeaders.get(expectedHeaderName))) {
385                 throw new TestingFrameworkException("Header value not expected: name=" + expectedHeaderName
386                         + "; expected=" + expectedHeaders.get(expectedHeaderName)
387                         + "; actual=" + actualHeaders.get(expectedHeaderName));
388             }
389         }
390     }
391 
392     private String getDefaultURI() {
393         return "http://localhost:" + port  + "/";
394     }
395 
396     /**
397      * Sets the {@link ClientTestingAdapter}.
398      *
399      * @param adapter
400      */
401     public void setAdapter(final ClientTestingAdapter adapter) {
402         this.adapter = adapter;
403     }
404 
405     /**
406      * Deletes all tests.
407      */
408     public void deleteTests() {
409         tests = new ArrayList<>();
410     }
411 
412     /**
413      * Call to add a test with defaults.
414      *
415      * @throws TestingFrameworkException
416      */
417     public void addTest() throws TestingFrameworkException {
418         addTest(null);
419     }
420 
421     /**
422      * Call to add a test.  The test is a map with a REQUEST and a RESPONSE key.
423      * See {@link ClientPOJOAdapter} for details on the format of the request and response.
424      *
425      * @param test Map with a REQUEST and a RESPONSE key.
426      * @throws TestingFrameworkException
427      */
428     @SuppressWarnings("unchecked")
429     public void addTest(final Map<String, Object> test) throws TestingFrameworkException {
430         final Map<String, Object> testCopy = (Map<String, Object>) deepcopy(test);
431 
432         tests.add(new FrameworkTest(testCopy));
433     }
434 
435     /**
436      * Used to make a "deep" copy of an object.  This testing framework makes deep copies
437      * of tests that are added as well as requestExpectations Maps and response Maps.
438      *
439      * @param orig a serializable object.
440      * @return a deep copy of the orig object.
441      * @throws TestingFrameworkException
442      */
443     public static Object deepcopy(final Object orig) throws TestingFrameworkException {
444         try {
445             // this is from http://stackoverflow.com/questions/13155127/deep-copy-map-in-groovy
446             final ByteArrayOutputStream bos = new ByteArrayOutputStream();
447             final ObjectOutputStream oos = new ObjectOutputStream(bos);
448             oos.writeObject(orig);
449             oos.flush();
450             final ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
451             final ObjectInputStream ois = new ObjectInputStream(bin);
452             return ois.readObject();
453         } catch (final ClassNotFoundException | IOException ex) {
454             throw new TestingFrameworkException(ex);
455         }
456     }
457 }