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