/*
* Weblounge: Web Content Management System
* Copyright (c) 2003 - 2011 The Weblounge Team
* http://entwinemedia.com/weblounge
*
* This program is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public License
* as published by the Free Software Foundation; either version 2
* of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation
* Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package ch.entwine.weblounge.common.impl.testing;
import ch.entwine.weblounge.common.impl.util.TestUtils;
import ch.entwine.weblounge.common.impl.util.xml.XPathHelper;
import ch.entwine.weblounge.common.url.UrlUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.http.Header;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This integration test is automatically created from a test definition using
* the {@link IntegrationTestParser}.
*/
public class IntegrationTestCase {
/** Logging facility */
private static final Logger logger = LoggerFactory.getLogger(IntegrationTestCase.class);
/** Name of the test case */
protected String name = null;
/** The request url and query */
protected String path = null;
/** The list of assertions */
protected List<IntegrationTestCaseAssertion> assertions = new ArrayList<IntegrationTestCaseAssertion>();
/** The request parameters */
protected Map<String, String[]> parameters = new HashMap<String, String[]>();
/**
* Creates a test case with the given name and url. The parameters will be
* added to the request, <code>null</code> is an acceptable value if no
* parameters are needed.
*
* @param name
* the test name
* @param path
* the test path
* @param the
* query parameters
* @throws IllegalArgumentException
* if <code>name</code> or <code>url</code> are empty
*/
public IntegrationTestCase(String name, String path,
Map<String, String[]> parameters) {
if (StringUtils.isBlank(name))
throw new IllegalArgumentException("Name of test case cannot be empty");
if (StringUtils.isBlank(path))
throw new IllegalArgumentException("Url cannot be empty");
this.name = name;
this.path = path;
this.parameters = parameters;
}
/**
* Executes this test case using the given server url. The server url will be
* prepended to the test case's url and then used in an http <code>GET</code>
* request. The response is then analyzed using the given assertions.
*/
public void execute(String serverUrl) throws Exception {
HttpGet request = new HttpGet(UrlUtils.concat(serverUrl, path));
String[][] params = new String[][] {};
if (this.parameters != null) {
int parameterCount = 0;
for (String[] parameterValues : this.parameters.values()) {
parameterCount += parameterValues.length;
}
params = new String[parameterCount][2];
int i = 0;
for (Map.Entry<String, String[]> param : this.parameters.entrySet()) {
for (String value : param.getValue()) {
params[i][0] = param.getKey();
params[i][1] = value;
i++;
}
}
}
// Send and the request and examine the response
logger.debug("Sending request to {}", request.getURI());
HttpClient httpClient = new DefaultHttpClient();
try {
HttpResponse response = TestUtils.request(httpClient, request, params);
// Prepare status code and response body
int code = response.getStatusLine().getStatusCode();
Document xml = TestUtils.parseXMLResponse(response);
// Prepare the headers collection
Map<String, String> headers = new HashMap<String, String>();
Header[] responseHeaders = response.getAllHeaders();
if (responseHeaders != null) {
for (Header header : responseHeaders) {
headers.put(header.getName(), header.getValue());
}
}
// Verify the assertions
for (IntegrationTestCaseAssertion assertion : assertions) {
assertion.verify(code, headers, xml);
}
} finally {
httpClient.getConnectionManager().shutdown();
}
}
/**
* Returns the assertions that have been registered.
*
* @return the assertions
*/
public List<IntegrationTestCaseAssertion> getAssertions() {
return assertions;
}
/**
* Returns the request parameters.
*
* @return the parameters
*/
public Map<String, String[]> getParameters() {
return parameters;
}
/**
* Tests if the http response code matches any of the given values. By
* default, the test requires a status code of <code>200</code>.
*
* @param statusCodes
* the acceptable status codes
* @throws IllegalArgumentException
* if no status codes are given
*/
public void assertResponseStatus(int[] statusCodes)
throws IllegalArgumentException {
if (statusCodes == null || statusCodes.length == 0)
throw new IllegalArgumentException("At least one code si required");
assertions.add(new StatusCodeAssertion(statusCodes));
}
/**
* Adds a test that asserts that the given path exists and does not contain an
* empty element.
*
* @param xpath
* path to the element in question
* @throws IllegalArgumentException
* if <code>xpath</code> is blank
*/
public void assertExists(String xpath) throws IllegalArgumentException {
if (StringUtils.isBlank(xpath))
throw new IllegalArgumentException("Path must not be blank");
assertions.add(new ExistenceAssertion(xpath, true));
}
/**
* Adds a test that asserts that there is no (or an empty) element at the
* given path.
*
* @param xpath
* path to the element in question
* @throws IllegalArgumentException
* if <code>xpath</code> is blank
*/
public void assertNotExists(String xpath) throws IllegalArgumentException {
if (StringUtils.isBlank(xpath))
throw new IllegalArgumentException("Path must not be blank");
assertions.add(new ExistenceAssertion(xpath, false));
}
/**
* Adds a test that asserts that there is an element at the given path and
* that the element content matches the given value.
*
* @param xpath
* path to the element in question
* @param value
* the value
* @param ignoreWhitespace
* <code>true</code> to ignore whitespace when matching
* @param ignoreCase
* <code>true</code> to ignore character case when matching
* @param regularExpression
* <code>true</code> to treat <code>value</code> as a regular
* expression
* @throws IllegalArgumentException
* if <code>xpath</code> is blank
*/
public void assertEquals(String xpath, String value,
boolean ignoreWhitespace, boolean ignoreCase, boolean regularExpression)
throws IllegalArgumentException {
if (StringUtils.isBlank(xpath))
throw new IllegalArgumentException("Path must not be blank");
if (StringUtils.isBlank(value))
throw new IllegalArgumentException("Value must not be blank");
assertions.add(new EqualityAssertion(xpath, value, ignoreWhitespace, ignoreCase, regularExpression, true));
}
/**
* Adds a test that asserts that there is an element at the given path and
* that the element content does not matches the given value.
*
* @param xpath
* path to the element in question
* @param value
* the value
* @param ignoreWhitespace
* <code>true</code> to ignore whitespace when matching
* @param ignoreCase
* <code>true</code> to ignore character case when matching
* @param regularExpression
* <code>true</code> to treat <code>value</code> as a regular
* expression
* @throws IllegalArgumentException
* if <code>xpath</code> is blank
*/
public void assertNotEquals(String xpath, String value,
boolean ignoreWhitespace, boolean ignoreCase, boolean regularExpression)
throws IllegalArgumentException {
if (StringUtils.isBlank(xpath))
throw new IllegalArgumentException("Path must not be blank");
if (StringUtils.isBlank(value))
throw new IllegalArgumentException("Value must not be blank");
assertions.add(new EqualityAssertion(xpath, value, ignoreWhitespace, ignoreCase, regularExpression, false));
}
/**
* {@inheritDoc}
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return name != null ? name : super.toString();
}
/**
* Implementation of an assertion that tests the status code to be in a
* predefined list of codes.
*/
public class StatusCodeAssertion implements IntegrationTestCaseAssertion {
/** The list of acceptable status codes */
protected List<Integer> statusCodes = new ArrayList<Integer>();
/**
* Creates an assertion that verifies whether the response status matches
* one of the status codes given.
*
* @param statusCodes
* the expected status codes
*/
StatusCodeAssertion(int[] statusCodes) {
for (int code : statusCodes) {
this.statusCodes.add(code);
}
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.testing.IntegrationTestCaseAssertion#verify(int,
* java.util.Map, org.w3c.dom.Node)
*/
public void verify(int statusCode, Map<String, String> headers,
Node response) throws Exception {
if (!statusCodes.contains(statusCode))
throw new IllegalStateException("Unexpected response code " + statusCode);
}
/**
* Returns a list of expected status codes.
*
* @return the status codes
*/
public List<Integer> getExpectedCodes() {
return statusCodes;
}
}
/**
* Assertion that will verify that the response contains given
* <code>xpath</code> expression.
*/
public class ExistenceAssertion implements IntegrationTestCaseAssertion {
/** The path to test for */
private String xpath = null;
/** <code>true</code> to test for existence */
private boolean testPositive = true;
/**
* Creates a new assertion that tests for existence (
* <code>testPositive</code> is <code>true</code>) or non-existence (
* <code>testPositive</code> is <code>false</code>) of the element defined
* by <code>xpath</code>.
*
* @param xpath
* the xpath to the element
* @param testPositive
* <code>true</code> to test for existence, <code>false</code> for
* non-existence
*/
ExistenceAssertion(String xpath, boolean testPositive) {
this.xpath = xpath;
this.testPositive = testPositive;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.testing.IntegrationTestCaseAssertion#verify(int,
* java.util.Map, org.w3c.dom.Node)
*/
public void verify(int statusCode, Map<String, String> headers,
Node response) throws Exception {
boolean isBlank = StringUtils.isBlank(XPathHelper.valueOf(response, xpath));
if (testPositive && isBlank)
throw new IllegalStateException("Expected content at " + xpath + " not found");
else if (!testPositive && !isBlank)
throw new IllegalStateException("Found unexpected content at " + xpath);
}
/**
* Returns the xpath expression.
*
* @return the xpath
*/
public String getXPath() {
return xpath;
}
/**
* Returns <code>true</code> if the test is testing for positive outcome.
*
* @return <code>true</code> for testing of positive outcome
*/
public boolean isPositive() {
return testPositive;
}
}
/**
* Assertion that will verify that the response contains an element at the
* given <code>xpath</code> expression that matches an expected value.
*/
public class EqualityAssertion implements IntegrationTestCaseAssertion {
/** The path to test for */
private String xpath = null;
/** The value to look for */
private String expectedValue = null;
/** True to ignore whitespace */
private boolean ignoreWhitespace = true;
/** True to ignore the case when matching */
private boolean ignoreCase = true;
/** True to match using regular expressions */
private boolean regularExpression = false;
/** <code>true</code> to test for existence */
private boolean testPositive = true;
/**
* Creates a new assertion that tests for equality (
* <code>testPositive</code> is <code>true</code>) or mismatch (
* <code>testPositive</code> is <code>false</code>) of the element defined
* by <code>xpath</code>.
*
* @param xpath
* the xpath to the element
* @param value
* the content to match
* @param ignoreWhitespace
* <code>true</code> to ignore whitespace when matching
* @param ignoreCase
* <code>true</code> to ignore case when matching
* @param regularExpression
* <code>true</code> to compare using regular expressions
* @param testPositive
* <code>true</code> to test for equality, <code>false</code> for
* mismatch
*/
EqualityAssertion(String xpath, String value, boolean ignoreWhitespace,
boolean ignoreCase, boolean regularExpression, boolean testPositive) {
this.xpath = xpath;
this.ignoreWhitespace = ignoreWhitespace;
this.ignoreCase = ignoreCase;
this.regularExpression = regularExpression;
this.testPositive = testPositive;
this.expectedValue = value;
}
/**
* {@inheritDoc}
*
* @see ch.entwine.weblounge.common.impl.testing.IntegrationTestCaseAssertion#verify(int,
* java.util.Map, org.w3c.dom.Node)
*/
public void verify(int statusCode, Map<String, String> headers,
Node response) throws Exception {
String actualValue = XPathHelper.valueOf(response, xpath);
String expected = this.expectedValue;
String found = actualValue;
if (actualValue == null)
throw new IllegalStateException("Expected content at " + xpath + " not found");
if (ignoreWhitespace) {
found = StringUtils.deleteWhitespace(found);
expected = StringUtils.deleteWhitespace(expected);
}
boolean matches = false;
if (regularExpression) {
int flags = ignoreCase ? Pattern.CASE_INSENSITIVE : 0;
Pattern p = Pattern.compile(expected, flags);
Matcher m = p.matcher(found);
matches = m.matches();
} else {
matches = ignoreCase ? found.equalsIgnoreCase(expected) : found.equals(expected);
}
if (testPositive && !matches)
throw new IllegalStateException("Expected '" + this.expectedValue + "' at " + xpath + " but found '" + actualValue + "'");
if (!testPositive && matches)
throw new IllegalStateException("Found unexpected content '" + actualValue + "' at " + xpath);
}
/**
* Returns the xpath expression.
*
* @return the xpath
*/
public String getXPath() {
return xpath;
}
/**
* Returns <code>true</code> if the test should ignore whitespace.
*
* @return <code>true</code> if the test ignores whitespace
*/
public boolean ignoreWhitespace() {
return ignoreWhitespace;
}
/**
* Returns <code>true</code> if the test should ignore case.
*
* @return <code>true</code> if the test ignores case
*/
public boolean ignoreCase() {
return ignoreCase;
}
/**
* Returns <code>true</code> if the test should be made using regular
* expression logic.
*
* @return <code>true</code> if the test is conducted using regular
* expression logic
*/
public boolean regularExpression() {
return regularExpression;
}
/**
* Returns <code>true</code> if the test is testing for positive outcome.
*
* @return <code>true</code> for testing of positive outcome
*/
public boolean isPositive() {
return testPositive;
}
/**
* Returns the expected value.
*
* @return the expected value
*/
public String getExpectedValue() {
return expectedValue;
}
}
}