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.impl.routing.RequestRouter;
56  import org.apache.hc.core5.http.io.HttpRequestHandler;
57  import org.apache.hc.core5.http.io.SocketConfig;
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 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                                           .setSocketConfig(socketConfig)
237                                           .setRequestRouter(RequestRouter.<HttpRequestHandler>builder()
238                                                 .addRoute(RequestRouter.LOCAL_AUTHORITY, "*", requestHandler)
239                                                 .resolveAuthority(RequestRouter.LOCAL_AUTHORITY_RESOLVER)
240                                                 .build());
241 
242         server = serverBootstrap.create();
243         try {
244             server.start();
245         } catch (final IOException e) {
246             throw new TestingFrameworkException(e);
247         }
248 
249         port = server.getLocalPort();
250     }
251 
252     private void stopServer() {
253         final HttpServer local = this.server;
254         this.server = null;
255         if (local != null) {
256             local.close(CloseMode.IMMEDIATE);
257         }
258     }
259 
260     private void callAdapter(final FrameworkTest test) throws TestingFrameworkException {
261         Map<String, Object> request = test.initRequest();
262 
263         /*
264          * If the adapter does not support the particular request, skip the test.
265          */
266         if (! adapter.isRequestSupported(request)) {
267             return;
268         }
269 
270         /*
271          * Allow the adapter to modify the request before the request expectations
272          * are given to the requestHandler.  Typically, adapters should not have
273          * to modify the request.
274          */
275         request = adapter.modifyRequest(request);
276 
277         // Tell the request handler what to expect in the request.
278         requestHandler.setRequestExpectations(request);
279 
280         Map<String, Object> responseExpectations = test.initResponseExpectations();
281         /*
282          * Allow the adapter to modify the response expectations before the handler
283          * is told what to return.  Typically, adapters should not have to modify
284          * the response expectations.
285          */
286         responseExpectations = adapter.modifyResponseExpectations(request, responseExpectations);
287 
288         // Tell the request handler what response to return.
289         requestHandler.setDesiredResponse(responseExpectations);
290 
291         /*
292          * Use the adapter to make the HTTP call.  Make sure the responseExpectations are not changed
293          * since they have already been sent to the request handler and they will later be used
294          * to check the response.
295          */
296         final String defaultURI = getDefaultURI();
297         final Map<String, Object> response = adapter.execute(
298                                                 defaultURI,
299                                                 request,
300                                                 requestHandler,
301                                                 Collections.unmodifiableMap(responseExpectations));
302         /*
303          * The adapter is welcome to call assertNothingThrown() earlier, but we will
304          * do it here to make sure it is done.  If the handler threw any exception
305          * while checking the request it received, it will be re-thrown here.
306          */
307         requestHandler.assertNothingThrown();
308 
309         assertResponseMatchesExpectation(request.get(METHOD), response, responseExpectations);
310     }
311 
312     @SuppressWarnings("unchecked")
313     private void assertResponseMatchesExpectation(final Object method, final Map<String, Object> actualResponse,
314                                                   final Map<String, Object> expectedResponse)
315                                                   throws TestingFrameworkException {
316         if (actualResponse == null) {
317             throw new TestingFrameworkException("response should not be null");
318         }
319         /*
320          * Now check the items in the response unless the adapter says they
321          * already checked something.
322          */
323         if (actualResponse.get(STATUS) != TestingFramework.ALREADY_CHECKED) {
324             assertStatusMatchesExpectation(actualResponse.get(STATUS), expectedResponse.get(STATUS));
325         }
326         if (! method.equals("HEAD")) {
327             if (actualResponse.get(BODY) != TestingFramework.ALREADY_CHECKED) {
328                 assertBodyMatchesExpectation(actualResponse.get(BODY), expectedResponse.get(BODY));
329             }
330             if (actualResponse.get(CONTENT_TYPE) != TestingFramework.ALREADY_CHECKED) {
331                 assertContentTypeMatchesExpectation(actualResponse.get(CONTENT_TYPE), expectedResponse.get(CONTENT_TYPE));
332             }
333         }
334         if (actualResponse.get(HEADERS) != TestingFramework.ALREADY_CHECKED) {
335             assertHeadersMatchExpectation((Map<String, String>) actualResponse.get(HEADERS),
336                                           (Map<String, String>) expectedResponse.get(HEADERS));
337         }
338     }
339 
340     private void assertStatusMatchesExpectation(final Object actualStatus, final Object expectedStatus)
341             throws TestingFrameworkException {
342         if (actualStatus == null) {
343             throw new TestingFrameworkException("Returned status is null.");
344         }
345         if ((expectedStatus != null) && (! actualStatus.equals(expectedStatus))) {
346             throw new TestingFrameworkException("Expected status not found. expected="
347                                                   + expectedStatus + "; actual=" + actualStatus);
348         }
349     }
350 
351     private void assertBodyMatchesExpectation(final Object actualBody, final Object expectedBody)
352         throws TestingFrameworkException {
353         if (actualBody == null) {
354             throw new TestingFrameworkException("Returned body is null.");
355         }
356         if ((expectedBody != null) && (! actualBody.equals(expectedBody))) {
357             throw new TestingFrameworkException("Expected body not found. expected="
358                                     + expectedBody + "; actual=" + actualBody);
359         }
360     }
361 
362     private void assertContentTypeMatchesExpectation(final Object actualContentType, final Object expectedContentType)
363         throws TestingFrameworkException {
364         if (expectedContentType != null) {
365             if (actualContentType == null) {
366                 throw new TestingFrameworkException("Returned contentType is null.");
367             }
368             if (! actualContentType.equals(expectedContentType)) {
369                 throw new TestingFrameworkException("Expected content type not found.  expected="
370                                     + expectedContentType + "; actual=" + actualContentType);
371             }
372         }
373     }
374 
375     private void assertHeadersMatchExpectation(final Map<String, String> actualHeaders,
376                                                final Map<String, String>  expectedHeaders)
377             throws TestingFrameworkException {
378         if (expectedHeaders == null) {
379             return;
380         }
381         for (final Map.Entry<String, String> expectedHeader : expectedHeaders.entrySet()) {
382             final String expectedHeaderName = expectedHeader.getKey();
383             if (! actualHeaders.containsKey(expectedHeaderName)) {
384                 throw new TestingFrameworkException("Expected header not found: name=" + expectedHeaderName);
385             }
386             if (! actualHeaders.get(expectedHeaderName).equals(expectedHeaders.get(expectedHeaderName))) {
387                 throw new TestingFrameworkException("Header value not expected: name=" + expectedHeaderName
388                         + "; expected=" + expectedHeaders.get(expectedHeaderName)
389                         + "; actual=" + actualHeaders.get(expectedHeaderName));
390             }
391         }
392     }
393 
394     private String getDefaultURI() {
395         return "http://localhost:" + port  + "/";
396     }
397 
398     /**
399      * Sets the {@link ClientTestingAdapter}.
400      *
401      * @param adapter
402      */
403     public void setAdapter(final ClientTestingAdapter adapter) {
404         this.adapter = adapter;
405     }
406 
407     /**
408      * Deletes all tests.
409      */
410     public void deleteTests() {
411         tests = new ArrayList<>();
412     }
413 
414     /**
415      * Call to add a test with defaults.
416      *
417      * @throws TestingFrameworkException
418      */
419     public void addTest() throws TestingFrameworkException {
420         addTest(null);
421     }
422 
423     /**
424      * Call to add a test.  The test is a map with a REQUEST and a RESPONSE key.
425      * See {@link ClientPOJOAdapter} for details on the format of the request and response.
426      *
427      * @param test Map with a REQUEST and a RESPONSE key.
428      * @throws TestingFrameworkException
429      */
430     @SuppressWarnings("unchecked")
431     public void addTest(final Map<String, Object> test) throws TestingFrameworkException {
432         final Map<String, Object> testCopy = (Map<String, Object>) deepcopy(test);
433 
434         tests.add(new FrameworkTest(testCopy));
435     }
436 
437     /**
438      * Used to make a "deep" copy of an object.  This testing framework makes deep copies
439      * of tests that are added as well as requestExpectations Maps and response Maps.
440      *
441      * @param orig a serializable object.
442      * @return a deep copy of the orig object.
443      * @throws TestingFrameworkException
444      */
445     public static Object deepcopy(final Object orig) throws TestingFrameworkException {
446         try {
447             // this is from http://stackoverflow.com/questions/13155127/deep-copy-map-in-groovy
448             final ByteArrayOutputStream bos = new ByteArrayOutputStream();
449             final ObjectOutputStream oos = new ObjectOutputStream(bos);
450             oos.writeObject(orig);
451             oos.flush();
452             final ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
453             final ObjectInputStream ois = new ObjectInputStream(bin);
454             return ois.readObject();
455         } catch (final ClassNotFoundException | IOException ex) {
456             throw new TestingFrameworkException(ex);
457         }
458     }
459 }