io.apiman.test.common.util.TestPlanRunner.java Source code

Java tutorial

Introduction

Here is the source code for io.apiman.test.common.util.TestPlanRunner.java

Source

/*
 * Copyright 2014 JBoss Inc
 *
 * Licensed 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.
 */
package io.apiman.test.common.util;

import io.apiman.test.common.plan.TestGroupType;
import io.apiman.test.common.plan.TestPlan;
import io.apiman.test.common.plan.TestType;
import io.apiman.test.common.resttest.RestTest;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.text.MessageFormat;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.codehaus.jackson.JsonNode;
import org.codehaus.jackson.map.ObjectMapper;
import org.codehaus.jackson.node.ArrayNode;
import org.codehaus.jackson.node.BooleanNode;
import org.codehaus.jackson.node.NullNode;
import org.codehaus.jackson.node.NumericNode;
import org.codehaus.jackson.node.ObjectNode;
import org.codehaus.jackson.node.TextNode;
import org.junit.Assert;
import org.mvel2.MVEL;
import org.mvel2.integration.PropertyHandler;
import org.mvel2.integration.PropertyHandlerFactory;
import org.mvel2.integration.VariableResolverFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * Runs a test plan.
 *
 * @author eric.wittmann@redhat.com
 */
public class TestPlanRunner {

    private static Logger logger = LoggerFactory.getLogger(TestPlanRunner.class);

    private CloseableHttpClient client = HttpClientBuilder.create().build();
    private String baseApiUrl;

    /**
     * Constructor.
     * @param baseApiUrl
     */
    public TestPlanRunner(String baseApiUrl) {
        this.baseApiUrl = baseApiUrl;
    }

    /**
     * Called to run a test plan.
     *
     * @param resourcePath
     * @param cl
     */
    public void runTestPlan(String resourcePath, ClassLoader cl) {
        client = HttpClientBuilder.create().build();
        try {
            TestPlan testPlan = TestUtil.loadTestPlan(resourcePath, cl);
            log(""); //$NON-NLS-1$
            log("-------------------------------------------------------------------------------"); //$NON-NLS-1$
            log("Executing Test Plan: " + resourcePath); //$NON-NLS-1$
            log("   Base API URL: " + baseApiUrl); //$NON-NLS-1$
            log("-------------------------------------------------------------------------------"); //$NON-NLS-1$
            log(""); //$NON-NLS-1$
            for (TestGroupType group : testPlan.getTestGroup()) {
                log("-----------------------------------------------------------"); //$NON-NLS-1$
                log("Starting Test Group [{0}]", group.getName()); //$NON-NLS-1$
                log("-----------------------------------------------------------"); //$NON-NLS-1$

                for (TestType test : group.getTest()) {
                    String rtPath = test.getValue();
                    log("Executing REST Test [{0}] - {1}", test.getName(), rtPath); //$NON-NLS-1$
                    RestTest restTest = TestUtil.loadRestTest(rtPath, cl);
                    runTest(restTest);
                    log("REST Test Completed"); //$NON-NLS-1$
                    log("+++++++++++++++++++"); //$NON-NLS-1$
                }

                log("Test Group [{0}] Completed Successfully", group.getName()); //$NON-NLS-1$
            }

            log(""); //$NON-NLS-1$
            log("-------------------------------------------------------------------------------"); //$NON-NLS-1$
            log("Test Plan successfully executed: " + resourcePath); //$NON-NLS-1$
            log("-------------------------------------------------------------------------------"); //$NON-NLS-1$
            log(""); //$NON-NLS-1$
        } finally {
            try {
                client.close();
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
            client = null;
        }
    }

    /**
     * Runs a single REST test.
     *
     * @param restTest
     */
    private void runTest(RestTest restTest) throws Error {
        try {
            String requestPath = TestUtil.doPropertyReplacement(restTest.getRequestPath());
            URI uri = getUri(requestPath);
            HttpRequestBase request = null;
            if (restTest.getRequestMethod().equalsIgnoreCase("GET")) { //$NON-NLS-1$
                request = new HttpGet();
            } else if (restTest.getRequestMethod().equalsIgnoreCase("POST")) { //$NON-NLS-1$
                request = new HttpPost();
                HttpEntity entity = new StringEntity(restTest.getRequestPayload());
                ((HttpPost) request).setEntity(entity);
            } else if (restTest.getRequestMethod().equalsIgnoreCase("PUT")) { //$NON-NLS-1$
                request = new HttpPut();
                HttpEntity entity = new StringEntity(restTest.getRequestPayload());
                ((HttpPut) request).setEntity(entity);
            } else if (restTest.getRequestMethod().equalsIgnoreCase("DELETE")) { //$NON-NLS-1$
                request = new HttpDelete();
            }
            if (request == null) {
                Assert.fail("Unsupported method in REST Test: " + restTest.getRequestMethod()); //$NON-NLS-1$
            }
            request.setURI(uri);

            Map<String, String> requestHeaders = restTest.getRequestHeaders();
            for (Entry<String, String> entry : requestHeaders.entrySet()) {
                request.setHeader(entry.getKey(), entry.getValue());
            }

            // Set up basic auth
            String authorization = createBasicAuthorization(restTest.getUsername(), restTest.getPassword());
            if (authorization != null) {
                request.setHeader("Authorization", authorization); //$NON-NLS-1$
            }

            HttpResponse response = client.execute(request);
            assertResponse(restTest, response);
        } catch (Error e) {
            logPlain("[ERROR] " + e.getMessage()); //$NON-NLS-1$
            throw e;
        } catch (Exception e) {
            throw new Error(e);
        }

    }

    /**
     * Create the basic auth header value.
     * @param username
     * @param password
     */
    private String createBasicAuthorization(String username, String password) {
        if (username == null || username.trim().length() == 0) {
            return null;
        }
        String val = username + ":" + password; //$NON-NLS-1$
        return "Basic " + Base64.encodeBase64String(val.getBytes()).trim(); //$NON-NLS-1$
    }

    /**
     * Assert that the response matched the expected.
     * @param restTest
     * @param response
     */
    private void assertResponse(RestTest restTest, HttpResponse response) {
        int actualStatusCode = response.getStatusLine().getStatusCode();
        try {
            Assert.assertEquals("Unexpected REST response status code.  Status message: " //$NON-NLS-1$
                    + response.getStatusLine().getReasonPhrase(), restTest.getExpectedStatusCode(),
                    actualStatusCode);
        } catch (Error e) {
            if (actualStatusCode >= 400) {
                try {
                    InputStream content = response.getEntity().getContent();
                    String payload = IOUtils.toString(content);
                    System.out.println("------ START ERROR PAYLOAD ------"); //$NON-NLS-1$
                    System.out.println(payload);
                    System.out.println("------ END   ERROR PAYLOAD ------"); //$NON-NLS-1$
                } catch (Exception e1) {
                }
            }
            throw e;
        }
        for (Entry<String, String> entry : restTest.getExpectedResponseHeaders().entrySet()) {
            String expectedHeaderName = entry.getKey();
            if (expectedHeaderName.startsWith("X-RestTest-")) //$NON-NLS-1$
                continue;
            String expectedHeaderValue = entry.getValue();
            Header header = response.getFirstHeader(expectedHeaderName);
            Assert.assertNotNull("Expected header to exist but was not found: " + expectedHeaderName, header); //$NON-NLS-1$
            String actualValue = header.getValue();
            Assert.assertEquals(expectedHeaderValue, actualValue);
        }
        Header ctHeader = response.getFirstHeader("Content-Type"); //$NON-NLS-1$
        if (ctHeader == null) {
            assertNoPayload(restTest, response);
        } else {
            String ct = ctHeader.getValue();
            if (ct.equals("application/json")) { //$NON-NLS-1$
                assertJsonPayload(restTest, response);
            } else if (ct.equals("text/plain")) { //$NON-NLS-1$
                assertTextPayload(restTest, response);
            } else {
                Assert.fail("Unsupported response payload type: " + ct); //$NON-NLS-1$
            }
        }
    }

    /**
     * Asserts that the response has no payload and that we are not expecting one.
     * @param restTest
     * @param response
     */
    private void assertNoPayload(RestTest restTest, HttpResponse response) {
        String expectedPayload = restTest.getExpectedResponsePayload();
        if (expectedPayload != null && expectedPayload.trim().length() > 0) {
            Assert.fail("Expected a payload but didn't get one."); //$NON-NLS-1$
        }
    }

    /**
     * Assume the payload is JSON and do some assertions based on the configuration
     * in the REST Test.
     * @param restTest
     * @param response
     */
    private void assertJsonPayload(RestTest restTest, HttpResponse response) {
        InputStream inputStream = null;
        try {
            inputStream = response.getEntity().getContent();
            ObjectMapper jacksonParser = new ObjectMapper();
            JsonNode actualJson = jacksonParser.readTree(inputStream);
            bindVariables(actualJson, restTest);
            String expectedPayload = TestUtil.doPropertyReplacement(restTest.getExpectedResponsePayload());
            Assert.assertNotNull("REST Test missing expected JSON payload.", expectedPayload); //$NON-NLS-1$
            JsonNode expectedJson = jacksonParser.readTree(expectedPayload);
            try {
                assertJson(restTest, expectedJson, actualJson);
            } catch (Error e) {
                System.out.println("--- START FAILED JSON PAYLOAD ---"); //$NON-NLS-1$
                System.out.println(actualJson.toString());
                System.out.println("--- END FAILED JSON PAYLOAD ---"); //$NON-NLS-1$
                throw e;
            }
        } catch (Exception e) {
            throw new Error(e);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
    }

    /**
     * Binds any variables found in the response JSON to system properties
     * so they can be used in later rest tests.
     * @param actualJson
     * @param restTest
     */
    private void bindVariables(JsonNode actualJson, RestTest restTest) {
        for (String headerName : restTest.getExpectedResponseHeaders().keySet()) {
            if (headerName.startsWith("X-RestTest-BindTo-")) { //$NON-NLS-1$
                String bindExpression = restTest.getExpectedResponseHeaders().get(headerName);
                String bindVarName = headerName.substring("X-RestTest-BindTo-".length()); //$NON-NLS-1$
                String bindValue = evaluate(bindExpression, actualJson);
                log("-- Binding value in response --"); //$NON-NLS-1$
                log("\tExpression: " + bindExpression); //$NON-NLS-1$
                log("\t    To Var: " + bindVarName); //$NON-NLS-1$
                log("\t New Value: " + bindValue); //$NON-NLS-1$
                if (bindValue == null) {
                    System.clearProperty(bindVarName);
                } else {
                    System.setProperty(bindVarName, bindValue);
                }
            }
        }
    }

    /**
     * Evaluates the given expression against the given JSON object.
     * 
     * @param bindExpression
     * @param json
     */
    private String evaluate(String bindExpression, final JsonNode json) {
        PropertyHandlerFactory.registerPropertyHandler(ObjectNode.class, new PropertyHandler() {
            @Override
            public Object setProperty(String name, Object contextObj, VariableResolverFactory variableFactory,
                    Object value) {
                throw new RuntimeException("Not supported!"); //$NON-NLS-1$
            }

            @Override
            public Object getProperty(String name, Object contextObj, VariableResolverFactory variableFactory) {
                ObjectNode node = (ObjectNode) contextObj;
                TestVariableResolver resolver = new TestVariableResolver(node, name);
                return resolver.getValue();
            }
        });
        return String.valueOf(MVEL.eval(bindExpression, new TestVariableResolverFactory(json)));
    }

    /**
     * Asserts that the JSON payload matches what we expected, as defined
     * in the configuration of the rest test.
     * @param restTest
     * @param expectedJson
     * @param actualJson
     */
    private void assertJson(RestTest restTest, JsonNode expectedJson, JsonNode actualJson) {
        if (expectedJson instanceof ArrayNode) {
            JsonNode actualValue = actualJson;
            ArrayNode expectedArray = (ArrayNode) expectedJson;
            Assert.assertEquals("Expected JSON array but found non-array [" //$NON-NLS-1$
                    + actualValue.getClass().getSimpleName() + "] instead.", expectedJson.getClass(), //$NON-NLS-1$
                    actualValue.getClass());
            ArrayNode actualArray = (ArrayNode) actualValue;
            Assert.assertEquals("Array size mismatch.", expectedArray.size(), actualArray.size()); //$NON-NLS-1$
            String ordering = restTest.getExpectedResponseHeaders().get("X-RestTest-ArrayOrdering"); //$NON-NLS-1$

            JsonNode[] expected = new JsonNode[expectedArray.size()];
            JsonNode[] actual = new JsonNode[actualArray.size()];
            for (int idx = 0; idx < expected.length; idx++) {
                expected[idx] = expectedArray.get(idx);
                actual[idx] = actualArray.get(idx);
            }
            // If strict ordering is disabled, then sort both arrays
            if ("any".equals(ordering)) { //$NON-NLS-1$
                Comparator<? super JsonNode> comparator = new Comparator<JsonNode>() {
                    @Override
                    public int compare(JsonNode o1, JsonNode o2) {
                        int cmp = o1.toString().compareTo(o2.toString());
                        if (cmp == 0)
                            cmp = 1;
                        return cmp;
                    }
                };
                Arrays.sort(expected, comparator);
                Arrays.sort(actual, comparator);
            }
            for (int idx = 0; idx < expected.length; idx++) {
                assertJson(restTest, expected[idx], actual[idx]);
            }
        } else {
            Iterator<Entry<String, JsonNode>> fields = expectedJson.getFields();
            while (fields.hasNext()) {
                Entry<String, JsonNode> entry = fields.next();
                String expectedFieldName = entry.getKey();
                JsonNode expectedValue = entry.getValue();
                if (expectedValue instanceof TextNode) {
                    TextNode tn = (TextNode) expectedValue;
                    String expected = tn.getTextValue();
                    JsonNode actualValue = actualJson.get(expectedFieldName);
                    Assert.assertNotNull("Expected JSON text field '" + expectedFieldName + "' with value '" //$NON-NLS-1$ //$NON-NLS-2$
                            + expected + "' but was not found.", actualValue); //$NON-NLS-1$
                    Assert.assertEquals("Expected JSON text field '" + expectedFieldName + "' with value '" //$NON-NLS-1$ //$NON-NLS-2$
                            + expected + "' but found non-text [" + actualValue.getClass().getSimpleName() //$NON-NLS-1$
                            + "] field with that name instead.", TextNode.class, actualValue.getClass()); //$NON-NLS-1$
                    String actual = ((TextNode) actualValue).getTextValue();
                    Assert.assertEquals("Value mismatch for text field '" + expectedFieldName + "'.", expected, //$NON-NLS-1$ //$NON-NLS-2$
                            actual);
                } else if (expectedValue instanceof NumericNode) {
                    NumericNode numeric = (NumericNode) expectedValue;
                    Number expected = numeric.getNumberValue();
                    JsonNode actualValue = actualJson.get(expectedFieldName);
                    Assert.assertNotNull("Expected JSON numeric field '" + expectedFieldName + "' with value '" //$NON-NLS-1$ //$NON-NLS-2$
                            + expected + "' but was not found.", actualValue); //$NON-NLS-1$
                    Assert.assertEquals("Expected JSON numeric field '" + expectedFieldName + "' with value '" //$NON-NLS-1$ //$NON-NLS-2$
                            + expected + "' but found non-numeric [" + actualValue.getClass().getSimpleName() //$NON-NLS-1$
                            + "] field with that name instead.", expectedValue.getClass(), actualValue.getClass()); //$NON-NLS-1$
                    Number actual = ((NumericNode) actualValue).getNumberValue();
                    Assert.assertEquals("Value mismatch for numeric field '" + expectedFieldName + "'.", expected, //$NON-NLS-1$ //$NON-NLS-2$
                            actual);
                } else if (expectedValue instanceof BooleanNode) {
                    BooleanNode bool = (BooleanNode) expectedValue;
                    Boolean expected = bool.getBooleanValue();
                    JsonNode actualValue = actualJson.get(expectedFieldName);
                    Assert.assertNotNull("Expected JSON boolean field '" + expectedFieldName + "' with value '" //$NON-NLS-1$ //$NON-NLS-2$
                            + expected + "' but was not found.", actualValue); //$NON-NLS-1$
                    Assert.assertEquals("Expected JSON boolean field '" + expectedFieldName + "' with value '" //$NON-NLS-1$ //$NON-NLS-2$
                            + expected + "' but found non-boolean [" + actualValue.getClass().getSimpleName() //$NON-NLS-1$
                            + "] field with that name instead.", expectedValue.getClass(), actualValue.getClass()); //$NON-NLS-1$
                    Boolean actual = ((BooleanNode) actualValue).getBooleanValue();
                    Assert.assertEquals("Value mismatch for boolean field '" + expectedFieldName + "'.", expected, //$NON-NLS-1$ //$NON-NLS-2$
                            actual);
                } else if (expectedValue instanceof ObjectNode) {
                    JsonNode actualValue = actualJson.get(expectedFieldName);
                    Assert.assertNotNull("Expected parent JSON field '" + expectedFieldName //$NON-NLS-1$
                            + "' but was not found.", actualValue); //$NON-NLS-1$
                    Assert.assertEquals("Expected parent JSON field '" + expectedFieldName //$NON-NLS-1$
                            + "' but found field of type '" + actualValue.getClass().getSimpleName() + "'.", //$NON-NLS-1$ //$NON-NLS-2$
                            ObjectNode.class, actualValue.getClass());
                    assertJson(restTest, expectedValue, actualValue);
                } else if (expectedValue instanceof ArrayNode) {
                    JsonNode actualValue = actualJson.get(expectedFieldName);
                    Assert.assertNotNull("Expected JSON array field '" + expectedFieldName //$NON-NLS-1$
                            + "' but was not found.", actualValue); //$NON-NLS-1$
                    ArrayNode expectedArray = (ArrayNode) expectedValue;
                    Assert.assertEquals("Expected JSON array field '" + expectedFieldName //$NON-NLS-1$
                            + "' but found non-array [" + actualValue.getClass().getSimpleName() //$NON-NLS-1$
                            + "] field with that name instead.", expectedValue.getClass(), actualValue.getClass()); //$NON-NLS-1$
                    ArrayNode actualArray = (ArrayNode) actualValue;
                    Assert.assertEquals("Field '" + expectedFieldName + "' array size mismatch.", //$NON-NLS-1$ //$NON-NLS-2$
                            expectedArray.size(), actualArray.size());
                    assertJson(restTest, expectedArray, actualArray);
                } else if (expectedValue instanceof NullNode) {
                    JsonNode actualValue = actualJson.get(expectedFieldName);
                    Assert.assertNotNull("Expected Null JSON field '" + expectedFieldName //$NON-NLS-1$
                            + "' but was not found.", actualValue); //$NON-NLS-1$
                    Assert.assertEquals("Expected Null JSON field '" + expectedFieldName //$NON-NLS-1$
                            + "' but found field of type '" + actualValue.getClass().getSimpleName() + "'.", //$NON-NLS-1$ //$NON-NLS-2$
                            NullNode.class, actualValue.getClass());
                } else {
                    Assert.fail("Unsupported field type: " + expectedValue.getClass().getSimpleName()); //$NON-NLS-1$
                }
            }
        }
    }

    /**
     * Assume the payload is Text and do some assertions based on the configuration
     * in the REST Test.
     * @param restTest
     * @param response
     */
    private void assertTextPayload(RestTest restTest, HttpResponse response) {
        InputStream inputStream = null;
        try {
            inputStream = response.getEntity().getContent();
            List<String> lines = IOUtils.readLines(inputStream);
            StringBuilder builder = new StringBuilder();
            for (String line : lines) {
                builder.append(line).append("\n"); //$NON-NLS-1$
            }

            String actual = builder.toString();
            String expected = restTest.getExpectedResponsePayload();
            Assert.assertEquals("Response payload (text/plain) mismatch.", expected, actual); //$NON-NLS-1$
        } catch (Exception e) {
            throw new Error(e);
        } finally {
            IOUtils.closeQuietly(inputStream);
        }
    }

    /**
     * Gets the absolute URL to use to invoke a rest service at a given path.
     * @param path
     * @throws URISyntaxException
     */
    public URI getUri(String path) throws URISyntaxException {
        return new URI(baseApiUrl + path);
    }

    /**
     * Logs a message.
     *
     * @param message
     * @param params
     */
    private void log(String message, Object... params) {
        String outmsg = MessageFormat.format(message, params);
        logger.info("    >> " + outmsg); //$NON-NLS-1$
    }

    /**
     * Logs a message.
     *
     * @param message
     */
    private void logPlain(String message) {
        logger.info("    >> " + message); //$NON-NLS-1$
    }

}