blob: a0658279702b9d3608d4e3c194ba3f33782e33e3 [file] [log] [blame]
/*
* ====================================================================
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
* ====================================================================
*
* This software consists of voluntary contributions made by many
* individuals on behalf of the Apache Software Foundation. For more
* information on the Apache Software Foundation, please see
* <http://www.apache.org/>.
*
*/
package org.apache.hc.core5.testing.framework;
import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.BODY;
import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.CONTENT_TYPE;
import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.HEADERS;
import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.METHOD;
import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.REQUEST;
import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.RESPONSE;
import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.STATUS;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import org.apache.hc.core5.http.HttpVersion;
import org.apache.hc.core5.http.ProtocolVersion;
import org.apache.hc.core5.http.impl.bootstrap.HttpServer;
import org.apache.hc.core5.http.impl.bootstrap.ServerBootstrap;
import org.apache.hc.core5.http.io.SocketConfig;
import org.apache.hc.core5.http.protocol.UriPatternMatcher;
import org.apache.hc.core5.io.CloseMode;
public class TestingFramework {
/**
* Use the ALL_METHODS list to conveniently cycle through all HTTP methods.
*/
public static final List<String> ALL_METHODS = Arrays.asList("HEAD", "GET", "DELETE", "POST", "PUT", "PATCH");
/**
* If an {@link ClassicTestClientTestingAdapter} is unable to return a response in
* the format this testing framework is needing, then it will need to check the
* item in the response (such as body, status, headers, or contentType) itself and set
* the returned value of the item as ALREADY_CHECKED.
*/
public static final Object ALREADY_CHECKED = new Object();
/**
* If a test does not specify a path, this one is used.
*/
public static final String DEFAULT_REQUEST_PATH = "a/path";
/**
* If a test does not specify a body, this one is used.
*/
public static final String DEFAULT_REQUEST_BODY = "{\"location\":\"home\"}";
/**
* If a test does not specify a request contentType, this one is used.
*/
public static final String DEFAULT_REQUEST_CONTENT_TYPE = "application/json";
/**
* If a test does not specify query parameters, these are used.
*/
public static final Map<String, String> DEFAULT_REQUEST_QUERY;
/**
* If a test does not specify a request headers, these are used.
*/
public static final Map<String, String> DEFAULT_REQUEST_HEADERS;
/**
* If a test does not specify a protocol version, this one is used.
*/
public static final ProtocolVersion DEFAULT_REQUEST_PROTOCOL_VERSION = HttpVersion.HTTP_1_1;
/**
* If a test does not specify an expected response status, this one is used.
*/
public static final int DEFAULT_RESPONSE_STATUS = 200;
/**
* If a test does not specify an expected response body, this one is used.
*/
public static final String DEFAULT_RESPONSE_BODY = "{\"location\":\"work\"}";
/**
* If a test does not specify an expected response contentType, this one is used.
*/
public static final String DEFAULT_RESPONSE_CONTENT_TYPE = "application/json";
/**
* If a test does not specify expected response headers, these are used.
*/
public static final Map<String, String> DEFAULT_RESPONSE_HEADERS;
static {
final Map<String, String> request = new HashMap<>();
request.put("p1", "this");
request.put("p2", "that");
DEFAULT_REQUEST_QUERY = Collections.unmodifiableMap(request);
Map<String, String> headers = new HashMap<>();
headers.put("header1", "stuff");
headers.put("header2", "more stuff");
DEFAULT_REQUEST_HEADERS = Collections.unmodifiableMap(headers);
headers = new HashMap<>();
headers.put("header3", "header_three");
headers.put("header4", "header_four");
DEFAULT_RESPONSE_HEADERS = Collections.unmodifiableMap(headers);
}
private ClientTestingAdapter adapter;
private TestingFrameworkRequestHandler requestHandler = new TestingFrameworkRequestHandler();
private List<FrameworkTest> tests = new ArrayList<>();
private HttpServer server;
private int port;
public TestingFramework() throws TestingFrameworkException {
this(null);
}
public TestingFramework(final ClientTestingAdapter adapter) throws TestingFrameworkException {
this.adapter = adapter;
/*
* By default, a set of tests that will exercise each HTTP method are pre-loaded.
*/
for (final String method : ALL_METHODS) {
final List<Integer> statusList = Arrays.asList(200, 201);
for (final Integer status : statusList) {
final Map<String, Object> request = new HashMap<>();
request.put(METHOD, method);
final Map<String, Object> response = new HashMap<>();
response.put(STATUS, status);
final Map<String, Object> test = new HashMap<>();
test.put(REQUEST, request);
test.put(RESPONSE, response);
addTest(test);
}
}
}
/**
* This is not likely to be used except during the testing of this class.
* It is used to inject a mocked request handler.
*
* @param requestHandler
*/
public void setRequestHandler(final TestingFrameworkRequestHandler requestHandler) {
this.requestHandler = requestHandler;
}
/**
* Run the tests that have been previously added. First, an in-process {@link HttpServer} is
* started. Then, all the tests are completed by passing each test to the adapter
* which will make the HTTP request.
*
* @throws TestingFrameworkException if there is a test failure or unexpected problem.
*/
public void runTests() throws TestingFrameworkException {
if (adapter == null) {
throw new TestingFrameworkException("adapter should not be null");
}
startServer();
try {
for (final FrameworkTest test : tests) {
try {
callAdapter(test);
} catch (final Throwable t) {
processThrowable(t, test);
}
}
} finally {
stopServer();
}
}
private void processThrowable(final Throwable t, final FrameworkTest test) throws TestingFrameworkException {
final TestingFrameworkException e;
if (t instanceof TestingFrameworkException) {
e = (TestingFrameworkException) t;
} else {
e = new TestingFrameworkException(t);
}
e.setAdapter(adapter);
e.setTest(test);
throw e;
}
private void startServer() throws TestingFrameworkException {
/*
* Start an in-process server and handle all HTTP requests
* with the requestHandler.
*/
final SocketConfig socketConfig = SocketConfig.custom()
.setSoTimeout(15000, TimeUnit.MILLISECONDS)
.build();
final ServerBootstrap serverBootstrap = ServerBootstrap.bootstrap()
.setLookupRegistry(new UriPatternMatcher<>())
.setSocketConfig(socketConfig)
.register("/*", requestHandler);
server = serverBootstrap.create();
try {
server.start();
} catch (final IOException e) {
throw new TestingFrameworkException(e);
}
port = server.getLocalPort();
}
private void stopServer() {
final HttpServer local = this.server;
this.server = null;
if (local != null) {
local.close(CloseMode.IMMEDIATE);
}
}
private void callAdapter(final FrameworkTest test) throws TestingFrameworkException {
Map<String, Object> request = test.initRequest();
/*
* If the adapter does not support the particular request, skip the test.
*/
if (! adapter.isRequestSupported(request)) {
return;
}
/*
* Allow the adapter to modify the request before the request expectations
* are given to the requestHandler. Typically, adapters should not have
* to modify the request.
*/
request = adapter.modifyRequest(request);
// Tell the request handler what to expect in the request.
requestHandler.setRequestExpectations(request);
Map<String, Object> responseExpectations = test.initResponseExpectations();
/*
* Allow the adapter to modify the response expectations before the handler
* is told what to return. Typically, adapters should not have to modify
* the response expectations.
*/
responseExpectations = adapter.modifyResponseExpectations(request, responseExpectations);
// Tell the request handler what response to return.
requestHandler.setDesiredResponse(responseExpectations);
/*
* Use the adapter to make the HTTP call. Make sure the responseExpectations are not changed
* since they have already been sent to the request handler and they will later be used
* to check the response.
*/
final String defaultURI = getDefaultURI();
final Map<String, Object> response = adapter.execute(
defaultURI,
request,
requestHandler,
Collections.unmodifiableMap(responseExpectations));
/*
* The adapter is welcome to call assertNothingThrown() earlier, but we will
* do it here to make sure it is done. If the handler threw any exception
* while checking the request it received, it will be re-thrown here.
*/
requestHandler.assertNothingThrown();
assertResponseMatchesExpectation(request.get(METHOD), response, responseExpectations);
}
@SuppressWarnings("unchecked")
private void assertResponseMatchesExpectation(final Object method, final Map<String, Object> actualResponse,
final Map<String, Object> expectedResponse)
throws TestingFrameworkException {
if (actualResponse == null) {
throw new TestingFrameworkException("response should not be null");
}
/*
* Now check the items in the response unless the adapter says they
* already checked something.
*/
if (actualResponse.get(STATUS) != TestingFramework.ALREADY_CHECKED) {
assertStatusMatchesExpectation(actualResponse.get(STATUS), expectedResponse.get(STATUS));
}
if (! method.equals("HEAD")) {
if (actualResponse.get(BODY) != TestingFramework.ALREADY_CHECKED) {
assertBodyMatchesExpectation(actualResponse.get(BODY), expectedResponse.get(BODY));
}
if (actualResponse.get(CONTENT_TYPE) != TestingFramework.ALREADY_CHECKED) {
assertContentTypeMatchesExpectation(actualResponse.get(CONTENT_TYPE), expectedResponse.get(CONTENT_TYPE));
}
}
if (actualResponse.get(HEADERS) != TestingFramework.ALREADY_CHECKED) {
assertHeadersMatchExpectation((Map<String, String>) actualResponse.get(HEADERS),
(Map<String, String>) expectedResponse.get(HEADERS));
}
}
private void assertStatusMatchesExpectation(final Object actualStatus, final Object expectedStatus)
throws TestingFrameworkException {
if (actualStatus == null) {
throw new TestingFrameworkException("Returned status is null.");
}
if ((expectedStatus != null) && (! actualStatus.equals(expectedStatus))) {
throw new TestingFrameworkException("Expected status not found. expected="
+ expectedStatus + "; actual=" + actualStatus);
}
}
private void assertBodyMatchesExpectation(final Object actualBody, final Object expectedBody)
throws TestingFrameworkException {
if (actualBody == null) {
throw new TestingFrameworkException("Returned body is null.");
}
if ((expectedBody != null) && (! actualBody.equals(expectedBody))) {
throw new TestingFrameworkException("Expected body not found. expected="
+ expectedBody + "; actual=" + actualBody);
}
}
private void assertContentTypeMatchesExpectation(final Object actualContentType, final Object expectedContentType)
throws TestingFrameworkException {
if (expectedContentType != null) {
if (actualContentType == null) {
throw new TestingFrameworkException("Returned contentType is null.");
}
if (! actualContentType.equals(expectedContentType)) {
throw new TestingFrameworkException("Expected content type not found. expected="
+ expectedContentType + "; actual=" + actualContentType);
}
}
}
private void assertHeadersMatchExpectation(final Map<String, String> actualHeaders,
final Map<String, String> expectedHeaders)
throws TestingFrameworkException {
if (expectedHeaders == null) {
return;
}
for (final Map.Entry<String, String> expectedHeader : expectedHeaders.entrySet()) {
final String expectedHeaderName = expectedHeader.getKey();
if (! actualHeaders.containsKey(expectedHeaderName)) {
throw new TestingFrameworkException("Expected header not found: name=" + expectedHeaderName);
}
if (! actualHeaders.get(expectedHeaderName).equals(expectedHeaders.get(expectedHeaderName))) {
throw new TestingFrameworkException("Header value not expected: name=" + expectedHeaderName
+ "; expected=" + expectedHeaders.get(expectedHeaderName)
+ "; actual=" + actualHeaders.get(expectedHeaderName));
}
}
}
private String getDefaultURI() {
return "http://localhost:" + port + "/";
}
/**
* Sets the {@link ClientTestingAdapter}.
*
* @param adapter
*/
public void setAdapter(final ClientTestingAdapter adapter) {
this.adapter = adapter;
}
/**
* Deletes all tests.
*/
public void deleteTests() {
tests = new ArrayList<>();
}
/**
* Call to add a test with defaults.
*
* @throws TestingFrameworkException
*/
public void addTest() throws TestingFrameworkException {
addTest(null);
}
/**
* Call to add a test. The test is a map with a REQUEST and a RESPONSE key.
* See {@link ClientPOJOAdapter} for details on the format of the request and response.
*
* @param test Map with a REQUEST and a RESPONSE key.
* @throws TestingFrameworkException
*/
@SuppressWarnings("unchecked")
public void addTest(final Map<String, Object> test) throws TestingFrameworkException {
final Map<String, Object> testCopy = (Map<String, Object>) deepcopy(test);
tests.add(new FrameworkTest(testCopy));
}
/**
* Used to make a "deep" copy of an object. This testing framework makes deep copies
* of tests that are added as well as requestExpectations Maps and response Maps.
*
* @param orig a serializable object.
* @return a deep copy of the orig object.
* @throws TestingFrameworkException
*/
public static Object deepcopy(final Object orig) throws TestingFrameworkException {
try {
// this is from http://stackoverflow.com/questions/13155127/deep-copy-map-in-groovy
final ByteArrayOutputStream bos = new ByteArrayOutputStream();
final ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(orig);
oos.flush();
final ByteArrayInputStream bin = new ByteArrayInputStream(bos.toByteArray());
final ObjectInputStream ois = new ObjectInputStream(bin);
return ois.readObject();
} catch (final ClassNotFoundException | IOException ex) {
throw new TestingFrameworkException(ex);
}
}
}