[maven-release-plugin]  copy for tag org.apache.sling.commons.testing-2.0.18

git-svn-id: https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.commons.testing-2.0.18@1672592 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/testing/README.txt b/testing/README.txt
new file mode 100644
index 0000000..5642f4c
--- /dev/null
+++ b/testing/README.txt
@@ -0,0 +1,26 @@
+Apache Sling Testing Utilities
+
+Tools and utilities for automated testing of Sling modules
+
+Getting Started
+===============
+
+This component uses a Maven 2 (http://maven.apache.org/) build
+environment. It requires a Java 5 JDK (or higher) and Maven (http://maven.apache.org/)
+2.0.7 or later. We recommend to use the latest Maven version.
+
+If you have Maven 2 installed, you can compile and
+package the jar using the following command:
+
+    mvn package
+
+See the Maven 2 documentation for other build features.
+
+The latest source code for this component is available in the
+Subversion (http://subversion.tigris.org/) source repository of
+the Apache Software Foundation. If you have Subversion installed,
+you can checkout the latest source using the following command:
+
+    svn checkout http://svn.apache.org/repos/asf/sling/trunk/commons/testing
+
+See the Subversion documentation for other source control features.
diff --git a/testing/pom.xml b/testing/pom.xml
new file mode 100644
index 0000000..c9e2926
--- /dev/null
+++ b/testing/pom.xml
@@ -0,0 +1,162 @@
+<?xml version="1.0" encoding="ISO-8859-1"?>
+<!--
+    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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>22</version>
+        <relativePath>../../../parent/pom.xml</relativePath>
+    </parent>
+
+    <artifactId>org.apache.sling.commons.testing</artifactId>
+    <packaging>jar</packaging>
+    <version>2.0.18</version>
+
+    <name>Apache Sling Testing Utilities</name>
+    <description>
+        Tools and utilities for automated testing of Sling modules
+    </description>
+
+    <scm>
+        <connection>
+            scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.commons.testing-2.0.18
+        </connection>
+        <developerConnection>
+            scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.commons.testing-2.0.18
+        </developerConnection>
+        <url>
+            http://svn.apache.org/viewvc/sling/tags/org.apache.sling.commons.testing-2.0.18
+        </url>
+    </scm>
+
+    <dependencies>
+        <dependency>
+            <groupId>javax.servlet</groupId>
+            <artifactId>servlet-api</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.compendium</artifactId>
+        </dependency>
+       
+        <!--
+            Dependencies required for testing and provided
+            transitively to modules using this artifact
+        -->
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.api</artifactId>
+            <version>2.4.2</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.jcr.api</artifactId>
+            <version>2.0.6</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>javax.jcr</groupId>
+            <artifactId>jcr</artifactId>
+            <version>2.0</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-api</artifactId>
+            <version>2.2.9</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-jcr-commons</artifactId>
+            <version>2.2.9</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.jackrabbit</groupId>
+            <artifactId>jackrabbit-core</artifactId>
+            <version>2.2.9</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-io</groupId>
+            <artifactId>commons-io</artifactId>
+            <version>1.4</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.adapter</artifactId>
+            <version>2.0.4</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>rhino</groupId>
+            <artifactId>js</artifactId>
+            <version>1.6R6</version>
+            <scope>compile</scope>
+        </dependency>
+        <dependency>
+            <groupId>commons-httpclient</groupId>
+            <artifactId>commons-httpclient</artifactId>
+            <version>3.1</version>
+            <scope>compile</scope>
+            <exclusions>
+                <!-- 
+                    Same problem as JCR-683: commons-httpclient depends on commons-logging. Since 
+                    this webapp uses log4j (see the slf4j-log4j12 dependency below), we need to
+                    override this dependency with jcl-over-sflf4j, found below
+                -->
+                <exclusion>
+                    <groupId>commons-logging</groupId>
+                    <artifactId>commons-logging</artifactId>
+                </exclusion>
+            </exclusions>
+        </dependency>
+        
+        <!-- Logging for tests -->
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        
+        <!-- Basic Test Controller JUnit -->
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <scope>compile</scope>
+        </dependency>
+        
+        <!-- Provide for Test Mocks -->
+        <dependency>
+            <groupId>org.jmock</groupId>
+            <artifactId>jmock-junit4</artifactId>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpAnyMethod.java b/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpAnyMethod.java
new file mode 100644
index 0000000..7403806
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpAnyMethod.java
@@ -0,0 +1,34 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.integration;
+
+import org.apache.commons.httpclient.HttpMethodBase;
+
+/** Allows any HTTP method for HtttpClient */
+public class HttpAnyMethod extends HttpMethodBase {
+    private final String methodName;
+    
+    public HttpAnyMethod(String methodName, String uri) {
+        super(uri);
+        this.methodName = methodName;
+    }
+
+    @Override
+    public String getName() {
+        return methodName;
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpStatusCodeException.java b/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpStatusCodeException.java
new file mode 100644
index 0000000..b5f6676
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpStatusCodeException.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.integration;
+
+import java.io.IOException;
+
+@SuppressWarnings("serial")
+public class HttpStatusCodeException extends IOException {
+
+    private final int expectedStatus;
+    
+    private final int actualStatus;
+    
+    public HttpStatusCodeException(int expectedStatus, int actualStatus,
+            String method, String url) {
+        this(expectedStatus, actualStatus, method, url, null);
+    }
+
+    public HttpStatusCodeException(int expectedStatus, int actualStatus,
+            String method, String url, String content) {
+        super("Expected status code " + expectedStatus + " for " + method
+                + ", got " + actualStatus + ", URL=" + url 
+                + (content != null ? ", Content=[" + content + "]" : "")
+                );
+        this.expectedStatus = expectedStatus;
+        this.actualStatus = actualStatus;
+    }
+
+    public int getExpectedStatus() {
+        return expectedStatus;
+    }
+    
+    public int getActualStatus() {
+        return actualStatus;
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpTest.java b/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpTest.java
new file mode 100644
index 0000000..bd6544b
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpTest.java
@@ -0,0 +1,79 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.integration;
+
+import java.io.IOException;
+
+import org.apache.commons.httpclient.HttpClient;
+
+/** Helper class for HTTP tests, extends HttpTestBase and adds
+ *  a few utilities that we commonly use in our integration tests.
+ *  
+ *  Meant to be used as a helper class in JUnit4-style tests, as we
+ *  gradually move away from JUnit3 style.  
+ */
+public class HttpTest extends HttpTestBase {
+    protected String scriptPath;
+    protected String testText;
+    protected String displayUrl;
+    
+    public String uploadTestScript(String localFilename,String filenameOnServer) throws IOException {
+        return uploadTestScript(scriptPath, localFilename, filenameOnServer);
+    }
+    
+    public static void assertContains(String content, String expected) {
+        if(!content.contains(expected)) {
+            fail("Content does not contain '" + expected + "' (content=" + content + ")");
+        }
+    }
+    
+    public static void assertNotContains(String content, String notExpected) {
+        if(content.contains(notExpected)) {
+            fail("Content contains '" + notExpected + "' (content=" + content + ")");
+        }
+    }
+    
+    public void setScriptPath(String scriptPath) {
+        this.scriptPath = scriptPath;
+    }
+    
+    public String getScriptPath() {
+        return scriptPath;
+    }
+    
+    public SlingIntegrationTestClient getTestClient() {
+        return testClient;
+    }
+    
+    public HttpClient getHttpClient() {
+        return httpClient;
+    }
+    
+    /** Making this public here, changing the base class to public is not convenient
+     *  as many derived classes override it as protected.
+     */
+    public void setUp() throws Exception {
+        super.setUp();
+    }
+    
+    /** Making this public here, changing the base class to public is not convenient
+     *  as many derived classes override it as protected.
+     */
+    public void tearDown() throws Exception {
+        super.tearDown();
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpTestBase.java b/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpTestBase.java
new file mode 100644
index 0000000..d0ab076
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpTestBase.java
@@ -0,0 +1,549 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.integration;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+import javax.servlet.http.HttpServletResponse;
+
+import junit.framework.TestCase;
+
+import org.apache.commons.httpclient.Credentials;
+import org.apache.commons.httpclient.Header;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.HttpMethodBase;
+import org.apache.commons.httpclient.NameValuePair;
+import org.apache.commons.httpclient.UsernamePasswordCredentials;
+import org.apache.commons.httpclient.auth.AuthScope;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.sling.commons.testing.util.JavascriptEngine;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.slf4j.MDC;
+
+/** Base class for HTTP-based Sling Launchpad integration tests */
+public class HttpTestBase extends TestCase {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    
+    /** If this system property is set, the startup check is skipped. */
+    public static final String PROPERTY_SKIP_STARTUP_CHECK = "launchpad.skip.startupcheck";
+
+    public static final String HTTP_URL = removeEndingSlash(System.getProperty("launchpad.http.server.url", "http://localhost:8888"));
+    public static final String HTTP_BASE_URL = removePath(HTTP_URL);
+    public static final String WEBDAV_BASE_URL = removeEndingSlash(System.getProperty("launchpad.webdav.server.url", HTTP_BASE_URL));
+    public static final String SERVLET_CONTEXT = removeEndingSlash(System.getProperty("launchpad.servlet.context", getPath(HTTP_URL)));
+    
+    public static final String READY_URL_PROP_PREFIX = "launchpad.ready.";
+    public static final int MAX_READY_URL_INDEX = 50;
+
+    /** base path for test files */
+    public static final String TEST_PATH = "/launchpad-integration-tests";
+    
+    public static final String HTTP_METHOD_GET = "GET";
+    public static final String HTTP_METHOD_POST = "POST";
+
+    public static final String CONTENT_TYPE_HTML = "text/html";
+    public static final String CONTENT_TYPE_XML = "application/xml";
+    public static final String CONTENT_TYPE_PLAIN = "text/plain";
+    public static final String CONTENT_TYPE_JSON = "application/json";
+    public static final String CONTENT_TYPE_JS = "application/javascript";
+    public static final String CONTENT_TYPE_CSS = "text/css";
+
+    public static final String SLING_RESOURCE_TYPE = "sling:resourceType";
+
+    public static final String SLING_POST_SERVLET_CREATE_SUFFIX = "/";
+	public static final String DEFAULT_EXT = ".txt";
+
+	public static final String EXECUTE_RESOURCE_TYPE = "SlingTesting" + HttpTestBase.class.getSimpleName();
+	private static int executeCounter;
+
+    public static final int READY_TIMEOUT_SECONDS = Integer.getInteger("HttpTestBase.readyTimeoutSeconds", 60);
+
+    protected SlingIntegrationTestClient testClient;
+    protected HttpClient httpClient;
+
+    private static Boolean slingStartupOk;
+
+    /** Means "don't care about Content-Type" in getContent(...) methods */
+    public static final String CONTENT_TYPE_DONTCARE = "*";
+
+    private static final Object startupCheckLock = new Object();
+
+    /** URLs stored here are deleted in tearDown */
+    protected final List<String> urlsToDelete = new LinkedList<String>();
+
+    /** Need to execute javascript code */
+    private final JavascriptEngine javascriptEngine = new JavascriptEngine();
+
+    /** Class that creates a test node under the given parentPath, and
+     *  stores useful values for testing. Created for JspScriptingTest,
+     *  older test classes do not use it, but it might simplify them.
+     */
+    protected class TestNode extends HttpTestNode {
+        public TestNode(String parentPath, Map<String, String> properties) throws IOException {
+            super(testClient, parentPath, properties);
+        }
+    }
+
+    public static String removeEndingSlash(String str) {
+        if(str != null && str.endsWith("/")) {
+            return str.substring(0, str.length() - 1);
+        }
+        return str;
+    }
+
+    private static String removePath(String str) {
+        final int pos = str.indexOf(":/");
+        final int slashPos = str.indexOf('/', pos+3);
+        if ( slashPos != -1 ) {
+            return str.substring(0, slashPos);
+        }
+        return str;
+    }
+
+    private static String getPath(String str) {
+        final int pos = str.indexOf(":/");
+        final int slashPos = str.indexOf('/', pos+3);
+        if ( slashPos != -1 ) {
+            return str.substring(slashPos);
+        }
+        return "";
+    }
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        MDC.put("testclass", getClass().getName());
+        MDC.put("testcase", getName());
+
+        // assume http and webdav are on the same host + port
+        URL url = null;
+        try {
+            url = new URL(HTTP_BASE_URL);
+        } catch(MalformedURLException mfe) {
+            // MalformedURLException doesn't tell us the URL by default
+            throw new IOException("MalformedURLException: " + HTTP_BASE_URL);
+        }
+
+        // setup HTTP client, with authentication (using default Jackrabbit credentials)
+        httpClient = new TestInfoPassingClient();
+        httpClient.getParams().setAuthenticationPreemptive(true);
+        Credentials defaultcreds = getDefaultCredentials();
+        httpClient.getState().setCredentials(new AuthScope(url.getHost(), url.getPort(), AuthScope.ANY_REALM), defaultcreds);
+
+        testClient = new SlingIntegrationTestClient(httpClient);
+
+        waitForSlingStartup();
+    }
+
+    /**
+     * Generate default credentials used for HTTP requests.
+     */
+    public Credentials getDefaultCredentials() {
+        return new UsernamePasswordCredentials("admin", "admin");
+    }
+
+    @Override
+    protected void tearDown() throws Exception {
+        MDC.remove("testcase");
+        MDC.remove("testclass");
+
+        super.tearDown();
+
+        for(String url : urlsToDelete) {
+            testClient.delete(url);
+        }
+    }
+
+    /** On the server side, initialization of Sling bundles is done
+     *  asynchronously once the webapp is started. This method checks
+     *  that everything's ready on the server side, by calling an URL
+     *  that requires the SlingPostServlet and the JCR repository to
+     *  work correctly.
+     */
+    protected void waitForSlingStartup() throws Exception {
+        // Use a static flag to make sure this runs only once in our test suite
+        // we must synchronize on this if we don't 2 threads could enter the check concurrently
+        // which would leave to random results.
+        synchronized(startupCheckLock) {
+            if (slingStartupOk != null) {
+                if(slingStartupOk) {
+                    return;
+                }
+                fail("Sling services not available. Already checked in earlier tests.");
+            }
+            if ( System.getProperty(PROPERTY_SKIP_STARTUP_CHECK) != null ) {
+                slingStartupOk = true;
+                return;
+            }
+            slingStartupOk = false;
+        }
+
+        System.err.println("Checking if the required Sling services are started (timeout " + READY_TIMEOUT_SECONDS + " seconds)...");
+        System.err.println("(base URLs=" + HTTP_BASE_URL + " and " + WEBDAV_BASE_URL + "; servlet context="+ SERVLET_CONTEXT +")");
+
+        // Try creating a node on server, every 500msec, until ok, with timeout
+        final List<String> exceptionMessages = new LinkedList<String>();
+        final long maxMsecToWait = READY_TIMEOUT_SECONDS * 1000L;
+        final long startupTime = System.currentTimeMillis();
+        String lastException = "";
+        int nTimesOk = 0;
+        
+        // Wait until slingServerReady returns true this many times,
+        // as in some cases more initializations might take place after
+        // this returns true
+        final int MIN_TIMES_OK = 4;
+        
+        while(!slingStartupOk && (System.currentTimeMillis() < startupTime + maxMsecToWait) ) {
+            try {
+                if(slingServerReady()) {
+                    nTimesOk++;
+                    if(nTimesOk >= MIN_TIMES_OK) {
+                        slingStartupOk = true;
+                        break;
+                    }
+                } else {
+                    nTimesOk = 0;
+                }
+            } catch(Exception e) {
+                nTimesOk = 0;
+                final String newX = e.toString();
+                if(!lastException.equals(newX)) {
+                    exceptionMessages.add(newX);
+                }
+                lastException = newX;
+            }
+            Thread.sleep(500L);
+        }
+
+        if(slingStartupOk) {
+            log.info("Sling server found ready after {} msec", System.currentTimeMillis() - startupTime);
+        } else {
+            StringBuffer msg = new StringBuffer("Server does not seem to be ready, after ");
+            msg.append(maxMsecToWait).append(" msec, got the following ").append(exceptionMessages.size()).append(" Exceptions:");
+            for (String e: exceptionMessages) {
+                msg.append(e).append("\n");
+            }
+            System.err.println(msg);
+            fail(msg.toString());
+        }
+
+        System.err.println("Sling services seem to be started, continuing with integration tests.");
+    }
+
+    /** Return true if able to create and retrieve a node on server */
+    protected boolean slingServerReady() throws Exception {
+        // create a node on the server
+        final String time = String.valueOf(System.currentTimeMillis());
+        final String url = HTTP_BASE_URL + "/WaitForSlingStartup/" + time;
+
+        // add some properties to the node
+        final Map<String,String> props = new HashMap<String,String>();
+        props.put("time", time);
+
+        // POST, get URL of created node and get content
+        String urlOfNewNode = null; 
+        try {
+            urlOfNewNode = testClient.createNode(url, props, null, true);
+            final GetMethod get = new GetMethod(urlOfNewNode + DEFAULT_EXT);
+            final int status = httpClient.executeMethod(get);
+            if(status!=200) {
+                throw new HttpStatusCodeException(200, status, "GET", urlOfNewNode);
+            }
+
+            final Header h = get.getResponseHeader("Content-Type");
+            final String contentType = h==null ? "" : h.getValue();
+            if(!contentType.startsWith("text/plain")) {
+                throw new IOException("Expected Content-Type=text/plain but got '" + contentType + "' for URL=" + urlOfNewNode);
+            }
+
+            final String content = get.getResponseBodyAsString();
+            if(!content.contains(time)) {
+                throw new IOException("Content does not contain '" + time + "' (" + content + ") at URL=" + urlOfNewNode);
+            }
+        } finally {
+            if(urlOfNewNode != null) {
+                try {
+                    testClient.delete(urlOfNewNode);
+                } catch(Exception ignore) {
+                }
+            }
+        }
+
+        // Also check that the WebDAV root is ready
+        {
+            // need the trailing slash in case the base URL is the context root
+            final String webDavUrl = WEBDAV_BASE_URL + "/";
+            final HttpAnyMethod options = new HttpAnyMethod("OPTIONS",webDavUrl);
+            final int status = httpClient.executeMethod(options);
+            if(status!=200) {
+                throw new HttpStatusCodeException(200, status, "OPTIONS", webDavUrl);
+            }
+
+            // The Allow header tells us that we're talking to a WebDAV server
+            final Header h = options.getResponseHeader("Allow");
+            if(h == null) {
+                throw new IOException("Response does not contain Allow header, at URL=" + webDavUrl);
+            } else if(h.getValue() == null) {
+                throw new IOException("Allow header has null value at URL=" + webDavUrl);
+            } else if(!h.getValue().contains("PROPFIND")) {
+                throw new IOException("Allow header (" + h.getValue() + " does not contain PROPFIND, at URL=" + webDavUrl);
+            }
+        }
+        
+        // And check optional additional URLs for readyness
+        // Defined by system properties like
+        //  launchpad.ready.1 = GET:/tmp/someUrl:200:expectedRegexpInResponse
+        {
+            for(int i=0; i <= MAX_READY_URL_INDEX ; i++) {
+                final String propName = READY_URL_PROP_PREFIX + i;
+                final String readyDef = System.getProperty(propName, "");
+                final String [] parts = readyDef.split(":");
+                if(parts.length == 4) {
+                    final String info = propName + "=" + readyDef;
+                    final HttpAnyMethod m = new HttpAnyMethod(parts[0],HTTP_BASE_URL + parts[1]);
+                    final int expectedStatus = Integer.valueOf(parts[2]);
+                    final int status = httpClient.executeMethod(m);
+                    if(expectedStatus != status) {
+                        throw new IOException("Status " + status + " does not match expected value: " + info); 
+                    }
+                    final String content = m.getResponseBodyAsString();
+                    final Pattern p = Pattern.compile("(?s).*" + parts[3] + ".*");
+                    if(!p.matcher(content).matches()) {
+                        throw new IOException("Content does not match expected regexp:" + info  + ", content=" + content);
+                    }
+                }
+            }
+        }
+
+        return true;
+    }
+
+    /** Verify that given URL returns expectedStatusCode
+     * @return the HttpMethod executed
+     * @throws IOException */
+    public HttpMethod assertHttpStatus(String urlString, int expectedStatusCode, String assertMessage) throws IOException {
+        final GetMethod get = new GetMethod(urlString);
+        final int status = httpClient.executeMethod(get);
+        if(assertMessage == null) {
+            assertEquals(urlString,expectedStatusCode, status);
+        } else {
+            assertEquals(assertMessage, expectedStatusCode, status);
+        }
+        return get;
+    }
+
+    /** Verify that given URL returns expectedStatusCode
+     * @return the HttpMethod executed
+     * @throws IOException */
+    public HttpMethod assertHttpStatus(String urlString, int expectedStatusCode) throws IOException {
+        return assertHttpStatus(urlString, expectedStatusCode, null);
+    }
+
+    /** Execute a POST request and check status
+     * @return the HttpMethod executed
+     * @throws IOException */
+    public HttpMethod assertPostStatus(String url, int expectedStatusCode, List<NameValuePair> postParams, String assertMessage)
+    throws IOException {
+        final PostMethod post = new PostMethod(url);
+        post.setFollowRedirects(false);
+
+        if(postParams!=null) {
+            final NameValuePair [] nvp = {};
+            post.setRequestBody(postParams.toArray(nvp));
+        }
+
+        final int status = httpClient.executeMethod(post);
+        if(assertMessage == null) {
+            assertEquals(expectedStatusCode, status);
+        } else {
+            assertEquals(assertMessage, expectedStatusCode, status);
+        }
+        return post;
+    }
+
+    /** retrieve the contents of given URL and assert its content type (default to HTTP GET method)*/
+    public String getContent(String url, String expectedContentType) throws IOException {
+        return getContent(url, expectedContentType, null);
+    }
+
+    /** retrieve the contents of given URL and assert its content type (default to HTTP GET method)*/
+    public String getContent(String url, String expectedContentType, List<NameValuePair> params) throws IOException {
+        return getContent(url, expectedContentType, params, HttpServletResponse.SC_OK);
+    }
+
+    /** retrieve the contents of given URL and assert its content type (default to HTTP GET method)
+     * @param expectedContentType use CONTENT_TYPE_DONTCARE if must not be checked
+     * @throws IOException
+     * @throws HttpException */
+    public String getContent(String url, String expectedContentType, List<NameValuePair> params, int expectedStatusCode) throws IOException {
+    	return getContent(url, expectedContentType, params, expectedStatusCode, HTTP_METHOD_GET);
+    }
+    
+    /** retrieve the contents of given URL and assert its content type
+     * @param expectedContentType use CONTENT_TYPE_DONTCARE if must not be checked
+     * @param httMethod supports just GET and POST methods
+     * @throws IOException
+     * @throws HttpException */
+    public String getContent(String url, String expectedContentType, List<NameValuePair> params, int expectedStatusCode, String httpMethod) throws IOException {
+    	HttpMethodBase method = null;
+    	
+    	if (HTTP_METHOD_GET.equals(httpMethod)){
+    		method= new GetMethod(url);
+    	}else if (HTTP_METHOD_POST.equals(httpMethod)){
+    		method = new PostMethod(url);
+    	}  else{
+    		fail("Http Method not supported in this test suite, method: "+httpMethod);
+    	}
+    	
+    	if(params != null) {
+            final NameValuePair [] nvp = new NameValuePair[0];
+            method.setQueryString(params.toArray(nvp));
+        }
+        final int status = httpClient.executeMethod(method);
+        final String content = getResponseBodyAsStream(method, 0);
+        assertEquals("Expected status " + expectedStatusCode + " for " + url + " (content=" + content + ")",
+                expectedStatusCode,status);
+        final Header h = method.getResponseHeader("Content-Type");
+        if(expectedContentType == null) {
+            if(h!=null) {
+                fail("Expected null Content-Type, got " + h.getValue());
+            }
+        } else if(CONTENT_TYPE_DONTCARE.equals(expectedContentType)) {
+            // no check
+        } else if(h==null) {
+            fail(
+                    "Expected Content-Type that starts with '" + expectedContentType
+                    +" but got no Content-Type header at " + url
+            );
+        } else {
+            assertTrue(
+                "Expected Content-Type that starts with '" + expectedContentType
+                + "' for " + url + ", got '" + h.getValue() + "'",
+                h.getValue().startsWith(expectedContentType)
+            );
+        }
+        return content.toString();
+    	
+    }
+    
+
+    /** upload rendering test script, and return its URL for future deletion */
+    public String uploadTestScript(String scriptPath, String localFilename,String filenameOnServer) throws IOException {
+        final String url = WEBDAV_BASE_URL + scriptPath + "/" + filenameOnServer;
+        final String testFile = "/integration-test/" + localFilename;
+        final InputStream data = getClass().getResourceAsStream(testFile);
+        if(data==null) {
+            fail("Test file not found:" + testFile);
+        }
+        try {
+            testClient.upload(url, data);
+        } finally {
+            if(data!=null) {
+                data.close();
+            }
+        }
+        return url;
+    }
+
+    /** Upload script, execute with no parameters and return content */
+    public String executeScript(String localFilename) throws Exception {
+        return executeScript(localFilename, null);
+    }
+
+    /** Upload script, execute with given parameters (optional) and return content */
+    public String executeScript(String localFilename, List<NameValuePair> params) throws Exception {
+
+        // Use unique resource type
+        int counter = 0;
+        synchronized (getClass()) {
+            counter = ++executeCounter;
+        }
+        final String resourceType = EXECUTE_RESOURCE_TYPE + counter;
+        final String scriptPath = "/apps/" + resourceType;
+        testClient.mkdirs(WEBDAV_BASE_URL , scriptPath);
+
+        final int pos = localFilename.lastIndexOf(".");
+        if(pos < 1) {
+            throw new IllegalArgumentException("localFilename must have extension (" + localFilename + ")");
+        }
+        final String ext = localFilename.substring(pos + 1);
+        final List<String> toDelete = new LinkedList<String>();
+        try {
+            toDelete.add(uploadTestScript(scriptPath, localFilename, "txt." + ext));
+            final Map<String, String> props = new HashMap<String, String>();
+            props.put(SLING_RESOURCE_TYPE, resourceType);
+            final String nodePath = scriptPath + "/node" + counter;
+            final String nodeUrl = testClient.createNode(HTTP_BASE_URL + nodePath, props);
+            toDelete.add(nodeUrl);
+            return getContent(nodeUrl + ".txt", CONTENT_TYPE_DONTCARE, params);
+        } finally {
+            for(String url : toDelete) {
+                testClient.delete(url);
+            }
+        }
+    }
+
+    public void assertJavascript(String expectedOutput, String jsonData, String code) throws IOException {
+        assertJavascript(expectedOutput, jsonData, code, null);
+    }
+
+    /** Evaluate given code using given jsonData as the "data" object */
+    public void assertJavascript(String expectedOutput, String jsonData, String code, String testInfo) throws IOException {
+    	final String result = javascriptEngine.execute(code, jsonData);
+        if(!result.equals(expectedOutput)) {
+            fail(
+                    "Expected '" + expectedOutput
+                    + "' but got '" + result
+                    + "' for script='" + code + "'"
+                    + "' and data='" + jsonData + "'"
+                    + (testInfo==null ? "" : ", test info=" + testInfo)
+            );
+        }
+    }
+
+    /** Return m's response body as a string, optionally limiting the length that we read
+     * @param maxLength if 0, no limit
+     */
+    public static String getResponseBodyAsStream(HttpMethodBase m, int maxLength) throws IOException {
+        final InputStream is = m.getResponseBodyAsStream();
+        final StringBuilder content = new StringBuilder();
+        final String charset = m.getResponseCharSet();
+        final byte [] buffer = new byte[16384];
+        int n = 0;
+        while( (n = is.read(buffer, 0, buffer.length)) > 0) {
+            content.append(new String(buffer, 0, n, charset));
+            if(maxLength != 0 && content.length() > maxLength) {
+                throw new IllegalArgumentException("Response body size is over maxLength (" + maxLength + ")");
+            }
+        }
+        return content.toString();
+    }
+}
\ No newline at end of file
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpTestNode.java b/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpTestNode.java
new file mode 100644
index 0000000..77af85f
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/integration/HttpTestNode.java
@@ -0,0 +1,48 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.integration;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+/** Utility to manage test nodes */
+public class HttpTestNode {
+    public final String testText;
+    public final String nodeUrl;
+    public final String resourceType;
+    public final String scriptPath;
+    private final SlingIntegrationTestClient testClient;
+
+    public HttpTestNode(SlingIntegrationTestClient testClient, String parentPath, Map<String, String> properties) throws IOException {
+        this.testClient = testClient;
+        if(properties == null) {
+            properties = new HashMap<String, String>();
+        }
+        testText = "This is a test node " + System.currentTimeMillis();
+        properties.put("text", testText);
+        nodeUrl = testClient.createNode(parentPath + HttpTestBase.SLING_POST_SERVLET_CREATE_SUFFIX, properties);
+        resourceType = properties.get(HttpTestBase.SLING_RESOURCE_TYPE);
+        scriptPath = "/apps/" + (resourceType == null ? "nt/unstructured" : resourceType);
+        testClient.mkdirs(HttpTestBase.WEBDAV_BASE_URL, scriptPath);
+    }
+
+    public void delete() throws IOException {
+        testClient.delete(nodeUrl);
+    }
+
+}
\ No newline at end of file
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/integration/NameValuePair.java b/testing/src/main/java/org/apache/sling/commons/testing/integration/NameValuePair.java
new file mode 100644
index 0000000..fe002cb
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/integration/NameValuePair.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.integration;
+
+
+/**
+ * A basic name-value pair class.
+ */
+public class NameValuePair {
+
+    public NameValuePair() {
+        this(null, null);
+    }
+
+    public NameValuePair(String name, String value) {
+        this.name = name;
+        this.value = value;
+    }
+
+    private String name = null;
+
+    private String value = null;
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public void setValue(String value) {
+        this.value = value;
+    }
+
+    public String getValue() {
+        return value;
+    }
+
+    @Override
+    public String toString() {
+        return String.format("name=%s, value=%s", name, value);
+    }
+
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/integration/NameValuePairList.java b/testing/src/main/java/org/apache/sling/commons/testing/integration/NameValuePairList.java
new file mode 100644
index 0000000..39cfff2
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/integration/NameValuePairList.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.integration;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+import java.util.ListIterator;
+import java.util.Map;
+
+/**
+ * A list of name-value pairs.
+ */
+public class NameValuePairList implements Iterable<NameValuePair> {
+
+    private final List<NameValuePair> delegate;
+
+    public NameValuePairList() {
+        delegate = new ArrayList<NameValuePair>();
+    }
+
+    public NameValuePairList(List<NameValuePair> init) {
+        delegate = new ArrayList<NameValuePair>(init);
+    }
+
+    public NameValuePairList(NameValuePairList clientNodeProperties) {
+        this(clientNodeProperties.delegate);
+    }
+
+    public NameValuePairList(Map<String, String> clientNodeProperties) {
+        this();
+        if (clientNodeProperties != null) {
+            for (Map.Entry<String, String> e : clientNodeProperties.entrySet()) {
+                add(e.getKey(), e.getValue());
+            }
+        }
+    }
+
+    public void add(String name, String value) {
+        delegate.add(new NameValuePair(name, value));
+    }
+
+    public void addIfNew(String name, String value) {
+        boolean found = false;
+        for (ListIterator<NameValuePair> li = delegate.listIterator(); li.hasNext();) {
+            NameValuePair current = li.next();
+            if (current.getName().equals(name)) {
+                found = true;
+                break;
+            }
+        }
+
+        if (!found) {
+            delegate.add(new NameValuePair(name, value));
+        }
+
+    }
+
+    public void addOrReplace(String name, String value) {
+        boolean replaced = false;
+        for (ListIterator<NameValuePair> li = delegate.listIterator(); li.hasNext();) {
+            NameValuePair current = li.next();
+            if (current.getName().equals(name)) {
+                if (!replaced) {
+                    current.setValue(value);
+                    replaced = true;
+                } else {
+                    li.remove();
+                }
+            }
+        }
+
+        if (!replaced) {
+            delegate.add(new NameValuePair(name, value));
+        }
+    }
+
+    public void addOrReplaceAll(NameValuePairList other) {
+        for (NameValuePair nvp : other) {
+            addOrReplace(nvp.getName(), nvp.getValue());
+        }
+    }
+
+    public void clear() {
+        delegate.clear();
+    }
+
+    public Iterator<NameValuePair> iterator() {
+        return delegate.iterator();
+    }
+
+    public void prependIfNew(String name, String value) {
+        boolean found = false;
+        for (ListIterator<NameValuePair> li = delegate.listIterator(); li.hasNext();) {
+            NameValuePair current = li.next();
+            if (current.getName().equals(name)) {
+                found = true;
+                break;
+            }
+        }
+
+        if (!found) {
+            delegate.add(0, new NameValuePair(name, value));
+        }
+
+    }
+
+    public int size() {
+        return delegate.size();
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/integration/SlingIntegrationTestClient.java b/testing/src/main/java/org/apache/sling/commons/testing/integration/SlingIntegrationTestClient.java
new file mode 100644
index 0000000..9263598
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/integration/SlingIntegrationTestClient.java
@@ -0,0 +1,267 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.integration;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpException;
+import org.apache.commons.httpclient.methods.DeleteMethod;
+import org.apache.commons.httpclient.methods.GetMethod;
+import org.apache.commons.httpclient.methods.InputStreamRequestEntity;
+import org.apache.commons.httpclient.methods.PostMethod;
+import org.apache.commons.httpclient.methods.PutMethod;
+import org.apache.commons.httpclient.methods.multipart.FilePart;
+import org.apache.commons.httpclient.methods.multipart.MultipartRequestEntity;
+import org.apache.commons.httpclient.methods.multipart.Part;
+import org.apache.commons.httpclient.methods.multipart.StringPart;
+
+/** Client functions to interact with Sling in integration tests */
+public class SlingIntegrationTestClient {
+    private final HttpClient httpClient;
+
+    public SlingIntegrationTestClient(HttpClient client) {
+        this.httpClient = client;
+    }
+
+    /** Upload a file to the Sling repository
+     *  @return the HTTP status code
+     */
+    public int upload(String toUrl, InputStream is) throws IOException {
+        final PutMethod put = new PutMethod(toUrl);
+        put.setRequestEntity(new InputStreamRequestEntity(is));
+        return httpClient.executeMethod(put);
+    }
+
+    /** Delete a file from the Sling repository
+     *  @return the HTTP status code
+     */
+    public int delete(String url) throws IOException {
+        final DeleteMethod delete = new DeleteMethod(url);
+        return httpClient.executeMethod(delete);
+    }
+
+    /** Create the given directory via WebDAV, if needed, under given URL */
+    public void mkdir(String url) throws IOException {
+        int status = 0;
+        status = httpClient.executeMethod(new GetMethod(url + ".txt"));
+        if(status != 200) {
+            status = httpClient.executeMethod(new HttpAnyMethod("MKCOL",url));
+            if(status!=201) {
+                throw new IOException("mkdir(" + url + ") failed, status code=" + status);
+            }
+        }
+    }
+
+    /** Create the given directory via WebDAV, including parent directories */
+    public void mkdirs(String baseUrl,String path) throws IOException {
+        final String [] paths = path.split("/");
+        if(baseUrl.endsWith("/")) {
+            baseUrl = baseUrl.substring(0,baseUrl.length() - 1);
+        }
+
+        String currentPath = baseUrl;
+        for(String pathElement : paths) {
+            if(pathElement.length() == 0) {
+                continue;
+            }
+            currentPath += "/" + pathElement;
+            mkdir(currentPath);
+        }
+
+        final String url = baseUrl + path;
+        final int status = httpClient.executeMethod(new GetMethod(url + ".txt"));
+        if(status!=200) {
+            throw new HttpStatusCodeException(200, status, "GET", url);
+        }
+    }
+
+    /** Call the other createNode method with headers==null */
+    public String createNode(String url, Map<String,String> nodeProperties) throws IOException {
+        return createNode(url, nodeProperties, null, false);
+    }
+
+    /** Create a node under given path, using a POST to Sling
+     *  @param url under which node is created
+     *  @param multiPart if true, does a multipart POST
+     *  @return the URL that Sling provides to display the node
+     */
+    public String createNode(String url, Map<String,String> clientNodeProperties, Map<String,String> requestHeaders,boolean multiPart)
+    throws IOException {
+        return createNode(url, new NameValuePairList(clientNodeProperties), requestHeaders, multiPart);
+    }
+
+    /** Create a node under given path, using a POST to Sling
+     *  @param url under which node is created
+     *  @param multiPart if true, does a multipart POST
+     *  @return the URL that Sling provides to display the node
+     */
+    public String createNode(String url, NameValuePairList clientNodeProperties, Map<String,String> requestHeaders, boolean multiPart)
+    throws IOException {
+    	return createNode(url, clientNodeProperties, requestHeaders, multiPart, null, null, null);
+    }
+    
+    /** Create a node under given path, using a POST to Sling
+     *  @param url under which node is created
+     *  @param multiPart if true, does a multipart POST
+     *  @param localFile file to upload
+     *  @param fieldName name of the file field
+     *  @param typeHint typeHint of the file field 
+     *  @return the URL that Sling provides to display the node
+     */
+    public String createNode(String url, NameValuePairList clientNodeProperties, Map<String,String> requestHeaders, boolean multiPart, 
+    		File localFile, String fieldName, String typeHint) 
+    throws IOException {
+    	
+        final PostMethod post = new PostMethod(url);
+        post.setFollowRedirects(false);
+
+        // create a private copy of the properties to not tamper with
+        // the properties of the client
+        NameValuePairList nodeProperties = new NameValuePairList(clientNodeProperties);
+
+        // add sling specific properties
+        nodeProperties.prependIfNew(":redirect", "*");
+        nodeProperties.prependIfNew(":displayExtension", "");
+        nodeProperties.prependIfNew(":status", "browser");
+
+        // add fake property - otherwise the node is not created
+        if (clientNodeProperties == null) {
+            nodeProperties.add("jcr:created", "");
+        }
+
+        // force form encoding to UTF-8, which is what we use to convert the
+        // string parts into stream data
+        nodeProperties.addOrReplace("_charset_", "UTF-8");
+
+        if( nodeProperties.size() > 0) {
+            if(multiPart) {
+                final List<Part> partList = new ArrayList<Part>();
+                for(NameValuePair e : nodeProperties) {
+                    if (e.getValue() != null) {
+                        partList.add(new StringPart(e.getName(), e.getValue(), "UTF-8"));
+                    }
+                }
+                if  (localFile != null) {
+                    partList.add(new FilePart(fieldName, localFile));
+                    if (typeHint != null) {
+                    	partList.add(new StringPart(fieldName + "@TypeHint", typeHint));
+                    }
+                }
+                final Part [] parts = partList.toArray(new Part[partList.size()]);
+                post.setRequestEntity(new MultipartRequestEntity(parts, post.getParams()));
+            } else {
+            	post.getParams().setContentCharset("UTF-8");
+                for(NameValuePair e : nodeProperties) {
+                    post.addParameter(e.getName(),e.getValue());
+                }
+            }
+        }
+
+        if(requestHeaders != null) {
+            for(Map.Entry<String,String> e : requestHeaders.entrySet()) {
+                post.addRequestHeader(e.getKey(), e.getValue());
+            }
+        }
+
+        final int expected = 302;
+        final int status = httpClient.executeMethod(post);
+        if(status!=expected) {
+            throw new HttpStatusCodeException(expected, status, "POST", url, HttpTestBase.getResponseBodyAsStream(post, 0));
+        }
+        String location = post.getResponseHeader("Location").getValue();
+        post.releaseConnection();
+        // simple check if host is missing
+        if (!location.startsWith("http://")) {
+            String host = HttpTestBase.HTTP_BASE_URL;
+            int idx = host.indexOf('/', 8);
+            if (idx > 0) {
+                host = host.substring(0, idx);
+            }
+            location = host + location;
+        }
+        return location;
+    }
+
+    /** Upload to an file node structure, see SLING-168 */
+    public void uploadToFileNode(String url, File localFile, String fieldName, String typeHint)
+        throws IOException {
+
+        final Part[] parts = new Part[typeHint == null ? 1 : 2];
+        parts[0] = new FilePart(fieldName, localFile);
+        if (typeHint != null) {
+            parts[1] = new StringPart(fieldName + "@TypeHint", typeHint);
+        }
+        final PostMethod post = new PostMethod(url);
+        post.setFollowRedirects(false);
+        post.setRequestEntity(new MultipartRequestEntity(parts, post.getParams()));
+
+        final int status = httpClient.executeMethod(post);
+        final int expected = 200;
+        if(status!=expected) {
+            throw new HttpStatusCodeException(expected, status, "POST", HttpTestBase.getResponseBodyAsStream(post, 0));
+        }
+    }
+
+    /** Upload multiple files to file node structures */
+    public void uploadToFileNodes(String url, File[] localFiles, String[] fieldNames, String[] typeHints)
+        throws IOException {
+
+    	List<Part> partsList = new ArrayList<Part>();
+    	for (int i=0; i < localFiles.length; i++) {
+            Part filePart = new FilePart(fieldNames[i], localFiles[i]);
+            partsList.add(filePart);
+            if (typeHints != null) {
+            	Part typeHintPart = new StringPart(fieldNames[i] + "@TypeHint", typeHints[i]);
+            	partsList.add(typeHintPart);
+            }
+		}
+
+        final Part[] parts = partsList.toArray(new Part[partsList.size()]);
+        final PostMethod post = new PostMethod(url);
+        post.setFollowRedirects(false);
+        post.setRequestEntity(new MultipartRequestEntity(parts, post.getParams()));
+
+        final int expected = 200;
+        final int status = httpClient.executeMethod(post);
+        if(status!=expected) {
+            throw new HttpStatusCodeException(expected, status, "POST", HttpTestBase.getResponseBodyAsStream(post, 0));
+        }
+    }
+    
+    public int post(String url, Map<String,String> properties) throws HttpException, IOException {
+        final PostMethod post = new PostMethod(url);
+        post.getParams().setContentCharset("UTF-8");
+        for(Entry<String, String> e : properties.entrySet()) {
+            post.addParameter(e.getKey(), e.getValue());
+        }
+        return httpClient.executeMethod(post);
+    }
+
+    public int get(String url) throws HttpException, IOException {
+        final GetMethod get = new GetMethod(url);
+        get.getParams().setContentCharset("UTF-8");
+        return httpClient.executeMethod(get);
+    }
+
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/integration/TestInfoPassingClient.java b/testing/src/main/java/org/apache/sling/commons/testing/integration/TestInfoPassingClient.java
new file mode 100644
index 0000000..314e33b
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/integration/TestInfoPassingClient.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+
+package org.apache.sling.commons.testing.integration;
+
+import java.io.IOException;
+import java.util.Map;
+
+import org.apache.commons.httpclient.HostConfiguration;
+import org.apache.commons.httpclient.HttpClient;
+import org.apache.commons.httpclient.HttpMethod;
+import org.apache.commons.httpclient.HttpState;
+import org.slf4j.MDC;
+
+/**
+ * HttpClient extension which also passes test related headers as part
+ * of outgoing HTTP request
+ */
+public class TestInfoPassingClient extends HttpClient {
+    //Defined in org.apache.sling.testing.tools.junit.TestLogRule
+    private static final String SLING_HEADER_PREFIX = "X-Sling-";
+
+    @Override
+    public int executeMethod(HostConfiguration hostconfig, HttpMethod method,
+                             HttpState state) throws IOException {
+        addSlingHeaders(method);
+        return super.executeMethod(hostconfig, method, state);
+    }
+
+    /**
+     * Adds all MDC key-value pairs as HTTP header where the key starts
+     * with 'X-Sling-'
+     */
+    private static void addSlingHeaders(HttpMethod m){
+        Map<?,?> mdc = MDC.getCopyOfContextMap();
+        if (mdc != null) {
+            for (Map.Entry<?, ?> e : mdc.entrySet()) {
+                Object key = e.getKey();
+                if (key instanceof String
+                        && ((String)key).startsWith(SLING_HEADER_PREFIX)
+                        && e.getValue() instanceof String) {
+                    m.addRequestHeader((String) key, (String) e.getValue());
+                }
+            }
+        }
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/EventHelper.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/EventHelper.java
new file mode 100644
index 0000000..f11fc53
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/EventHelper.java
@@ -0,0 +1,95 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.observation.Event;
+import javax.jcr.observation.EventIterator;
+import javax.jcr.observation.EventListener;
+
+/** Used by tests to wait until JCR notification events
+ * 	have been delivered.
+ */
+public class EventHelper implements EventListener {
+	private final Session session;
+	private int eventCount;
+	public static final String WAIT_NODE_FOLDER = "WAIT_NODE";
+	public static final String WAIT_NODE_NODE = EventHelper.class.getSimpleName();
+	private final Node waitNodeFolder;
+
+	public EventHelper(Session s) throws RepositoryException {
+		session = s;
+
+        final int eventTypes = Event.NODE_ADDED | Event.NODE_REMOVED;
+        final boolean isDeep = true;
+        final boolean noLocal = false;
+        session.getWorkspace().getObservationManager().addEventListener(
+        		this, eventTypes, "/" + WAIT_NODE_FOLDER, isDeep, null, null, noLocal);
+
+        if(session.getRootNode().hasNode(WAIT_NODE_FOLDER)) {
+        	waitNodeFolder = session.getRootNode().getNode(WAIT_NODE_FOLDER);
+        } else {
+        	waitNodeFolder = session.getRootNode().addNode(WAIT_NODE_FOLDER, "nt:unstructured");
+        }
+        session.save();
+	}
+
+    public void onEvent(EventIterator it) {
+        eventCount++;
+    }
+
+    /** To make sure observation events have been delivered,
+     * 	create or delete a a node and wait for the corresponding
+     * 	events to be received.
+     */
+	public void waitForEvents(long timeoutMsec) throws RepositoryException {
+		final int targetEventCount = eventCount + 1;
+
+		if(waitNodeFolder.hasNode(WAIT_NODE_NODE)) {
+			waitNodeFolder.getNode(WAIT_NODE_NODE).remove();
+		} else {
+			waitNodeFolder.addNode(WAIT_NODE_NODE);
+		}
+		session.save();
+
+    	final long end = System.currentTimeMillis() + timeoutMsec;
+    	while(eventCount < targetEventCount && System.currentTimeMillis() < end) {
+    		try {
+    			Thread.sleep(100);
+    		} catch(InterruptedException ignored) {
+    		}
+    	}
+
+    	if(eventCount < targetEventCount) {
+    		throw new IllegalStateException("Event counter did not reach " + targetEventCount + ", waited " + timeoutMsec + " msec");
+    	}
+    }
+
+    /**
+     * Remove the event listener from the observation listener.
+     */
+    public void dispose() {
+        try {
+            session.getWorkspace().getObservationManager().removeEventListener(this);
+        } catch (RepositoryException e) {
+        }
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockNode.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockNode.java
new file mode 100644
index 0000000..997a41e
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockNode.java
@@ -0,0 +1,489 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import javax.jcr.Binary;
+import javax.jcr.InvalidLifecycleTransitionException;
+import javax.jcr.Item;
+import javax.jcr.ItemVisitor;
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.UnsupportedRepositoryOperationException;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+import javax.jcr.lock.Lock;
+import javax.jcr.lock.LockException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
+import javax.jcr.nodetype.NodeDefinition;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.version.Version;
+import javax.jcr.version.VersionException;
+import javax.jcr.version.VersionHistory;
+
+import org.apache.jackrabbit.util.ChildrenCollectorFilter;
+
+// simple mock implementation of a node
+public class MockNode implements Node {
+
+    private String path;
+    private Map <String, Property> properties = new HashMap <String, Property>();
+
+    private NodeType nodeType;
+    private Session session;
+
+    public MockNode(String path) {
+        this(path, null);
+    }
+
+    public MockNode(String path, String type) {
+        this.path = path;
+        this.nodeType = new MockNodeType(type);
+    }
+
+    public String getName() {
+        return path.substring(path.lastIndexOf('/') + 1);
+    }
+
+    public Node getParent() {
+        return new MockNode(path.substring(0, path.lastIndexOf('/')));
+    }
+
+    public String getPath() {
+        return path;
+    }
+
+    public NodeType getPrimaryNodeType() {
+        return nodeType;
+    }
+
+    public boolean isSame(Item otherItem) {
+        return equals(otherItem);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (obj == this) {
+            return true;
+        } else if (!(obj instanceof MockNode)) {
+            return false;
+        }
+
+        return ((MockNode) obj).getPath().equals(getPath());
+    }
+
+    @Override
+    public int hashCode() {
+        return super.hashCode();
+    }
+
+    @Override
+    public String toString() {
+        return "MockNode: path=" + getPath();
+    }
+
+    public void addMixin(String mixinName) {
+    }
+
+    public Node addNode(String relPath) {
+        return null;
+    }
+
+    public Node addNode(String relPath, String primaryNodeTypeName) {
+        return null;
+    }
+
+    public boolean canAddMixin(String mixinName) {
+        return false;
+    }
+
+    public void cancelMerge(Version version) {
+
+    }
+
+    public Version checkin() {
+        return null;
+    }
+
+    public void checkout() {
+    }
+
+    public void doneMerge(Version version) {
+    }
+
+    public Version getBaseVersion() {
+        return null;
+    }
+
+    public String getCorrespondingNodePath(String workspaceName) {
+        return null;
+    }
+
+    public NodeDefinition getDefinition() {
+        return null;
+    }
+
+    public int getIndex() {
+        return 0;
+    }
+
+    public Lock getLock() {
+        return null;
+    }
+
+    public NodeType[] getMixinNodeTypes() {
+        return null;
+    }
+
+    public Node getNode(String relPath) {
+        return new MockNode(path + "/" + relPath);
+    }
+
+    public NodeIterator getNodes() {
+        return new MockNodeIterator();
+    }
+
+    public NodeIterator getNodes(String namePattern) {
+        return new MockNodeIterator();
+    }
+
+    public Item getPrimaryItem() {
+        return null;
+    }
+
+    public PropertyIterator getProperties() {
+        return new MockPropertyIterator(properties.values().iterator());
+    }
+
+    public PropertyIterator getProperties(String namePattern) throws RepositoryException {
+        PropertyIterator iterator = getProperties();
+        List<Property> properties = new ArrayList<Property>();
+
+        while (iterator.hasNext()) {
+            Property p = iterator.nextProperty();
+            String name = p.getName();
+            if (ChildrenCollectorFilter.matches(name, namePattern)) {
+                properties.add(p);
+            }
+        }
+
+        return new MockPropertyIterator(properties.iterator());
+    }
+
+    public Property getProperty(String relPath) {
+        return properties.get(relPath);
+    }
+
+    public PropertyIterator getReferences() {
+        return null;
+    }
+
+    public String getUUID() {
+        return null;
+    }
+
+    public VersionHistory getVersionHistory() {
+        return null;
+    }
+
+    public boolean hasNode(String relPath) {
+        return false;
+    }
+
+    public boolean hasNodes() {
+        return false;
+    }
+
+    public boolean hasProperties() {
+        return false;
+    }
+
+    public boolean hasProperty(String relPath) {
+        return properties.containsKey(relPath);
+    }
+
+    public boolean holdsLock() {
+        return false;
+    }
+
+    public boolean isCheckedOut() {
+        return false;
+    }
+
+    public boolean isLocked() {
+        return false;
+    }
+
+    public boolean isNodeType(String nodeTypeName) {
+        return false;
+    }
+
+    public Lock lock(boolean isDeep, boolean isSessionScoped) {
+        return null;
+    }
+
+    public NodeIterator merge(String srcWorkspace, boolean bestEffort) {
+        return null;
+    }
+
+    public void orderBefore(String srcChildRelPath, String destChildRelPath) {
+    }
+
+    public void removeMixin(String mixinName) {
+    }
+
+    public void restore(String versionName, boolean removeExisting) {
+    }
+
+    public void restore(Version version, boolean removeExisting) {
+    }
+
+    public void restore(Version version, String relPath, boolean removeExisting) {
+    }
+
+    public void restoreByLabel(String versionLabel, boolean removeExisting) {
+    }
+
+    public Property setProperty(String name, Value value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
+        MockProperty p = new MockProperty(name);
+        p.setValue(value);
+        properties.put(name, p);
+        return p;
+    }
+
+    public Property setProperty(String name, Value[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
+        MockProperty p = new MockProperty(name);
+        p.setValue(values);
+        properties.put(name, p);
+        return p;
+    }
+
+    public Property setProperty(String name, String[] values) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
+        MockProperty p = new MockProperty(name);
+        p.setValue(values);
+        properties.put(name, p);
+        return p;
+    }
+
+    public Property setProperty(String name, String value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
+        MockProperty p = new MockProperty(name);
+        p.setValue(value);
+        properties.put(name, p);
+        return p;
+    }
+
+    public Property setProperty(String name, InputStream value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
+        MockProperty p = new MockProperty(name);
+        p.setValue(value);
+        properties.put(name, p);
+        return p;
+    }
+
+    public Property setProperty(String name, boolean value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
+        MockProperty p = new MockProperty(name);
+        p.setValue(value);
+        properties.put(name, p);
+        return p;
+    }
+
+    public Property setProperty(String name, double value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
+        MockProperty p = new MockProperty(name);
+        p.setValue(value);
+        properties.put(name, p);
+        return p;
+    }
+
+    public Property setProperty(String name, long value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
+        MockProperty p = new MockProperty(name);
+        p.setValue(value);
+        properties.put(name, p);
+        return p;
+    }
+
+    public Property setProperty(String name, Calendar value) throws ValueFormatException, VersionException, LockException, ConstraintViolationException, RepositoryException {
+        MockProperty p = new MockProperty(name);
+        p.setValue(value);
+        properties.put(name, p);
+        return p;
+    }
+
+    public Property setProperty(String name, Node value) {
+        return null;
+    }
+
+    public Property setProperty(String name, Value value, int type) {
+        return null;
+    }
+
+    public Property setProperty(String name, Value[] values, int type) {
+        return null;
+    }
+
+    public Property setProperty(String name, String[] values, int type) {
+        return null;
+    }
+
+    public Property setProperty(String name, String value, int type) {
+        return null;
+    }
+
+    public void unlock() {
+    }
+
+    public void update(String srcWorkspaceName) {
+    }
+
+    public void accept(ItemVisitor visitor) {
+    }
+
+    public Item getAncestor(int depth) {
+        return null;
+    }
+
+    public int getDepth() {
+        return 0;
+    }
+
+    public Session getSession() {
+        return this.session;
+    }
+
+    public void setSession(Session session) {
+      this.session = session;
+    }
+
+    public boolean isModified() {
+        return false;
+    }
+
+    public boolean isNew() {
+        return false;
+    }
+
+    public boolean isNode() {
+        return true;
+    }
+
+    public void refresh(boolean keepChanges) {
+    }
+
+    public void remove() {
+    }
+
+    public void save() {
+    }
+
+    // JCR 2.0 methods
+
+    public void followLifecycleTransition(String transition)
+            throws UnsupportedRepositoryOperationException,
+            InvalidLifecycleTransitionException, RepositoryException {
+        // TODO Auto-generated method stub
+
+    }
+
+    public String[] getAllowedLifecycleTransistions()
+            throws UnsupportedRepositoryOperationException, RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public String getIdentifier() throws RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public NodeIterator getNodes(String[] nameGlobs) throws RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public PropertyIterator getProperties(String[] nameGlobs)
+            throws RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public PropertyIterator getReferences(String name)
+            throws RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public NodeIterator getSharedSet() throws RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public PropertyIterator getWeakReferences() throws RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public PropertyIterator getWeakReferences(String name)
+            throws RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public void removeShare() throws VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        // TODO Auto-generated method stub
+
+    }
+
+    public void removeSharedSet() throws VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        // TODO Auto-generated method stub
+
+    }
+
+    public void setPrimaryType(String nodeTypeName)
+            throws NoSuchNodeTypeException, VersionException,
+            ConstraintViolationException, LockException, RepositoryException {
+        // TODO Auto-generated method stub
+
+    }
+
+    public Property setProperty(String name, BigDecimal value)
+            throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public Property setProperty(String name, Binary value)
+            throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockNodeIterator.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockNodeIterator.java
new file mode 100644
index 0000000..fff3dba
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockNodeIterator.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import java.util.NoSuchElementException;
+
+import javax.jcr.Node;
+import javax.jcr.NodeIterator;
+
+public class MockNodeIterator implements NodeIterator {
+
+    private Node[] nodes;
+    private int idx;
+    private static final Node [] EMPTY_NODE_ARRAY = {};
+
+    public MockNodeIterator() {
+        this(EMPTY_NODE_ARRAY);
+    }
+    
+    public MockNodeIterator(Node[] nodes) {
+        this.nodes = (nodes != null) ? nodes : new Node[0];
+        this.idx = 0;
+    }
+
+    public Node nextNode() {
+        if (!hasNext()) {
+            throw new NoSuchElementException();
+        }
+
+        return nodes[idx++];
+    }
+
+    public long getPosition() {
+        return idx-1;
+    }
+
+    public long getSize() {
+        return nodes.length;
+    }
+
+    public void skip(long skipNum) {
+        idx += skipNum;
+    }
+
+    public boolean hasNext() {
+        return idx < nodes.length;
+    }
+
+    public Object next() {
+        return nextNode();
+    }
+
+    public void remove() {
+        throw new UnsupportedOperationException();
+    }
+
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockNodeType.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockNodeType.java
new file mode 100644
index 0000000..17405b7
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockNodeType.java
@@ -0,0 +1,135 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeDefinition;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.NodeTypeIterator;
+import javax.jcr.nodetype.PropertyDefinition;
+
+public class MockNodeType implements NodeType {
+
+    private String name;
+
+    public MockNodeType(String name) {
+        this.name = (name == null) ? "nt:unstructured" : name;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public boolean canAddChildNode(String childNodeName) {
+        return false;
+    }
+
+    public boolean canAddChildNode(String childNodeName, String nodeTypeName) {
+        return false;
+    }
+
+    public boolean canRemoveItem(String itemName) {
+        return false;
+    }
+
+    public boolean canSetProperty(String propertyName, Value value) {
+        return false;
+    }
+
+    public boolean canSetProperty(String propertyName, Value[] values) {
+        return false;
+    }
+
+    public NodeDefinition[] getChildNodeDefinitions() {
+        return null;
+    }
+
+    public NodeDefinition[] getDeclaredChildNodeDefinitions() {
+        return null;
+    }
+
+    public PropertyDefinition[] getDeclaredPropertyDefinitions() {
+        return null;
+    }
+
+    public NodeType[] getDeclaredSupertypes() {
+        return null;
+    }
+
+    public String getPrimaryItemName() {
+        return null;
+    }
+
+    public PropertyDefinition[] getPropertyDefinitions() {
+        return null;
+    }
+
+    public NodeType[] getSupertypes() {
+        return null;
+    }
+
+    public boolean hasOrderableChildNodes() {
+        return false;
+    }
+
+    public boolean isMixin() {
+        return false;
+    }
+
+    public boolean isNodeType(String nodeTypeName) {
+        return false;
+    }
+
+    // JCR 2.0 methods
+
+    public boolean canRemoveNode(String nodeName) {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public boolean canRemoveProperty(String propertyName) {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public NodeTypeIterator getDeclaredSubtypes() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public NodeTypeIterator getSubtypes() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public String[] getDeclaredSupertypeNames() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public boolean isAbstract() {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public boolean isQueryable() {
+        // TODO Auto-generated method stub
+        return false;
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockProperty.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockProperty.java
new file mode 100644
index 0000000..d9866fb
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockProperty.java
@@ -0,0 +1,287 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+import javax.jcr.AccessDeniedException;
+import javax.jcr.Binary;
+import javax.jcr.InvalidItemStateException;
+import javax.jcr.Item;
+import javax.jcr.ItemExistsException;
+import javax.jcr.ItemNotFoundException;
+import javax.jcr.ItemVisitor;
+import javax.jcr.Node;
+import javax.jcr.Property;
+import javax.jcr.PropertyType;
+import javax.jcr.ReferentialIntegrityException;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+import javax.jcr.lock.LockException;
+import javax.jcr.nodetype.ConstraintViolationException;
+import javax.jcr.nodetype.NoSuchNodeTypeException;
+import javax.jcr.nodetype.PropertyDefinition;
+import javax.jcr.version.VersionException;
+
+public class MockProperty implements Property {
+
+    private Value [] values = {};
+    private final String name;
+
+    public MockProperty(String name) {
+        this.name = name;
+    }
+
+    public boolean getBoolean() throws ValueFormatException, RepositoryException {
+        if (values.length > 1) {
+            throw new ValueFormatException();
+        }
+        return values[0].getBoolean();
+    }
+
+    public Calendar getDate() throws ValueFormatException, RepositoryException {
+        if (values.length > 1) {
+            throw new ValueFormatException();
+        }
+        return values[0].getDate();
+    }
+
+    public PropertyDefinition getDefinition() throws RepositoryException {
+        return new MockPropertyDefinition(values.length > 1);
+    }
+
+    public double getDouble() throws ValueFormatException, RepositoryException {
+        if (values.length > 1) {
+            throw new ValueFormatException();
+        }
+        return values[0].getDouble();
+    }
+
+    public long getLength() throws ValueFormatException, RepositoryException {
+        return 0;
+    }
+
+    public long[] getLengths() throws ValueFormatException, RepositoryException {
+        return null;
+    }
+
+    public long getLong() throws ValueFormatException, RepositoryException {
+        if (values.length > 1) {
+            throw new ValueFormatException();
+        }
+        return values[0].getLong();
+    }
+
+    public Node getNode() throws ValueFormatException, RepositoryException {
+        return null;
+    }
+
+    public InputStream getStream() throws ValueFormatException, RepositoryException {
+        if (values.length > 1) {
+            throw new ValueFormatException();
+        }
+        return values[0].getStream();
+    }
+
+    public String getString() throws ValueFormatException, RepositoryException {
+        if(values.length > 0) return values[0].getString();
+        return null;
+    }
+
+    public int getType() throws RepositoryException {
+        return PropertyType.STRING;
+    }
+
+    public Value getValue() throws ValueFormatException, RepositoryException {
+        if (values.length > 1) {
+            throw new ValueFormatException();
+        }
+        return values[0];
+    }
+
+    public Value[] getValues() throws ValueFormatException, RepositoryException {
+        return values;
+    }
+
+    public void setValue(boolean value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+      MockValue val = new MockValue();
+      val.setValue(value);
+      values = new MockValue[1];
+      values[0] = val;
+    }
+
+    public void setValue(Calendar value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+      MockValue val = new MockValue();
+      val.setValue(value);
+      values = new MockValue[1];
+      values[0] = val;
+    }
+
+    public void setValue(double value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+      MockValue val = new MockValue();
+      val.setValue(value);
+      values = new MockValue[1];
+      values[0] = val;
+    }
+
+    public void setValue(InputStream value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+      MockValue val = new MockValue();
+      val.setValue(value);
+      values = new MockValue[1];
+      values[0] = val;
+    }
+
+    public void setValue(long value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+      MockValue val = new MockValue();
+      val.setValue(value);
+      values = new MockValue[1];
+      values[0] = val;
+    }
+
+    public void setValue(Node value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+    }
+
+    public void setValue(String value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        values =new MockValue[1];
+        values[0] = new MockValue(value);
+    }
+
+    public void setValue(String[] inputValues) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+        this.values = new MockValue[inputValues.length];
+        int i = 0;
+        for(String str : inputValues) {
+            values[i++] = new MockValue(str);
+        }
+    }
+
+    public void setValue(Value value) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+      values = new Value[1];
+      values[0] = value;
+    }
+
+    public void setValue(Value[] values) throws ValueFormatException, VersionException, LockException,
+            ConstraintViolationException, RepositoryException {
+      this.values = values;
+    }
+
+    public void accept(ItemVisitor visitor) throws RepositoryException {
+    }
+
+    public Item getAncestor(int depth) throws ItemNotFoundException, AccessDeniedException, RepositoryException {
+        return null;
+    }
+
+    public int getDepth() throws RepositoryException {
+        return 0;
+    }
+
+    public String getName() throws RepositoryException {
+        return name;
+    }
+
+    public Node getParent() throws ItemNotFoundException, AccessDeniedException, RepositoryException {
+        return null;
+    }
+
+    public String getPath() throws RepositoryException {
+        return null;
+    }
+
+    public Session getSession() throws RepositoryException {
+        return null;
+    }
+
+    public boolean isModified() {
+        return false;
+    }
+
+    public boolean isNew() {
+        return false;
+    }
+
+    public boolean isNode() {
+        return false;
+    }
+
+    public boolean isSame(Item otherItem) throws RepositoryException {
+        return false;
+    }
+
+    public void refresh(boolean keepChanges) throws InvalidItemStateException, RepositoryException {
+    }
+
+    public void remove() throws VersionException, LockException, ConstraintViolationException, RepositoryException {
+    }
+
+    public void save() throws AccessDeniedException, ItemExistsException, ConstraintViolationException,
+            InvalidItemStateException, ReferentialIntegrityException, VersionException, LockException,
+            NoSuchNodeTypeException, RepositoryException {
+    }
+
+    // JCR 2.0 methods
+
+    public Binary getBinary() throws ValueFormatException, RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public BigDecimal getDecimal() throws ValueFormatException,
+            RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public Property getProperty() throws ItemNotFoundException,
+            ValueFormatException, RepositoryException {
+        // TODO Auto-generated method stub
+        return null;
+    }
+
+    public boolean isMultiple() throws RepositoryException {
+        // TODO Auto-generated method stub
+        return false;
+    }
+
+    public void setValue(BigDecimal value) throws ValueFormatException,
+            VersionException, LockException, ConstraintViolationException,
+            RepositoryException {
+        // TODO Auto-generated method stub
+
+    }
+
+    public void setValue(Binary value) throws ValueFormatException,
+            VersionException, LockException, ConstraintViolationException,
+            RepositoryException {
+        // TODO Auto-generated method stub
+
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockPropertyDefinition.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockPropertyDefinition.java
new file mode 100644
index 0000000..864f961
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockPropertyDefinition.java
@@ -0,0 +1,87 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import javax.jcr.Value;
+import javax.jcr.nodetype.NodeType;
+import javax.jcr.nodetype.PropertyDefinition;
+
+public class MockPropertyDefinition implements PropertyDefinition {
+
+    private boolean multiple;
+
+    public MockPropertyDefinition(boolean multiple) {
+        this.multiple = multiple;
+
+    }
+    public Value[] getDefaultValues() {
+        return null;
+    }
+
+    public int getRequiredType() {
+        return 0;
+    }
+
+    public String[] getValueConstraints() {
+        return null;
+    }
+
+    public boolean isMultiple() {
+        return multiple;
+    }
+
+    public NodeType getDeclaringNodeType() {
+        return null;
+    }
+
+    public String getName() {
+        return null;
+    }
+
+    public int getOnParentVersion() {
+        return 0;
+    }
+
+    public boolean isAutoCreated() {
+        return false;
+    }
+
+    public boolean isMandatory() {
+        return false;
+    }
+
+    public boolean isProtected() {
+        return false;
+    }
+
+    // JCR 2.0 methods
+
+    public String[] getAvailableQueryOperators() {
+        // TODO Auto-generated method stub
+        return null;
+    }
+    public boolean isFullTextSearchable() {
+        // TODO Auto-generated method stub
+        return false;
+    }
+    public boolean isQueryOrderable() {
+        // TODO Auto-generated method stub
+        return false;
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockPropertyIterator.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockPropertyIterator.java
new file mode 100644
index 0000000..c141af4
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockPropertyIterator.java
@@ -0,0 +1,61 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import java.util.Iterator;
+
+import javax.jcr.Property;
+import javax.jcr.PropertyIterator;
+
+public class MockPropertyIterator implements PropertyIterator {
+
+    private final Iterator<Property> iterator;
+    int index = 0;
+    
+    public MockPropertyIterator(Iterator <Property> it) {
+        iterator = it;
+    }
+    
+    public Property nextProperty() {
+        return iterator.next();
+    }
+
+    public long getPosition() {
+        return -1;
+    }
+
+    public long getSize() {
+        return -1;
+    }
+
+    public void skip(long skipNum) {
+    }
+
+    public boolean hasNext() {
+        return iterator.hasNext();
+    }
+
+    public Object next() {
+        return nextProperty();
+    }
+
+    public void remove() {
+        throw new Error("Not implemented: remove");
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockValue.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockValue.java
new file mode 100644
index 0000000..8a1774b
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/MockValue.java
@@ -0,0 +1,114 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import java.io.InputStream;
+import java.math.BigDecimal;
+import java.util.Calendar;
+
+import javax.jcr.Binary;
+import javax.jcr.PropertyType;
+import javax.jcr.RepositoryException;
+import javax.jcr.Value;
+import javax.jcr.ValueFormatException;
+
+public class MockValue implements Value {
+
+    private String stringValue;
+    private boolean booleanValue;
+    private Calendar calendarValue;
+    private double doubleValue;
+    private long longValue;
+    private InputStream stream;
+    private int propertyType;
+    private BigDecimal decimal;
+
+    public MockValue() {
+    }
+
+    public MockValue(String str) {
+        stringValue = str;
+        propertyType = PropertyType.STRING;
+    }
+
+    public boolean getBoolean() throws ValueFormatException, IllegalStateException, RepositoryException {
+        return booleanValue;
+    }
+
+    public Calendar getDate() throws ValueFormatException, IllegalStateException, RepositoryException {
+        return calendarValue;
+    }
+
+    public double getDouble() throws ValueFormatException, IllegalStateException, RepositoryException {
+        return doubleValue;
+    }
+
+    public long getLong() throws ValueFormatException, IllegalStateException, RepositoryException {
+        return longValue;
+    }
+
+    public InputStream getStream() throws IllegalStateException, RepositoryException {
+        return stream;
+    }
+
+    public String getString() throws ValueFormatException, IllegalStateException, RepositoryException {
+        return stringValue;
+    }
+
+    public int getType() {
+        return propertyType;
+    }
+
+
+    public void setValue(String stringValue) {
+      this.stringValue = stringValue;
+    }
+
+    public void setValue(boolean booleanValue) {
+      this.booleanValue = booleanValue;
+    }
+
+    public void setValue(Calendar calendarValue) {
+      this.calendarValue = calendarValue;
+    }
+
+    public void setValue(double doubleValue) {
+      this.doubleValue = doubleValue;
+    }
+
+    public void setValue(long longValue) {
+      this.longValue = longValue;
+    }
+
+    public void setValue(InputStream stream) {
+      this.stream = stream;
+    }
+
+    public void setDecimal(BigDecimal value) {
+        this.decimal = value;
+    }
+
+    public Binary getBinary() throws RepositoryException {
+        return null;
+    }
+
+    public BigDecimal getDecimal() throws ValueFormatException, RepositoryException {
+        return decimal;
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/RepositoryProvider.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/RepositoryProvider.java
new file mode 100644
index 0000000..1ab2742
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/RepositoryProvider.java
@@ -0,0 +1,65 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import javax.jcr.RepositoryException;
+
+import org.apache.sling.jcr.api.SlingRepository;
+
+/** Provide a Repository to test classes, on-demand,
+ *  with auto-cleanup at JVM shutdown.
+ */
+public class RepositoryProvider {
+    private static RepositoryProvider INSTANCE;
+    private SlingRepository repository;
+    
+    private static class ShutdownThread extends Thread {
+        @Override
+        public void run() {
+            try {
+                RepositoryUtil.stopRepository();
+            } catch(Exception e) {
+                System.out.println("Exception in ShutdownThread:" + e);
+            }
+        }
+        
+    };
+    
+    private RepositoryProvider() {
+    }
+    
+    public synchronized static RepositoryProvider instance() {
+        if(INSTANCE == null) {
+            INSTANCE = new RepositoryProvider();
+        }
+        return INSTANCE;
+    }
+    
+    /** Return a SlingRepository. First call initializes it, and a JVM
+    *  shutdown hook is registered to stop it.
+    **/
+    public synchronized SlingRepository getRepository() throws RepositoryException {
+        if(repository == null) {
+            RepositoryUtil.startRepository();
+            repository = RepositoryUtil.getRepository();
+            Runtime.getRuntime().addShutdownHook(new ShutdownThread());
+        }
+        return repository;
+    }
+}
\ No newline at end of file
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/RepositoryTestBase.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/RepositoryTestBase.java
new file mode 100644
index 0000000..3c990b4
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/RepositoryTestBase.java
@@ -0,0 +1,69 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.naming.NamingException;
+
+import junit.framework.TestCase;
+
+import org.apache.sling.jcr.api.SlingRepository;
+
+/** Base class for JUnit3-style tests which need a Repository. 
+ *  Should eventually be deprecated in favor of {@link RepositoryProvider}
+ *  which is less intrusive
+ */
+public class RepositoryTestBase extends TestCase {
+    protected Node testRoot;
+    protected Session session;
+    private int counter;
+    
+    @Override
+    protected void tearDown() throws Exception {
+        super.tearDown();
+        if(session != null) {
+            session.logout();
+        }
+    }
+
+    /** Return a JCR Session, initialized on demand */ 
+    protected Session getSession() throws RepositoryException, NamingException {
+        if(session == null) {
+            session = getRepository().loginAdministrative(null);
+        }
+        return session;
+    }
+    
+    /** Return a test root node, created on demand, with a unique path */ 
+    protected Node getTestRootNode() throws RepositoryException, NamingException {
+        if(testRoot==null) {
+            final Node root = getSession().getRootNode();
+            final Node classRoot = root.addNode(getClass().getSimpleName()); 
+            testRoot = classRoot.addNode(System.currentTimeMillis() + "_" + (++counter));
+        }
+        return testRoot;
+    }
+
+    /** Return a Repository */
+    protected SlingRepository getRepository() throws RepositoryException, NamingException {
+        return RepositoryProvider.instance().getRepository();
+    }
+}
\ No newline at end of file
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/jcr/RepositoryUtil.java b/testing/src/main/java/org/apache/sling/commons/testing/jcr/RepositoryUtil.java
new file mode 100644
index 0000000..1981bfc
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/jcr/RepositoryUtil.java
@@ -0,0 +1,230 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+
+import javax.jcr.Credentials;
+import javax.jcr.LoginException;
+import javax.jcr.NoSuchWorkspaceException;
+import javax.jcr.Repository;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.SimpleCredentials;
+import javax.jcr.Value;
+import javax.naming.NamingException;
+
+import org.apache.jackrabbit.commons.JcrUtils;
+import org.apache.jackrabbit.commons.cnd.CndImporter;
+import org.apache.sling.jcr.api.SlingRepository;
+
+/**
+ * Utility class for managing JCR repositories, used to initialize temporary
+ * Jackrabbit repositories for testing.
+ */
+public class RepositoryUtil {
+
+    public static final String ADMIN_NAME = "admin";
+
+    public static final String ADMIN_PASSWORD = "admin";
+
+    public static final String HOME_DIR = "target/repository";
+
+    public static final String CONFIG_FILE = "jackrabbit-test-config.xml";
+
+    private static SlingRepository repository;
+
+    private static Session adminSession;
+
+    /**
+     * Start a new repository
+     *
+     * @throws RepositoryException when it is not possible to start the
+     *             repository.
+     */
+    public static void startRepository() throws RepositoryException {
+        if ( adminSession == null ) {
+            // copy the repository configuration file to the repository HOME_DIR
+            InputStream ins = RepositoryUtil.class.getClassLoader().getResourceAsStream(
+                CONFIG_FILE);
+            if (ins == null) {
+                throw new RepositoryException("Cannot get " + CONFIG_FILE);
+            }
+
+            File configFile = new File(HOME_DIR, "repository.xml");
+            configFile.getParentFile().mkdirs();
+
+            FileOutputStream out = null;
+            try {
+                out = new FileOutputStream(configFile);
+                byte[] buf = new byte[1024];
+                int rd;
+                while ((rd = ins.read(buf)) >= 0) {
+                    out.write(buf, 0, rd);
+                }
+            } catch (IOException ioe) {
+                throw new RepositoryException("Cannot copy configuration file to "
+                    + configFile);
+            } finally {
+                try {
+                    ins.close();
+                } catch (IOException ignore) {
+                }
+                if (out != null) {
+                    try {
+                        out.close();
+                    } catch (IOException ignore) {
+                    }
+                }
+            }
+
+            // somewhat dirty hack to have the derby.log file in a sensible
+            // location, but don't overwrite anything already set
+            if (System.getProperty("derby.stream.error.file") == null) {
+                String derbyLog = HOME_DIR + "/derby.log";
+                System.setProperty("derby.stream.error.file", derbyLog);
+            }
+
+            final File f = new File(HOME_DIR);
+            repository = new RepositoryWrapper(JcrUtils.getRepository(f.toURI().toString()));
+            adminSession = repository.loginAdministrative(null);
+        }
+    }
+
+    /**
+     * Stop a repository.
+     */
+    public static void stopRepository() throws NamingException {
+        if ( adminSession != null ) {
+            adminSession.logout();
+            adminSession = null;
+            repository = null;
+        }
+    }
+
+    /**
+     * Get a repository
+     *
+     * @return a JCR repository reference
+     */
+    public static SlingRepository getRepository() {
+        return repository;
+    }
+
+    /**
+     * Registers node types from the CND file read from the <code>source</code>
+     * with the node type manager available from the given <code>session</code>.
+     * <p>
+     * This method is not synchronized. It is up to the calling method to
+     * prevent paralell execution.
+     *
+     * @param session The <code>Session</code> providing the node type manager
+     *            through which the node type is to be registered.
+     * @param source The <code>InputStream</code> from which the CND file is
+     *            read.
+     * @return <code>true</code> if registration of all node types succeeded.
+     */
+    public static boolean registerNodeType(Session session, InputStream source)
+            throws IOException, RepositoryException {
+        try {
+            CndImporter.registerNodeTypes(new InputStreamReader(source, "UTF-8"), session);
+            return true;
+        } catch (Exception e) {
+            // ignore
+            return false;
+        }
+    }
+
+    public static void registerSlingNodeTypes(Session adminSession) throws IOException, RepositoryException {
+        final Class<RepositoryUtil> clazz = RepositoryUtil.class;
+        registerNodeType(adminSession,
+                clazz.getResourceAsStream("/SLING-INF/nodetypes/folder.cnd"));
+        RepositoryUtil.registerNodeType(adminSession,
+                clazz.getResourceAsStream("/SLING-INF/nodetypes/resource.cnd"));
+        RepositoryUtil.registerNodeType(adminSession,
+                clazz.getResourceAsStream("/SLING-INF/nodetypes/vanitypath.cnd"));
+    }
+
+    public static final class RepositoryWrapper implements SlingRepository {
+
+        protected final Repository wrapped;
+
+        public RepositoryWrapper(Repository r) {
+            wrapped = r;
+        }
+
+        public String getDescriptor(String key) {
+            return wrapped.getDescriptor(key);
+        }
+
+        public String[] getDescriptorKeys() {
+            return wrapped.getDescriptorKeys();
+        }
+
+        public Session login() throws LoginException, RepositoryException {
+            return wrapped.login();
+        }
+
+        public Session login(Credentials credentials, String workspaceName)
+                throws LoginException, NoSuchWorkspaceException,
+                RepositoryException {
+            return wrapped.login(credentials, (workspaceName == null ? getDefaultWorkspace() : workspaceName));
+        }
+
+        public Session login(Credentials credentials) throws LoginException,
+                RepositoryException {
+            return wrapped.login(credentials);
+        }
+
+        public Session login(String workspaceName) throws LoginException,
+                NoSuchWorkspaceException, RepositoryException {
+            return wrapped.login((workspaceName == null ? getDefaultWorkspace() : workspaceName));
+        }
+
+        public String getDefaultWorkspace() {
+            return "default";
+        }
+
+        public Session loginAdministrative(String workspaceName)
+                throws RepositoryException {
+            final Credentials credentials = new SimpleCredentials(ADMIN_NAME,
+                ADMIN_PASSWORD.toCharArray());
+            return this.login(credentials, (workspaceName == null ? getDefaultWorkspace() : workspaceName));
+        }
+
+        public Value getDescriptorValue(String key) {
+            return wrapped.getDescriptorValue(key);
+        }
+
+        public Value[] getDescriptorValues(String key) {
+            return wrapped.getDescriptorValues(key);
+        }
+
+        public boolean isSingleValueDescriptor(String key) {
+            return wrapped.isSingleValueDescriptor(key);
+        }
+
+        public boolean isStandardDescriptor(String key) {
+            return wrapped.isStandardDescriptor(key);
+        }
+
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/junit/Retry.java b/testing/src/main/java/org/apache/sling/commons/testing/junit/Retry.java
new file mode 100644
index 0000000..219547a
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/junit/Retry.java
@@ -0,0 +1,33 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.junit;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/** Used to annotate JUnit tests as being retryable */
+@Retention(RetentionPolicy.RUNTIME)
+public @interface Retry {
+    
+    /** Retries for at most this many milliseconds */ 
+    int timeoutMsec() default -1;
+    
+    /** Wait this many milliseconds between retries */
+    int intervalMsec() default -1;
+}	
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/junit/RetryRule.java b/testing/src/main/java/org/apache/sling/commons/testing/junit/RetryRule.java
new file mode 100644
index 0000000..d2d13ad
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/junit/RetryRule.java
@@ -0,0 +1,112 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.junit;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** JUnit Rule that implements retries, see tests for usage example */
+public class RetryRule implements TestRule {
+    
+    public static final int DEFAULT_DEFAULT_TIMEOUT_MSEC = 5000;
+    public static final int DEFAULT_DEFAULT_INTERVAL_MSEC = 500;
+    
+    private static final Logger log = LoggerFactory.getLogger(RetryRule.class);
+    
+    private final long defaultTimeout;
+    private final long defaultInterval;
+
+    /** Create a RetryRule with default values for default timeout and interval */
+    public RetryRule() {
+        this(DEFAULT_DEFAULT_TIMEOUT_MSEC, DEFAULT_DEFAULT_INTERVAL_MSEC);
+    }
+    
+    /** Create a RetryRule with specific values for default timeout and interval */
+    public RetryRule(long defaultTimeout, long defaultInterval) {
+        this.defaultTimeout = defaultTimeout;
+        this.defaultInterval = defaultInterval;
+    }
+    
+    @Override
+    public String toString() {
+        return new StringBuilder()
+        .append(getClass().getSimpleName())
+        .append(", default interval=")
+        .append(defaultInterval)
+        .append(" msec, default timeout=")
+        .append(defaultTimeout)
+        .append(" msec")
+        .toString();
+    }
+    
+    public Statement apply(final Statement statement, final Description description) {
+        return new Statement() {
+            
+            private Throwable eval(Statement stmt) {
+                try {
+                    stmt.evaluate();
+                } catch(Throwable t) {
+                    return t;
+                }
+                return null;
+            }
+            
+            @Override
+            public void evaluate() throws Throwable {
+                int retries = 0;
+                Throwable t = eval(statement);
+                if(t != null) {
+                    final Retry r = description.getAnnotation(Retry.class);
+                    if(r != null) {
+                        final long timeout = System.currentTimeMillis() + getTimeout(r.timeoutMsec());
+                        while(System.currentTimeMillis() < timeout) {
+                            retries++;
+                            t = eval(statement);
+                            if(t == null) {
+                                break;
+                            }
+                            Thread.sleep(getInterval(r.intervalMsec()));
+                        }
+                    }
+                }
+                if(t != null) {
+                    if(retries > 0) {
+                        log.debug("{} fails after retrying {} time(s)", statement, retries);
+                    }
+                    throw t;
+                }
+                if(retries > 0) {
+                    log.debug("{} succeeds after retrying {} time(s)", statement, retries);
+                }
+            }
+        };
+    }
+    
+    long getTimeout(long ruleTimeout) {
+        return ruleTimeout > 0 ? ruleTimeout : defaultTimeout;
+    }
+    
+    long getInterval(long ruleInterval) {
+        return ruleInterval > 0 ? ruleInterval : defaultInterval;
+    }
+    
+}
\ No newline at end of file
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/junit/categories/JackrabbitOnly.java b/testing/src/main/java/org/apache/sling/commons/testing/junit/categories/JackrabbitOnly.java
new file mode 100644
index 0000000..5ffae5b
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/junit/categories/JackrabbitOnly.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.junit.categories;
+
+/** JUnit Category for tests that run on Jackrabbit only */
+public interface JackrabbitOnly {
+}
\ No newline at end of file
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/junit/categories/OakOnly.java b/testing/src/main/java/org/apache/sling/commons/testing/junit/categories/OakOnly.java
new file mode 100644
index 0000000..58354bc
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/junit/categories/OakOnly.java
@@ -0,0 +1,23 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.junit.categories;
+
+/** JUnit Category for tests that run on Oak only */
+public interface OakOnly {    
+}
\ No newline at end of file
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockBundle.java b/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockBundle.java
new file mode 100644
index 0000000..0188162
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockBundle.java
@@ -0,0 +1,143 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.osgi;
+
+import java.io.InputStream;
+import java.net.URL;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Map;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.Version;
+
+public class MockBundle implements Bundle {
+
+    private long bundleId;
+
+    public MockBundle(long bundleId) {
+        this.bundleId = bundleId;
+    }
+
+    public long getBundleId() {
+        return bundleId;
+    }
+
+    public Enumeration<?> findEntries(String path, String filePattern,
+            boolean recurse) {
+        return null;
+    }
+
+    public URL getEntry(String name) {
+        return null;
+    }
+
+    public Enumeration<?> getEntryPaths(String path) {
+        return null;
+    }
+
+    public Dictionary<?, ?> getHeaders() {
+        return null;
+    }
+
+    public Dictionary<?, ?> getHeaders(String locale) {
+        return null;
+    }
+
+    public long getLastModified() {
+        return 0;
+    }
+
+    public String getLocation() {
+        return null;
+    }
+
+    public ServiceReference[] getRegisteredServices() {
+        return null;
+    }
+
+    public URL getResource(String name) {
+        return null;
+    }
+
+    public Enumeration<?> getResources(String name) {
+        return null;
+    }
+
+    public ServiceReference[] getServicesInUse() {
+        return null;
+    }
+
+    public int getState() {
+        return 0;
+    }
+
+    public String getSymbolicName() {
+        return null;
+    }
+
+    public boolean hasPermission(Object permission) {
+        return false;
+    }
+
+    public Class<?> loadClass(String name) throws ClassNotFoundException {
+        throw new ClassNotFoundException(name);
+    }
+
+    public void start() {
+
+    }
+
+    public void stop() {
+
+    }
+
+    public void uninstall() {
+
+    }
+
+    public void update() {
+
+    }
+
+    public void update(InputStream in) {
+
+    }
+
+    public BundleContext getBundleContext() {
+        return null;
+    }
+
+    public void start(int options) throws BundleException {
+    }
+
+    public void stop(int options) throws BundleException {
+    }
+
+    public Map getSignerCertificates(int signersType) {
+        return null;
+    }
+
+    public Version getVersion() {
+        return null;
+    }
+}
\ No newline at end of file
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockBundleContext.java b/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockBundleContext.java
new file mode 100644
index 0000000..168b748
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockBundleContext.java
@@ -0,0 +1,157 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.osgi;
+
+import java.io.File;
+import java.io.InputStream;
+import java.util.Dictionary;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.BundleException;
+import org.osgi.framework.BundleListener;
+import org.osgi.framework.Filter;
+import org.osgi.framework.FrameworkListener;
+import org.osgi.framework.InvalidSyntaxException;
+import org.osgi.framework.ServiceListener;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+
+public class MockBundleContext implements BundleContext {
+    private MockBundle bundle;
+
+    public MockBundleContext(MockBundle bundle) {
+        this.bundle = bundle;
+    }
+
+    public String getProperty(String s) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public Bundle getBundle() {
+        return bundle;
+
+    }
+
+    public Bundle installBundle(String s) throws BundleException {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public Bundle installBundle(String s, InputStream inputStream)
+            throws BundleException {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public Bundle getBundle(long l) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public Bundle[] getBundles() {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public void addServiceListener(ServiceListener serviceListener, String s)
+            throws InvalidSyntaxException {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public void addServiceListener(ServiceListener serviceListener) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public void removeServiceListener(ServiceListener serviceListener) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public void addBundleListener(BundleListener bundleListener) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public void removeBundleListener(BundleListener bundleListener) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public void addFrameworkListener(FrameworkListener frameworkListener) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public void removeFrameworkListener(FrameworkListener frameworkListener) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public ServiceRegistration registerService(String[] strings, Object o,
+            Dictionary dictionary) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public ServiceRegistration registerService(String s, Object o,
+            Dictionary dictionary) {
+        return new MockServiceRegistration();
+    }
+
+    public ServiceReference[] getServiceReferences(String s, String s1)
+            throws InvalidSyntaxException {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public ServiceReference[] getAllServiceReferences(String s, String s1)
+            throws InvalidSyntaxException {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public ServiceReference getServiceReference(String s) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public Object getService(ServiceReference serviceReference) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public boolean ungetService(ServiceReference serviceReference) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public File getDataFile(String s) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public Filter createFilter(String s) throws InvalidSyntaxException {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockComponentContext.java b/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockComponentContext.java
new file mode 100644
index 0000000..d31993c
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockComponentContext.java
@@ -0,0 +1,116 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.osgi;
+
+import java.util.Dictionary;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Properties;
+
+import javax.servlet.Servlet;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.service.component.ComponentContext;
+import org.osgi.service.component.ComponentInstance;
+
+public class MockComponentContext implements ComponentContext {
+
+    private Dictionary<Object, Object> properties = new Properties();
+
+    private MockBundleContext mockBundleContext;
+
+    private Servlet servlet;
+
+    private Map<ServiceReference, Object> services = new HashMap<ServiceReference, Object>();
+
+    public MockComponentContext(MockBundle bundle) {
+        mockBundleContext = new MockBundleContext(bundle);
+    }
+    
+    public MockComponentContext(MockBundleContext mockBundleContext) {
+        this.mockBundleContext = mockBundleContext;
+    }
+
+    public MockComponentContext(MockBundle bundle, Servlet servlet) {
+        mockBundleContext = new MockBundleContext(bundle);
+        this.servlet = servlet;
+    }
+    
+    public MockComponentContext(MockBundleContext mockBundleContext, Servlet servlet) {
+        this.mockBundleContext = mockBundleContext;
+        this.servlet = servlet;
+    }
+
+    public void addService(ServiceReference reference, Object service) {
+        services.put(reference, service);
+    }
+
+
+    public void setProperty(Object key, Object value) {
+        // noinspection unchecked
+        this.properties.put(key, value);
+    }
+
+    public Dictionary<Object, Object> getProperties() {
+        // noinspection ReturnOfCollectionOrArrayField
+        return this.properties;
+    }
+
+    public Object locateService(String name, ServiceReference reference) {
+        // the constant is from Sling Core, but we don't want to have a dep just because of this
+        String referenceName = (String) reference.getProperty("sling.core.servletName");
+        if (referenceName != null && referenceName.equals(name)) {
+            return this.servlet;
+        }
+
+        return services.get(reference);
+    }
+
+    public BundleContext getBundleContext() {
+        return mockBundleContext;
+    }
+
+    public void disableComponent(String name) {
+    }
+
+    public void enableComponent(String name) {
+    }
+
+    public ComponentInstance getComponentInstance() {
+        return null;
+    }
+
+    public ServiceReference getServiceReference() {
+        return null;
+    }
+
+    public Bundle getUsingBundle() {
+        return null;
+    }
+
+    public Object locateService(String name) {
+        return null;
+    }
+
+    public Object[] locateServices(String name) {
+        return null;
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockServiceReference.java b/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockServiceReference.java
new file mode 100644
index 0000000..5439b50
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockServiceReference.java
@@ -0,0 +1,73 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.osgi;
+
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Hashtable;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.Constants;
+import org.osgi.framework.ServiceReference;
+
+public class MockServiceReference implements ServiceReference {
+
+    private static long serviceIdCounter = 0;
+
+    private Bundle bundle;
+
+    private Dictionary<String, Object> props;
+
+    public MockServiceReference(Bundle bundle) {
+        this.bundle = bundle;
+        this.props = new Hashtable<String, Object>();
+
+        // mockup a service id
+        props.put(Constants.SERVICE_ID, serviceIdCounter++);
+    }
+
+    public Bundle getBundle() {
+        return bundle;
+    }
+
+    public void setProperty(String key, Object value) {
+        props.put(key, value);
+    }
+
+    public Object getProperty(String key) {
+        return props.get(key);
+    }
+
+    public String[] getPropertyKeys() {
+        return Collections.list(props.keys()).toArray(new String[props.size()]);
+    }
+
+    public Bundle[] getUsingBundles() {
+        return null;
+    }
+
+    public boolean isAssignableTo(Bundle bundle, String className) {
+        return false;
+    }
+
+    public int compareTo(Object reference) {
+        return 0;
+    }
+
+}
\ No newline at end of file
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockServiceRegistration.java b/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockServiceRegistration.java
new file mode 100644
index 0000000..70557f1
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/osgi/MockServiceRegistration.java
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.osgi;
+
+import java.util.Dictionary;
+
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.ServiceRegistration;
+
+class MockServiceRegistration implements ServiceRegistration {
+
+    public ServiceReference getReference() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public void setProperties(Dictionary dictionary) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public void unregister() {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/sling/MockRequestPathInfo.java b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockRequestPathInfo.java
new file mode 100644
index 0000000..12e2645
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockRequestPathInfo.java
@@ -0,0 +1,70 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.sling;
+
+import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.api.resource.Resource;
+
+class MockRequestPathInfo implements RequestPathInfo {
+
+    public Resource getSuffixResource() {
+        return null;
+    }
+
+    private final String selectors;
+
+    private final String extension;
+
+    private final String suffix;
+
+	private final String path;
+
+	public MockRequestPathInfo(String selectors, String extension, String suffix) {
+		this(selectors, extension, suffix, null);
+	}
+
+	public MockRequestPathInfo(String selectors, String extension, String suffix, String path) {
+		this.selectors = selectors;
+		this.extension = extension;
+		this.suffix = suffix;
+		this.path = path;
+	}
+
+    public String getExtension() {
+        return extension;
+    }
+
+    public String getResourcePath() {
+        return path;
+    }
+
+    public String getSelectorString() {
+        return selectors;
+    }
+
+    public String[] getSelectors() {
+        return (getSelectorString() != null)
+                ? getSelectorString().split("\\.")
+                : new String[0];
+    }
+
+    public String getSuffix() {
+        return suffix;
+    }
+}
\ No newline at end of file
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/sling/MockRequestProgressTracker.java b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockRequestProgressTracker.java
new file mode 100644
index 0000000..41c43e2
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockRequestProgressTracker.java
@@ -0,0 +1,53 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.sling;
+
+import java.io.PrintWriter;
+import java.util.Collections;
+import java.util.Iterator;
+
+import org.apache.sling.api.request.RequestProgressTracker;
+
+public class MockRequestProgressTracker implements RequestProgressTracker {
+
+    public void dump(PrintWriter writer) {
+    }
+
+    public Iterator<String> getMessages() {
+        return Collections.<String> emptyList().iterator();
+    }
+
+    public void log(String message) {
+    }
+
+    public void log(String format, Object... args) {
+    }
+
+    public void logTimer(String timerName) {
+    }
+
+    public void logTimer(String timerName, String format, Object... args) {
+    }
+
+    public void startTimer(String timerName) {
+    }
+
+    public void done() {
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/sling/MockResource.java b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockResource.java
new file mode 100644
index 0000000..d74d737
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockResource.java
@@ -0,0 +1,91 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.sling;
+
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.SyntheticResource;
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class MockResource extends SyntheticResource {
+
+    private String resourceType;
+    private String resourceSuperType;
+    private Map<String,Object> properties = new HashMap<String,Object>();
+
+    public MockResource(ResourceResolver resourceResolver, String path,
+            String resourceType) {
+        this(resourceResolver, path, resourceType, null);
+    }
+
+    public MockResource(ResourceResolver resourceResolver, String path,
+            String resourceType, String resourceSuperType) {
+        super(resourceResolver, path, resourceType);
+
+        setResourceType(resourceType);
+        setResourceSuperType(resourceSuperType);
+    }
+
+    public void addProperty(String key, Object value){
+        this.properties.put(key,value);
+    }
+
+    public Map<String,Object> getProperties(){
+        return this.properties;
+    }
+
+    @Override
+    public String getResourceType() {
+        return resourceType;
+    }
+
+    public void setResourceType(String resourceType) {
+        this.resourceType = resourceType;
+    }
+
+    @Override
+    public String getResourceSuperType() {
+        return resourceSuperType;
+    }
+
+    public void setResourceSuperType(String resourceSuperType) {
+        this.resourceSuperType = resourceSuperType;
+    }
+
+    @SuppressWarnings("unchecked")
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        if (type == ValueMap.class) {
+            ValueMap map = new ValueMapDecorator(new HashMap<String, Object>());
+            if (resourceType != null) {
+                map.put("resourceType", resourceType);
+            }
+            if (resourceSuperType != null) {
+                map.put("resourceSuperType", resourceSuperType);
+            }
+            for (String key : this.properties.keySet()) {
+                map.put(key,this.properties.get(key));
+            }
+            return (AdapterType) map;
+        }
+        throw new UnsupportedOperationException("AdaptTo " + type.getSimpleName() + " not implemented");
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/sling/MockResourceResolver.java b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockResourceResolver.java
new file mode 100644
index 0000000..f0ba860
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockResourceResolver.java
@@ -0,0 +1,230 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.sling;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.NoSuchElementException;
+
+import javax.servlet.http.HttpServletRequest;
+
+import org.apache.sling.api.resource.LoginException;
+import org.apache.sling.api.resource.PersistenceException;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+
+/**
+ * @deprecated Please use the Mock Resource Resolver Implementation from testing/resourceresolver-mock instead.
+ */
+@Deprecated
+public class MockResourceResolver implements ResourceResolver {
+
+    private String[] searchPath;
+
+    private Map<String, Resource> resources = new LinkedHashMap<String, Resource>();
+
+    private Map<String, Collection<Resource>> children = new LinkedHashMap<String, Collection<Resource>>();
+
+    public void addResource(Resource resource) {
+        this.resources.put(resource.getPath(), resource);
+    }
+
+    public void addChildren(Resource parent, Collection<Resource> children) {
+        this.children.put(parent.getPath(), children);
+    }
+
+    public Resource resolve(HttpServletRequest request) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public Resource resolve(String absPath) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public String map(String resourcePath) {
+        return resourcePath;	// a rather simplistic 1:1 map...
+
+    }
+
+    public Resource getResource(String path) {
+        return resources.get(path);
+    }
+
+    public Resource getResource(Resource base, String path) {
+        if (!path.startsWith("/")) {
+            path = base.getPath() + "/" + path;
+        }
+        return getResource(path);
+    }
+
+    public String[] getSearchPath() {
+        return searchPath.clone();
+
+    }
+
+    public Iterator<Resource> listChildren(final Resource parent) {
+        Collection<Resource> childCollection = children.get(parent.getPath());
+        if (childCollection != null) {
+            return childCollection.iterator();
+        }
+
+        return new Iterator<Resource>() {
+            final String parentPath = parent.getPath() + "/";
+
+            final Iterator<Resource> elements = resources.values().iterator();
+
+            Resource nextResource = seek();
+
+            public boolean hasNext() {
+                return nextResource != null;
+            }
+
+            public Resource next() {
+                if (!hasNext()) {
+                    throw new NoSuchElementException();
+                }
+
+                Resource result = nextResource;
+                nextResource = seek();
+                return result;
+            }
+
+            public void remove() {
+                throw new UnsupportedOperationException();
+            }
+
+            private Resource seek() {
+                while (elements.hasNext()) {
+                    Resource next = elements.next();
+                    String path = next.getPath();
+                    if (path.startsWith(parentPath)
+                        && path.indexOf('/', parentPath.length()) < 0) {
+                        return next;
+                    }
+                }
+                return null;
+            }
+        };
+    }
+
+    public Iterator<Resource> findResources(String query, String language) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public Iterator<Map<String, Object>> queryResources(String query,
+            String language) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        throw new UnsupportedOperationException("Not implemented");
+
+    }
+
+    public void setSearchPath(String... searchPath) {
+        if (searchPath == null) {
+            this.searchPath = new String[0];
+        } else {
+            this.searchPath = new String[searchPath.length];
+            for (int i=0; i < searchPath.length; i++) {
+                String entry = searchPath[i];
+                if (!entry.endsWith("/")) {
+                    entry = entry.concat("/");
+                }
+                this.searchPath[i] = entry;
+            }
+        }
+    }
+
+    public String map(HttpServletRequest request, String resourcePath) {
+		return request.getContextPath() + resourcePath;
+    }
+
+    public Resource resolve(HttpServletRequest request, String absPath) {
+        throw new UnsupportedOperationException("Not implemented");
+    }
+
+    public void close() {
+        // nothing to do
+    }
+
+    public String getUserID() {
+        return null;
+    }
+
+    public boolean isLive() {
+        return true;
+    }
+
+    public ResourceResolver clone(Map<String, Object> authenticationInfo)
+    throws LoginException {
+        return null;
+    }
+
+    public Object getAttribute(String name) {
+        return null;
+    }
+
+    public Iterator<String> getAttributeNames() {
+        return null;
+    }
+
+    public void commit() throws PersistenceException {
+    }
+
+    public Resource create(Resource arg0, String arg1, Map<String, Object> arg2)
+            throws PersistenceException {
+        return null;
+    }
+
+    public void delete(Resource arg0) throws PersistenceException {
+    }
+
+    public Iterable<Resource> getChildren(Resource arg0) {
+        return null;
+    }
+
+    public String getParentResourceType(Resource arg0) {
+        return null;
+    }
+
+    public String getParentResourceType(String arg0) {
+        return null;
+    }
+
+    public boolean hasChanges() {
+        return false;
+    }
+
+    public boolean isResourceType(Resource arg0, String arg1) {
+        return false;
+    }
+
+    public void refresh() {
+    }
+
+    public void revert() {
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/sling/MockSlingHttpServletRequest.java b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockSlingHttpServletRequest.java
new file mode 100644
index 0000000..3516fe5
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockSlingHttpServletRequest.java
@@ -0,0 +1,403 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.sling;
+
+import java.io.BufferedReader;
+import java.security.Principal;
+import java.util.Enumeration;
+import java.util.Locale;
+import java.util.Map;
+import java.util.ResourceBundle;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.ServletInputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpSession;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.request.RequestDispatcherOptions;
+import org.apache.sling.api.request.RequestParameter;
+import org.apache.sling.api.request.RequestParameterMap;
+import org.apache.sling.api.request.RequestPathInfo;
+import org.apache.sling.api.request.RequestProgressTracker;
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.SyntheticResource;
+
+/**
+ * Mock request object. This does not do anything useful, it just returns the
+ * constructor parameter <code>secure</code> in the <code>isSecure</code>
+ * method.
+ */
+public class MockSlingHttpServletRequest implements SlingHttpServletRequest {
+
+    private Resource resource;
+
+    private String method;
+
+    private final RequestPathInfo requestPathInfo;
+
+    private final String queryString;
+
+	private final String scheme;
+	private final String server;
+	private final int port;
+	private final String contextPath;
+
+    private boolean secure = false;
+
+    private ResourceResolver mockResourceResolver;
+
+    private RequestProgressTracker mockRequestProgressTracker;
+
+    public static final String RESOURCE_TYPE = "foo/bar";
+
+    MockSlingHttpServletRequest() {
+        this(null, null, null, null, null);
+    }
+
+    public MockSlingHttpServletRequest(String resourcePath, String selectors,
+            String extension, String suffix, String queryString) {
+		this(resourcePath, selectors, extension, suffix, queryString,
+			resourcePath, null, null, 0, null);
+    }
+
+	public MockSlingHttpServletRequest(String resourcePath, String selectors,
+			String extension, String suffix, String queryString,
+			String requestPath, String scheme, String server, int port,
+			String contextPath) {
+		this.resource = new SyntheticResource(null, resourcePath, RESOURCE_TYPE);
+		this.requestPathInfo = new MockRequestPathInfo(selectors, extension,
+			suffix, requestPath);
+		this.queryString = queryString;
+		this.scheme = scheme;
+		this.server = server;
+		this.port = port;
+		this.contextPath = contextPath;
+
+		setMethod(null);
+	}
+
+    public void setResourceResolver(ResourceResolver resolver) {
+        this.mockResourceResolver = resolver;
+
+        // recreate request resource with the new resolver
+        if (resource.getResourceResolver() == null) {
+            this.resource = new SyntheticResource(resolver, resource.getPath(),
+                resource.getResourceType());
+        }
+    }
+
+    public void setResource(Resource resource) {
+        this.resource = resource;
+    }
+
+    public void setSecure(boolean secure) {
+        this.secure = secure;
+    }
+
+    public void setMethod(String method) {
+        this.method = (method == null) ? "GET" : method.toUpperCase();
+    }
+
+    public Cookie getCookie(String name) {
+        return null;
+    }
+
+    public RequestDispatcher getRequestDispatcher(String path,
+            RequestDispatcherOptions options) {
+        return null;
+    }
+
+    public RequestDispatcher getRequestDispatcher(Resource resource,
+            RequestDispatcherOptions options) {
+        return null;
+    }
+
+    public RequestDispatcher getRequestDispatcher(Resource resource) {
+        return null;
+    }
+
+    public RequestParameter getRequestParameter(String name) {
+        return null;
+    }
+
+    public RequestParameterMap getRequestParameterMap() {
+        return null;
+    }
+
+    public RequestParameter[] getRequestParameters(String name) {
+        return null;
+    }
+
+    public RequestPathInfo getRequestPathInfo() {
+        return requestPathInfo;
+    }
+
+    public RequestProgressTracker getRequestProgressTracker() {
+        if (mockRequestProgressTracker == null) {
+            mockRequestProgressTracker = new MockRequestProgressTracker();
+        }
+        return mockRequestProgressTracker;
+    }
+
+    public Resource getResource() {
+        return resource;
+    }
+
+    public ResourceBundle getResourceBundle(Locale locale) {
+        return null;
+    }
+
+    public ResourceBundle getResourceBundle(String baseName, Locale locale) {
+        return null;
+    }
+
+    public ResourceResolver getResourceResolver() {
+        return mockResourceResolver;
+    }
+
+    public String getResponseContentType() {
+        return null;
+    }
+
+    public Enumeration<String> getResponseContentTypes() {
+        return null;
+    }
+
+    public String getAuthType() {
+        return null;
+    }
+
+	public String getContextPath() {
+		return contextPath;
+	}
+
+    public Cookie[] getCookies() {
+        return null;
+    }
+
+    public long getDateHeader(String name) {
+        return 0;
+    }
+
+    public String getHeader(String name) {
+        return null;
+    }
+
+    public Enumeration<?> getHeaderNames() {
+        return null;
+    }
+
+    public Enumeration<?> getHeaders(String name) {
+        return null;
+    }
+
+    public int getIntHeader(String name) {
+        return 0;
+    }
+
+    public String getMethod() {
+        return method;
+    }
+
+    public String getPathInfo() {
+        return null;
+    }
+
+    public String getPathTranslated() {
+        return null;
+    }
+
+    public String getQueryString() {
+        return queryString;
+    }
+
+    public String getRemoteUser() {
+        return null;
+    }
+
+    public String getRequestURI() {
+        return null;
+    }
+
+    public StringBuffer getRequestURL() {
+        return null;
+    }
+
+    public String getRequestedSessionId() {
+        return null;
+    }
+
+    public String getServletPath() {
+        return null;
+    }
+
+    public HttpSession getSession() {
+        return null;
+    }
+
+    public HttpSession getSession(boolean create) {
+        return null;
+    }
+
+    public Principal getUserPrincipal() {
+        return null;
+    }
+
+    public boolean isRequestedSessionIdFromCookie() {
+        return false;
+    }
+
+    public boolean isRequestedSessionIdFromURL() {
+        return false;
+    }
+
+    public boolean isRequestedSessionIdFromUrl() {
+        return false;
+    }
+
+    public boolean isRequestedSessionIdValid() {
+        return false;
+    }
+
+    public boolean isUserInRole(String role) {
+        return false;
+    }
+
+    public Object getAttribute(String name) {
+        return null;
+    }
+
+    public Enumeration<?> getAttributeNames() {
+        return null;
+    }
+
+    public String getCharacterEncoding() {
+        return null;
+    }
+
+    public int getContentLength() {
+        return 0;
+    }
+
+    public String getContentType() {
+        return null;
+    }
+
+    public ServletInputStream getInputStream() {
+        return null;
+    }
+
+    public String getLocalAddr() {
+        return null;
+    }
+
+    public String getLocalName() {
+        return null;
+    }
+
+    public int getLocalPort() {
+        return 0;
+    }
+
+    public Locale getLocale() {
+        return null;
+    }
+
+    public Enumeration<?> getLocales() {
+        return null;
+    }
+
+    public String getParameter(String name) {
+        return null;
+    }
+
+    public Map<?, ?> getParameterMap() {
+        return null;
+    }
+
+    public Enumeration<?> getParameterNames() {
+        return null;
+    }
+
+    public String[] getParameterValues(String name) {
+        return null;
+    }
+
+    public String getProtocol() {
+        return null;
+    }
+
+    public BufferedReader getReader() {
+        return null;
+    }
+
+    public String getRealPath(String path) {
+        return null;
+    }
+
+    public String getRemoteAddr() {
+        return null;
+    }
+
+    public String getRemoteHost() {
+        return null;
+    }
+
+    public int getRemotePort() {
+        return 0;
+    }
+
+    public RequestDispatcher getRequestDispatcher(String path) {
+        return null;
+    }
+
+    public String getScheme() {
+        return scheme;
+    }
+
+    public String getServerName() {
+        return server;
+    }
+
+    public int getServerPort() {
+        return port;
+    }
+
+    public boolean isSecure() {
+        return this.secure;
+    }
+
+    public void removeAttribute(String name) {
+
+    }
+
+    public void setAttribute(String name, Object o) {
+
+    }
+
+    public void setCharacterEncoding(String env) {
+
+    }
+
+    public <AdapterType> AdapterType adaptTo(Class<AdapterType> type) {
+        return null;
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/sling/MockSlingHttpServletResponse.java b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockSlingHttpServletResponse.java
new file mode 100644
index 0000000..5228110
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/sling/MockSlingHttpServletResponse.java
@@ -0,0 +1,196 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.sling;
+
+import org.apache.sling.api.SlingHttpServletResponse;
+
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.Cookie;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.Writer;
+import java.util.Locale;
+
+public class MockSlingHttpServletResponse implements SlingHttpServletResponse {
+
+	private StringBuffer output = new StringBuffer();
+	private String contentType;
+	private String encoding;
+	private int status = SC_OK;
+
+	public StringBuffer getOutput() {
+		return output;
+	}
+
+	public void addCookie(Cookie cookie) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".addCookie");
+	}
+
+	public boolean containsHeader(String s) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".containsHeader");
+	}
+
+	public String encodeURL(String s) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".encodeURL");
+	}
+
+	public String encodeRedirectURL(String s) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".encodeRedirectURL");
+	}
+
+	public String encodeUrl(String s) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".encodeUrl");
+	}
+
+	public String encodeRedirectUrl(String s) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".encodeRedirectUrl");
+	}
+
+	public void sendError(int i, String s) throws IOException {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".sendError");
+	}
+
+	public void sendError(int i) throws IOException {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".sendError");
+	}
+
+	public void sendRedirect(String s) throws IOException {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".sendRedirect");
+	}
+
+	public void setDateHeader(String s, long l) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".setDateHeader");
+	}
+
+	public void addDateHeader(String s, long l) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".addDateHeader");
+	}
+
+	public void setHeader(String s, String s1) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".setHeader");
+	}
+
+	public void addHeader(String s, String s1) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".addHeader");
+	}
+
+	public void setIntHeader(String s, int i) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".setIntHeader");
+	}
+
+	public void addIntHeader(String s, int i) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".addIntHeader");
+	}
+
+	public void setStatus(int i) {
+		this.status = i;
+	}
+
+	public void setStatus(int i, String s) {
+		this.status = i;
+	}
+
+	public String getCharacterEncoding() {
+		return encoding;
+	}
+
+	public String getContentType() {
+		return contentType;
+	}
+
+	public ServletOutputStream getOutputStream() throws IOException {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".getOutputStream");
+	}
+
+	public PrintWriter getWriter() throws IOException {
+		MockSlingHttpServletResponse.MockWriter writer = new MockWriter(output);
+		return new PrintWriter(writer);
+	}
+
+	public void setCharacterEncoding(String encoding) {
+		this.encoding = encoding;
+	}
+
+	public void setContentLength(int i) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".setContentLength");
+	}
+
+	public void setContentType(String contentType) {
+		this.contentType = contentType;
+	}
+
+	public void setBufferSize(int i) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".setBufferSize");
+	}
+
+	public int getBufferSize() {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".getBufferSize");
+	}
+
+	public void flushBuffer() throws IOException {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".flushBuffer");
+	}
+
+	public void resetBuffer() {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".resetBuffer");
+	}
+
+	public boolean isCommitted() {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".isCommitted");
+	}
+
+	public void reset() {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".reset");
+	}
+
+	public void setLocale(Locale locale) {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".setLocale");
+	}
+
+	public Locale getLocale() {
+		throw new UnsupportedOperationException("Not implemented: " + getClass().getName() + ".getLocale");
+	}
+
+	public <AdapterType> AdapterType adaptTo(Class<AdapterType> adapterTypeClass) {
+		return null;
+	}
+
+	private class MockWriter extends Writer {
+		private StringBuffer buf;
+
+		public MockWriter(StringBuffer output) {
+			buf = output;
+		}
+
+		@Override
+		public void write(char[] cbuf, int off, int len) throws IOException {
+			buf.append(cbuf, off, len);
+		}
+
+		@Override
+		public void flush() throws IOException {
+			buf.setLength(0);
+		}
+
+		@Override
+		public void close() throws IOException {
+			buf = null;
+		}
+	}
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/util/JavascriptEngine.java b/testing/src/main/java/org/apache/sling/commons/testing/util/JavascriptEngine.java
new file mode 100644
index 0000000..f157017
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/util/JavascriptEngine.java
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.util;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+import org.mozilla.javascript.Context;
+import org.mozilla.javascript.ScriptableObject;
+
+/** Simplistic Javascript engine using Rhino, meant 
+ * 	for automated tests */
+public class JavascriptEngine {
+	/** Execute supplied code against supplied data, 
+	 * 	see JavascriptEngineTest for examples */
+	public String execute(String code, String jsonData) throws IOException {
+        final String jsCode = "data=" + jsonData + ";\n" + code;
+        final Context rhinoContext = Context.enter();
+        final ScriptableObject scope = rhinoContext.initStandardObjects();
+
+        // execute the script, out script variable maps to sw
+        final StringWriter sw = new StringWriter();
+        final PrintWriter pw = new PrintWriter(sw, true);
+        ScriptableObject.putProperty(scope, "out", Context.javaToJS(pw, scope));
+        final int lineNumber = 1;
+        final Object securityDomain = null;
+        try {
+            rhinoContext.evaluateString(
+            		scope, 
+            		jsCode, 
+            		getClass().getSimpleName(),
+                    lineNumber, 
+                    securityDomain);
+        } catch(Exception e) {
+        	final IOException ioe = new IOException("While executing [" + code + "]:" + e);
+        	ioe.initCause(e);
+        	throw ioe;
+        }
+
+        // check script output
+        pw.flush();
+        return sw.toString().trim();
+    }
+}
diff --git a/testing/src/main/java/org/apache/sling/commons/testing/util/TestStringUtil.java b/testing/src/main/java/org/apache/sling/commons/testing/util/TestStringUtil.java
new file mode 100644
index 0000000..f732d62
--- /dev/null
+++ b/testing/src/main/java/org/apache/sling/commons/testing/util/TestStringUtil.java
@@ -0,0 +1,40 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.util;
+
+/** String utilities for testing */
+public class TestStringUtil {
+    static private final String NATIVE_LINE_SEP = System.getProperty("line.separator");
+
+    /** Replace \n with . in strings to make it easier to compare visually for testing */
+    public static String flatten(String str) {
+
+        // First replace native line-endings
+        if(str.indexOf(NATIVE_LINE_SEP) >= 0) {
+            str = str.replace(NATIVE_LINE_SEP, ".");
+        }
+
+        // Now find non-native line-endings, e.g. cygwin needs this
+        if(str.indexOf('\n') >= 0) {
+            str = str.replace('\n', '.');
+        }
+
+        return str;
+    }
+}
diff --git a/testing/src/main/resources/jackrabbit-test-config.xml b/testing/src/main/resources/jackrabbit-test-config.xml
new file mode 100644
index 0000000..7df6bfa
--- /dev/null
+++ b/testing/src/main/resources/jackrabbit-test-config.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0"?>
+<!--
+   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.
+-->
+
+<!DOCTYPE Repository
+          PUBLIC "-//The Apache Software Foundation//DTD Jackrabbit 2.0//EN"
+          "http://jackrabbit.apache.org/dtd/repository-2.0.dtd">
+
+<!-- Example Repository Configuration File
+     Used by
+     - org.apache.jackrabbit.core.config.RepositoryConfigTest.java
+     -
+-->
+<Repository>
+    <!--
+        virtual file system where the repository stores global state
+        (e.g. registered namespaces, custom node types, etc.)
+    -->
+    <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+        <param name="path" value="${rep.home}/repository"/>
+    </FileSystem>
+
+    <!--
+        data store configuration
+    -->
+    <DataStore class="org.apache.jackrabbit.core.data.FileDataStore"/>
+
+    <!--
+        security configuration
+    -->
+    <Security appName="Jackrabbit">
+        <!--
+            security manager:
+            class: FQN of class implementing the JackrabbitSecurityManager interface
+        -->
+        <SecurityManager class="org.apache.jackrabbit.core.DefaultSecurityManager" workspaceName="security">
+            <!--
+            workspace access:
+            class: FQN of class implementing the WorkspaceAccessManager interface
+            -->
+            <!-- <WorkspaceAccessManager class="..."/> -->
+            <!-- <param name="config" value="${rep.home}/security.xml"/> -->
+        </SecurityManager>
+
+        <!--
+            access manager:
+            class: FQN of class implementing the AccessManager interface
+        -->
+        <AccessManager class="org.apache.jackrabbit.core.security.DefaultAccessManager">
+            <!-- <param name="config" value="${rep.home}/access.xml"/> -->
+        </AccessManager>
+
+        <LoginModule class="org.apache.jackrabbit.core.security.authentication.DefaultLoginModule">
+           <!-- 
+              anonymous user name ('anonymous' is the default value)
+            -->
+           <param name="anonymousId" value="anonymous"/>
+           <!--
+              administrator user id (default value if param is missing is 'admin')
+            -->
+           <param name="adminId" value="admin"/>
+        </LoginModule>
+    </Security>
+
+    <!--
+        location of workspaces root directory and name of default workspace
+    -->
+    <Workspaces rootPath="${rep.home}/workspaces" defaultWorkspace="default"/>
+    <!--
+        workspace configuration template:
+        used to create the initial workspace if there's no workspace yet
+    -->
+    <Workspace name="${wsp.name}">
+        <!--
+            virtual file system of the workspace:
+            class: FQN of class implementing the FileSystem interface
+        -->
+        <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+            <param name="path" value="${wsp.home}"/>
+        </FileSystem>
+        <!--
+            persistence manager of the workspace:
+            class: FQN of class implementing the PersistenceManager interface
+        -->
+        <PersistenceManager class="org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager">
+          <param name="url" value="jdbc:derby:${wsp.home}/db;create=true"/>
+          <param name="schemaObjectPrefix" value="${wsp.name}_"/>
+        </PersistenceManager>
+        <!--
+            Search index and the file system it uses.
+            class: FQN of class implementing the QueryHandler interface
+        -->
+        <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+            <param name="path" value="${wsp.home}/index"/>
+            <param name="supportHighlighting" value="true"/>
+        </SearchIndex>
+    </Workspace>
+
+    <!--
+        Configures the versioning
+    -->
+    <Versioning rootPath="${rep.home}/version">
+        <!--
+            Configures the filesystem to use for versioning for the respective
+            persistence manager
+        -->
+        <FileSystem class="org.apache.jackrabbit.core.fs.local.LocalFileSystem">
+            <param name="path" value="${rep.home}/version" />
+        </FileSystem>
+
+        <!--
+            Configures the persistence manager to be used for persisting version state.
+            Please note that the current versioning implementation is based on
+            a 'normal' persistence manager, but this could change in future
+            implementations.
+        -->
+        <PersistenceManager class="org.apache.jackrabbit.core.persistence.pool.DerbyPersistenceManager">
+          <param name="url" value="jdbc:derby:${rep.home}/version/db;create=true"/>
+          <param name="schemaObjectPrefix" value="version_"/>
+        </PersistenceManager>
+    </Versioning>
+
+    <!--
+        Search index for content that is shared repository wide
+        (/jcr:system tree, contains mainly versions)
+    -->
+    <SearchIndex class="org.apache.jackrabbit.core.query.lucene.SearchIndex">
+        <param name="path" value="${rep.home}/repository/index"/>
+        <param name="supportHighlighting" value="true"/>
+    </SearchIndex>
+</Repository>
diff --git a/testing/src/test/java/org/apache/sling/commons/testing/jcr/RepositoryProviderTest.java b/testing/src/test/java/org/apache/sling/commons/testing/jcr/RepositoryProviderTest.java
new file mode 100644
index 0000000..e0cd15d
--- /dev/null
+++ b/testing/src/test/java/org/apache/sling/commons/testing/jcr/RepositoryProviderTest.java
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import static org.junit.Assert.assertNotNull;
+
+import javax.jcr.Session;
+
+import org.apache.sling.jcr.api.SlingRepository;
+import org.junit.Before;
+import org.junit.Test;
+
+/** JUnit 4 style RepositoryProvider test */
+public class RepositoryProviderTest {
+    private SlingRepository repo;
+    
+    @Before
+    public void getRepo() throws Exception {
+        repo = RepositoryProvider.instance().getRepository();
+    }
+    
+    @Test 
+    public void testRepository() throws Exception {
+        assertNotNull("Expecting SlingRepository to be setup", repo);
+    }
+    
+    @Test 
+    public void testRootNode() throws Exception {
+        final Session s = repo.loginAdministrative(null);
+        try {
+            assertNotNull("Expecting a non-null Session", s);
+        } finally {
+            s.logout();
+        }
+    }
+}
diff --git a/testing/src/test/java/org/apache/sling/commons/testing/jcr/RepositoryTestBaseTest.java b/testing/src/test/java/org/apache/sling/commons/testing/jcr/RepositoryTestBaseTest.java
new file mode 100644
index 0000000..8881009
--- /dev/null
+++ b/testing/src/test/java/org/apache/sling/commons/testing/jcr/RepositoryTestBaseTest.java
@@ -0,0 +1,30 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import javax.jcr.Node;
+
+/** Test the RepositoryTestBase */
+public class RepositoryTestBaseTest extends RepositoryTestBase {
+    
+    public void testTestNode() throws Exception {
+        final Node n = getTestRootNode();
+        assertNotNull("Expecting to get a test Node", n);
+    }
+}
diff --git a/testing/src/test/java/org/apache/sling/commons/testing/jcr/RepositoryUtilTest.java b/testing/src/test/java/org/apache/sling/commons/testing/jcr/RepositoryUtilTest.java
new file mode 100644
index 0000000..81c392d
--- /dev/null
+++ b/testing/src/test/java/org/apache/sling/commons/testing/jcr/RepositoryUtilTest.java
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.jcr;
+
+import static org.junit.Assert.assertNotNull;
+
+import javax.jcr.Session;
+
+/**
+ * This is a simple test for the repository util. We start
+ * the repository, try to login as admin and anonymous and
+ * then stop the repository.
+ */
+public class RepositoryUtilTest {
+
+    @org.junit.Test public void testRepository() throws Exception {
+        // start the repository
+        RepositoryUtil.startRepository();
+
+        // get admin session
+        final Session adminSession = RepositoryUtil.getRepository().loginAdministrative(null);
+        assertNotNull(adminSession);
+        adminSession.logout();
+
+        // get anonymous session
+        final Session anonSession = RepositoryUtil.getRepository().login();
+        assertNotNull(anonSession);
+        anonSession.logout();
+
+        // stop the repository
+        RepositoryUtil.stopRepository();
+    }
+}
diff --git a/testing/src/test/java/org/apache/sling/commons/testing/junit/RetryRuleTest.java b/testing/src/test/java/org/apache/sling/commons/testing/junit/RetryRuleTest.java
new file mode 100644
index 0000000..ea03904
--- /dev/null
+++ b/testing/src/test/java/org/apache/sling/commons/testing/junit/RetryRuleTest.java
@@ -0,0 +1,110 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.junit;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+
+public class RetryRuleTest {
+    private int setupCounter;
+    private int callCount = 0;
+    private long callTime = -1;
+
+    @Rule
+    public final RetryRule retryRule = new RetryRule();
+    
+    @Before
+    public void setup() {
+        setupCounter = 0;
+    }
+    
+    @Retry
+    @Test
+    public void testDefaultParameters() {
+        callCount++;
+        setupCounter++;
+        
+        if(callTime > 0) {
+            final long delta = System.currentTimeMillis();
+            assertTrue("Expecting at least 500 msec between calls", delta >= 500);
+        }
+        callTime = System.currentTimeMillis();
+        
+        assertTrue("Expecting to be called several times before passing", callCount > 3);
+        assertEquals("Expecting setup() to be called before every retry", 1, setupCounter);
+
+        // Once we pass, reset counters for other tests
+        callTime = -1;
+        callCount = 0;
+    }
+    
+    @Retry
+    @Test
+    public void testRetryOnException() throws Exception {
+        callCount++;
+        setupCounter++;
+
+        if(callCount <= 3) {
+            throw new Exception("Expecting to be called several times before passing");
+        }
+        
+        assertEquals("Expecting setup() to be called before every retry", 1, setupCounter);
+
+        // Once we pass, reset counters for other tests
+        callTime = -1;
+        callCount = 0;
+    }
+    
+    @Retry(timeoutMsec=500, intervalMsec=1)
+    @Test
+    public void testCustomParameters() {
+        callCount++;
+        setupCounter++;
+        
+        assertTrue("Expecting to be called many times before passing", callCount > 100);
+        assertEquals("Expecting setup() to be called before every retry", 1, setupCounter);
+        
+        // Once we pass, reset callTime for other tests
+        callTime = -1;
+        callCount = 0;
+    }
+    
+    @Test
+    public void testDefaultDefaultTimings() {
+        final RetryRule r = new RetryRule();
+        assertEquals("Expecting default default timeout", RetryRule.DEFAULT_DEFAULT_TIMEOUT_MSEC, r.getTimeout(-1));
+        assertEquals("Expecting default default interval", RetryRule.DEFAULT_DEFAULT_INTERVAL_MSEC, r.getInterval(-1));
+    }
+    
+    @Test
+    public void testSpecifiedDefaultTimings() {
+        final RetryRule r = new RetryRule(12, 42);
+        assertEquals("Expecting specified default timeout", 12, r.getTimeout(-1));
+        assertEquals("Expecting specified default interval", 42, r.getInterval(-1));
+    }
+    
+    @Test
+    public void testRuleTimings() {
+        final RetryRule r = new RetryRule(12, 42);
+        assertEquals("Expecting timeout from Rule", 1012, r.getTimeout(1012));
+        assertEquals("Expecting interval from Rule", 1024, r.getInterval(1024));
+    }
+}
diff --git a/testing/src/test/java/org/apache/sling/commons/testing/sling/MockResourceTest.java b/testing/src/test/java/org/apache/sling/commons/testing/sling/MockResourceTest.java
new file mode 100644
index 0000000..7661bae
--- /dev/null
+++ b/testing/src/test/java/org/apache/sling/commons/testing/sling/MockResourceTest.java
@@ -0,0 +1,63 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.sling;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ValueMap;
+import org.junit.Test;
+
+public class MockResourceTest {
+    public static final String PATH = "/foo/bar";
+    public static final String RT = "someResourceType";
+    
+    @Test 
+    public void basicTest() throws Exception {
+        final MockResource r = new MockResource(null, PATH, RT);
+        r.addProperty("1", Integer.MAX_VALUE);
+        r.addProperty("2", "two");
+        
+        assertTrue(r instanceof Resource);
+        assertEquals(PATH, r.getPath());
+        assertEquals(RT, r.getResourceType());
+        assertNull(r.getResourceSuperType());
+        
+        final ValueMap m = r.adaptTo(ValueMap.class);
+        assertEquals(Integer.MAX_VALUE, m.get("1"));
+        assertEquals("two", m.get("2"));
+        
+        // changes to r do not affect m
+        assertNull(m.get("third"));
+        r.getProperties().put("third", "trois");
+        assertNull(m.get("third"));
+        
+        // but we get the new value after adapting again
+        assertEquals("trois", r.adaptTo(ValueMap.class).get("third"));
+    }
+    
+    @Test
+    public void testNoProperties() throws Exception {
+        final MockResource r = new MockResource(null, PATH, RT);
+        assertNotNull(r.adaptTo(ValueMap.class));
+    }
+}
\ No newline at end of file
diff --git a/testing/src/test/java/org/apache/sling/commons/testing/util/JavascriptEngineTest.java b/testing/src/test/java/org/apache/sling/commons/testing/util/JavascriptEngineTest.java
new file mode 100644
index 0000000..7f4d78a
--- /dev/null
+++ b/testing/src/test/java/org/apache/sling/commons/testing/util/JavascriptEngineTest.java
@@ -0,0 +1,46 @@
+/*
+ * 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.
+ */
+package org.apache.sling.commons.testing.util;
+
+import java.io.IOException;
+
+import org.apache.sling.commons.testing.util.JavascriptEngine;
+import org.junit.Test;
+import static org.junit.Assert.assertEquals;
+
+public class JavascriptEngineTest {
+	private final JavascriptEngine engine = new JavascriptEngine();
+	
+	@Test
+	public void testSimpleScripts() throws IOException {
+		
+		// Each triplet of data is: json data, code, expected output
+		final String [] data = {
+			"{}", "out.print('hello')", "hello",	
+			"{ i:26, j:'a' }", "out.print(data.i + data.j)", "26a",
+			"{ i:'ab', j:'cd' }", "out.print(data.i) ; out.print('+') ; out.print(data.j)", "ab+cd"
+		};
+		
+		for(int i=0; i < data.length; i+= 3) {
+			final String json = data[i];
+			final String code = data[i+1];
+			final String expected = data[i+2];
+			final String actual = engine.execute(code, json);
+			assertEquals("At index " + i, expected, actual);
+		}
+	}
+}