/**
* Copyright (C) 2017 Alfresco Software Limited.
* <p/>
* This file is part of the Alfresco SDK project.
* <p/>
* 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
* <p/>
* http://www.apache.org/licenses/LICENSE-2.0
* <p/>
* 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 org.alfresco.rad.test;
import org.alfresco.rad.SpringContextHolder;
import org.apache.commons.codec.binary.Base64;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.CredentialsProvider;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.junit.Ignore;
import org.junit.runner.Description;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
/**
* This is a JUnit test runner that is designed to work with an Alfresco repository.
* It detects if it's executing a test inside of a running Alfresco instance. If that
* is the case the tests are all run normally. If however the test is being run from
* outside the repository, from the maven command line or from an IDE
* such as IntelliJ or STS/Eclipse for example, then instead of running the actual
* test an HTTP request is made to a Web Script in a running Alfresco instance. This
* Web Script runs the test and returns enough information to this class so we can
* emulate having run the test locally.
* <p/>
* By doing this, we are able to create Integration Tests (IT) using standard JUnit
* capabilities. These can then be run from our IDEs with the associated visualizations,
* support for re-running failed tests, etc.
* <p/>
* Integration testing framework donated by Zia Consulting
*
* @author Bindu Wavell <bindu@ziaconsulting.com>
* @author martin.bergljung@alfresco.com (some editing)
* @since 3.0
*/
public class AlfrescoTestRunner extends BlockJUnit4ClassRunner {
public static final String SUCCESS = "SUCCESS";
public static final String FAILURE = "FAILURE";
public AlfrescoTestRunner(Class<?> klass) throws InitializationError {
super(klass);
}
public static String serializableToString(Serializable serializable) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(baos);
oos.writeObject(serializable);
oos.close();
String string = Base64.encodeBase64URLSafeString(baos.toByteArray());
return string;
}
@Override
protected void runChild(FrameworkMethod method, RunNotifier notifier) {
if (areWeRunningInAlfresco()) {
// Just run the test as normally
super.runChild(method, notifier);
} else {
// We are not running in an Alfresco Server, we need to call one and have it execute the test...
Description desc = describeChild(method);
if (method.getAnnotation(Ignore.class) != null) {
notifier.fireTestIgnored(desc);
} else {
callProxiedChild(method, notifier, desc);
}
}
}
/**
* Call a remote Alfresco server and have the test run there.
*
* @param method the test method to run
* @param notifier
* @param desc
*/
protected void callProxiedChild(FrameworkMethod method, RunNotifier notifier, Description desc) {
notifier.fireTestStarted(desc);
String className = method.getMethod().getDeclaringClass().getCanonicalName();
String methodName = method.getName();
if (null != methodName) {
className += "#" + methodName;
}
// Login credentials for Alfresco Repo
// TODO: Maybe configure credentials in props...
CredentialsProvider provider = new BasicCredentialsProvider();
UsernamePasswordCredentials credentials = new UsernamePasswordCredentials("admin", "admin");
provider.setCredentials(AuthScope.ANY, credentials);
// Create HTTP Client with credentials
CloseableHttpClient httpclient = HttpClientBuilder.create()
.setDefaultCredentialsProvider(provider)
.build();
// Create the GET Request for the Web Script that will run the test
String testWebScriptUrl = "/service/testing/test.xml?clazz=" + className.replace("#", "%23");
//System.out.println("AlfrescoTestRunner: Invoking Web Script for test execution: " + testWebScriptUrl);
HttpGet get = new HttpGet(getContextRoot(method) + testWebScriptUrl);
try {
// Send proxied request and read response
HttpResponse resp = httpclient.execute(get);
InputStream is = resp.getEntity().getContent();
InputStreamReader ir = new InputStreamReader(is);
BufferedReader br = new BufferedReader(ir);
String body = "";
String line;
while ((line = br.readLine()) != null) {
body += line + "\n";
}
// Process response
if (body.length() > 0) {
DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = null;
dBuilder = dbFactory.newDocumentBuilder();
Document doc = dBuilder.parse(new InputSource(new StringReader(body)));
Element root = doc.getDocumentElement();
NodeList results = root.getElementsByTagName("result");
if (null != results && results.getLength() > 0) {
String result = results.item(0).getFirstChild().getNodeValue();
if (SUCCESS.equals(result)) {
notifier.fireTestFinished(desc);
} else {
boolean failureFired = false;
NodeList throwableNodes = root.getElementsByTagName("throwable");
for (int tid = 0; tid < throwableNodes.getLength(); tid++) {
String throwableBody = null;
Object object = null;
Throwable throwable = null;
throwableBody = throwableNodes.item(tid).getFirstChild().getNodeValue();
if (null != throwableBody) {
try {
object = objectFromString(throwableBody);
} catch (ClassNotFoundException e) {
}
if (null != object && object instanceof Throwable) {
throwable = (Throwable) object;
}
}
if (null == throwable) {
throwable = new Throwable("Unable to process exception body: " + throwableBody);
}
notifier.fireTestFailure(new Failure(desc, throwable));
failureFired = true;
}
if (!failureFired) {
notifier.fireTestFailure(new Failure(desc, new Throwable(
"There was an error but we can't figure out what it was, sorry!")));
}
}
} else {
notifier.fireTestFailure(new Failure(desc, new Throwable(
"Unable to process response for proxied test request: " + body)));
}
} else {
notifier.fireTestFailure(new Failure(desc, new Throwable(
"Attempt to proxy test into running Alfresco instance failed, no response received")));
}
} catch (IOException e) {
notifier.fireTestFailure(new Failure(desc, e));
} catch (ParserConfigurationException e) {
notifier.fireTestFailure(new Failure(desc, e));
} catch (SAXException e) {
notifier.fireTestFailure(new Failure(desc, e));
} finally {
try {
httpclient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
protected static Object objectFromString(String string) throws IOException, ClassNotFoundException {
byte[] buffer = Base64.decodeBase64(string);
ObjectInputStream ois = new ObjectInputStream(
new ByteArrayInputStream(buffer));
Object object = ois.readObject();
ois.close();
return object;
}
/**
* Check if we are running this test in an Alfresco server instance.
*
* @return true if we are running in an Alfresco server
*/
protected boolean areWeRunningInAlfresco() {
Object contextHolder = SpringContextHolder.Instance();
return (contextHolder != null);
}
/**
* Check the @Remote config on the test class to see where the
* Alfresco Repo is running
*
* @param method
* @return
*/
protected String getContextRoot(FrameworkMethod method) {
Class<?> declaringClass = method.getMethod().getDeclaringClass();
boolean annotationPresent = declaringClass.isAnnotationPresent(Remote.class);
if (annotationPresent) {
Remote annotation = declaringClass.getAnnotation(Remote.class);
return annotation.endpoint();
}
return "http://localhost:8080/alfresco";
}
}