Added: 
release/sling/src/test/java/org/apache/sling/testing/DelayRequestInterceptorTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/DelayRequestInterceptorTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/DelayRequestInterceptorTest.java
 Thu Apr  9 13:50:42 2020
@@ -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.testing;
+
+import org.apache.sling.testing.clients.interceptors.DelayRequestInterceptor;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class DelayRequestInterceptorTest {
+
+    @Test
+    public void testDelay() throws Exception {
+        DelayRequestInterceptor interceptor = new DelayRequestInterceptor(700);
+        long before = System.currentTimeMillis();
+        interceptor.process(null, null);
+        long after = System.currentTimeMillis();
+        Assert.assertTrue(after - before >= 700);
+    }
+
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/HttpServerRule.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/HttpServerRule.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/HttpServerRule.java
 Thu Apr  9 13:50:42 2020
@@ -0,0 +1,84 @@
+/*******************************************************************************
+ * 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
+ * <p>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p>
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ 
******************************************************************************/
+package org.apache.sling.testing.clients;
+
+import java.io.IOException;
+import java.net.URI;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.http.HttpHost;
+import org.apache.http.client.utils.URIUtils;
+import org.apache.http.config.SocketConfig;
+import org.apache.http.impl.bootstrap.HttpServer;
+import org.apache.http.impl.bootstrap.ServerBootstrap;
+import org.apache.http.localserver.SSLTestContexts;
+import org.junit.rules.ExternalResource;
+
+/** JUnit Rule that starts an HTTP server */
+public class HttpServerRule extends ExternalResource {
+    public static final String ORIGIN = "TEST/1.1";
+    private HttpServer server;
+    private HttpHost host;
+    private URI uri;
+
+    protected ServerBootstrap serverBootstrap;
+
+    public static enum ProtocolScheme {
+        http,
+        https;
+        private ProtocolScheme() {
+        }
+    }
+
+    protected final ProtocolScheme protocolScheme;
+
+    public HttpServerRule() {
+        this(ProtocolScheme.http);
+    }
+
+    public HttpServerRule(ProtocolScheme protocolScheme) {
+        this.protocolScheme = protocolScheme;
+    }
+
+    @Override
+    protected void after() {
+        server.shutdown(-1, TimeUnit.SECONDS);
+    }
+
+    @Override
+    protected void before() throws Throwable {
+        final SocketConfig socketConfig = 
SocketConfig.custom().setSoTimeout(5000).build();
+        serverBootstrap = 
ServerBootstrap.bootstrap().setSocketConfig(socketConfig).setServerInfo(ORIGIN);
+        if(ProtocolScheme.https.equals(protocolScheme)) {
+            
serverBootstrap.setSslContext(SSLTestContexts.createServerSSLContext());
+        }
+        registerHandlers();
+        server = serverBootstrap.create();
+        server.start();
+        host = new HttpHost("127.0.0.1", server.getLocalPort(), 
protocolScheme.name());
+        uri = URIUtils.rewriteURI(new URI("/"), host);
+    }
+
+    protected void registerHandlers() throws IOException {
+    }
+
+    public URI getURI() {
+        return uri;
+    }
+}
\ No newline at end of file

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientConfigTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientConfigTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientConfigTest.java
 Thu Apr  9 13:50:42 2020
@@ -0,0 +1,44 @@
+/*
+ * 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.testing.clients;
+
+import org.junit.Test;
+
+import java.net.URISyntaxException;
+
+public class SlingClientConfigTest {
+
+    @Test(expected = ClientException.class)
+    public void testUrlNoProtocolUnauthenticated() throws URISyntaxException, 
ClientException {
+        // Should fail because URL doesn't start with http or https
+        SlingClientConfig.Builder
+                .create()
+                .setUrl("example.com")
+                .build();
+    }
+
+    @Test(expected = ClientException.class)
+    public void testUrlNoProtocolAuthenticated() throws URISyntaxException, 
ClientException {
+        // Should fail because URL doesn't start with http or https
+        SlingClientConfig.Builder
+                .create()
+                .setUser("username")
+                .setPassword("pass")
+                .setUrl("example.com")
+                .build();
+    }
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientConstructorTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientConstructorTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientConstructorTest.java
 Thu Apr  9 13:50:42 2020
@@ -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.testing.clients;
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.codehaus.jackson.JsonNode;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.net.URI;
+
+import static org.junit.Assert.assertEquals;
+
+public class SlingClientConstructorTest {
+
+    @Test
+    public void testConstructorUrlHttp() throws Exception {
+        URI url = URI.create("http://example.com";);
+        URI expected = URI.create("http://example.com/";);
+        SlingClient c = new SlingClient(url, "user", "pass");
+        assertEquals(expected, c.getUrl());
+    }
+
+    @Test
+    public void testConstructorUrlHttpTrailingSlash() throws Exception {
+        URI url = URI.create("http://example.com/";);
+        URI expected = URI.create("http://example.com/";);
+        SlingClient c = new SlingClient(url, "user", "pass");
+        assertEquals(expected, c.getUrl());
+    }
+
+    @Test
+    public void testConstructorUrlHttps() throws Exception {
+        URI url = URI.create("https://example.com";);
+        URI expected = URI.create("https://example.com/";);
+        SlingClient c = new SlingClient(url, "user", "pass");
+        assertEquals(expected, c.getUrl());
+    }
+
+    @Test
+    public void testConstructorUrlHttpsTrailingSlash() throws Exception {
+        URI url = URI.create("https://example.com/";);
+        SlingClient c = new SlingClient(url, "user", "pass");
+        assertEquals(url, c.getUrl());
+    }
+
+    @Test
+    public void testConstructorUrlBasePath() throws Exception {
+        URI url = URI.create("https://example.com/mypath";);
+        URI expected = URI.create("https://example.com/mypath/";);
+        SlingClient c = new SlingClient(url, "user", "pass");
+        assertEquals(expected, c.getUrl());
+    }
+
+    @Test
+    public void testConstructorUrlBasePathTrailingSlash() throws Exception {
+        URI url = URI.create("https://example.com/mypath/";);
+        SlingClient c = new SlingClient(url, "user", "pass");
+        assertEquals(url, c.getUrl());
+    }
+
+    @Test
+    public void testConstructorAnonymous() throws Exception {
+        URI url = URI.create("https://example.com/";);
+        SlingClient c = new SlingClient(url, null, null);
+        assertEquals(url, c.getUrl());
+    }
+
+    @Test(expected = ClientException.class)
+    public void testConstructorUrlNoProtocol() throws Exception {
+        URI url = URI.create("example.com/");
+        SlingClient c = new SlingClient(url, "user", "pass");
+        assertEquals(url, c.getUrl());
+    }
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientDoGetJsonTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientDoGetJsonTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientDoGetJsonTest.java
 Thu Apr  9 13:50:42 2020
@@ -0,0 +1,71 @@
+/*
+ * 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.testing.clients;
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.codehaus.jackson.JsonNode;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.*;
+
+public class SlingClientDoGetJsonTest {
+    private static final String GET_JSON_PATH = "/test/json/resource";
+    private static final String JSON_RESPONSE = 
"{\"jcr:primaryType\":\"cq:Page\",\"jcr:createdBy\":\"admin-json\"}";
+    private static final String JSON_INF_RESPONSE = 
"{\"jcr:primaryType\":\"cq:Page\",\"jcr:createdBy\":\"admin-infinity\"}";
+
+    @ClassRule
+    public static HttpServerRule httpServer = new HttpServerRule() {
+        @Override
+        protected void registerHandlers() throws IOException {
+            serverBootstrap.registerHandler(GET_JSON_PATH + ".1.json", new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context) throws HttpException, IOException {
+                    response.setEntity(new StringEntity(JSON_RESPONSE));
+                }
+            });
+
+            serverBootstrap.registerHandler(GET_JSON_PATH + ".infinity.json", 
new HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context) throws HttpException, IOException {
+                    response.setEntity(new StringEntity(JSON_INF_RESPONSE));
+                }
+            });
+        }
+    };
+
+    @Test
+    public void testDoGetJson() throws Exception {
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        JsonNode res = c.doGetJson(GET_JSON_PATH, 1, 200);
+        assertEquals("admin-json", res.get("jcr:createdBy").getTextValue());
+    }
+
+    @Test
+    public void testDoGetJsonInfinity() throws Exception {
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        JsonNode res = c.doGetJson(GET_JSON_PATH, -1, 200);
+        assertEquals("admin-infinity", 
res.get("jcr:createdBy").getTextValue());
+    }
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientImportContentTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientImportContentTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientImportContentTest.java
 Thu Apr  9 13:50:42 2020
@@ -0,0 +1,164 @@
+/*
+ * 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.testing.clients;
+
+import org.apache.commons.io.IOUtils;
+import org.apache.http.*;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.codehaus.jackson.node.JsonNodeFactory;
+import org.codehaus.jackson.node.ObjectNode;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+import static org.apache.http.HttpStatus.SC_BAD_REQUEST;
+import static org.apache.http.HttpStatus.SC_CREATED;
+
+public class SlingClientImportContentTest {
+    private static final Logger LOG = 
LoggerFactory.getLogger(SlingClientImportContentTest.class);
+
+    private static final String IMPORT_PATH = "/test/import/parent";
+    private static final String IMPORT_FILE_PATH = "/content/importfile";
+
+    private static final String IMPORT_FILE_CONTENT = 
"{\"nodefromfile\":{\"prop1\":\"val1\"}}";
+
+    @ClassRule
+    public static HttpServerRule httpServer = new HttpServerRule() {
+        @Override
+        protected void registerHandlers() throws IOException {
+            serverBootstrap.registerHandler(IMPORT_PATH, new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context) throws HttpException, IOException {
+                    List<NameValuePair> params = extractParameters(request);
+                    String operation = getParameter(":operation", params);
+                    String content = getParameter(":content", params);
+
+                    if (!"import".equals(operation)) {
+                        response.setStatusCode(SC_BAD_REQUEST);
+                        response.setEntity(new StringEntity("Unexpected 
operation: " + operation));
+                        return;
+                    }
+
+                    if 
(!"{\"something\":{\"prop1\":\"val1\"}}".equals(content)) {
+                        response.setStatusCode(SC_BAD_REQUEST);
+                        response.setEntity(new StringEntity("Unexpected 
content: " + content));
+                        return;
+                    }
+
+                    response.setStatusCode(SC_CREATED);
+                }
+            });
+
+            serverBootstrap.registerHandler(IMPORT_FILE_PATH, new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context) throws HttpException, IOException {
+                    LOG.debug("received: {}", request);
+                    if (request instanceof HttpEntityEnclosingRequest) {
+                        HttpEntity entity = ((HttpEntityEnclosingRequest) 
request).getEntity();
+                        String content = IOUtils.toString(entity.getContent(), 
UTF_8);
+                        LOG.debug("content: {}", content);
+
+                        if (!content.contains(":operation") || 
!content.contains("import")) {
+                            response.setStatusCode(SC_BAD_REQUEST);
+                            response.setEntity(new StringEntity("Operation not 
found"));
+                            return;
+                        } else if (!content.contains(IMPORT_FILE_CONTENT)) {
+                            response.setStatusCode(SC_BAD_REQUEST);
+                            response.setEntity(new StringEntity("File content 
not found"));
+                            return;
+                        }
+
+                        response.setStatusCode(SC_CREATED);
+                    } else {
+                        response.setStatusCode(SC_BAD_REQUEST);
+                        response.setEntity(new StringEntity("Request doesn't 
contain an entity"));
+                    }
+                }
+            });
+        }
+
+        private List<NameValuePair> extractParameters(HttpRequest request) {
+            if (request instanceof HttpEntityEnclosingRequest) {
+                HttpEntity entity = ((HttpEntityEnclosingRequest) 
request).getEntity();
+                try {
+                    return URLEncodedUtils.parse(entity);
+                } catch (IOException e) {
+                    LOG.error("Failed to parse entity", e);
+                }
+            }
+
+            return new ArrayList<>();
+        }
+
+        private String getParameter(String parameterName, List<NameValuePair> 
parameters) {
+            for (NameValuePair parameter : parameters) {
+                if (parameter.getName().equals(parameterName)) {
+                    return parameter.getValue();
+                }
+            }
+
+            return null;
+        }
+    };
+
+
+
+    private SlingClient client;
+
+    public SlingClientImportContentTest() throws ClientException {
+        client = new SlingClient(httpServer.getURI(), "user", "pass");
+        // to use with an already running instance
+        // client = new 
SlingClient(java.net.URI.create("http://localhost:8080";), "admin", "admin");
+    }
+
+    @Test
+    public void testImportContent() throws Exception {
+        client.importContent(IMPORT_PATH, "json", 
"{\"something\":{\"prop1\":\"val1\"}}");
+    }
+
+    @Test
+    public void testImportJson() throws Exception {
+        ObjectNode node = JsonNodeFactory.instance.objectNode();
+        ObjectNode props = JsonNodeFactory.instance.objectNode();
+        props.put("prop1", "val1");
+
+        node.put("something", props);
+        client.importJson(IMPORT_PATH, node);
+    }
+    @Test
+    public void testImportContentFile() throws Exception {
+        File tmp = File.createTempFile("import-json", null);
+        LOG.debug("created: " + tmp);
+        PrintWriter pw = new PrintWriter(tmp);
+        pw.write(IMPORT_FILE_CONTENT);
+        pw.close();
+
+        client.importContent(IMPORT_FILE_PATH, "json", tmp);
+    }
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientRetryStrategyTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientRetryStrategyTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientRetryStrategyTest.java
 Thu Apr  9 13:50:42 2020
@@ -0,0 +1,169 @@
+/*
+ * 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.testing.clients;
+
+import org.apache.http.entity.StringEntity;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static 
org.apache.sling.testing.clients.SystemPropertiesConfig.CONFIG_PROP_PREFIX;
+import static 
org.apache.sling.testing.clients.SystemPropertiesConfig.HTTP_RETRIES_ERROR_CODES_PROP;
+import static org.junit.Assert.assertEquals;
+
+public class SlingClientRetryStrategyTest {
+    private static final String GET_UNAVAILABLE_PATH = 
"/test/unavailable/resource";
+    private static final String GET_INEXISTENT_PATH = 
"/test/inexistent/resource";
+    private static final String GET_INTERNAL_ERROR_PATH = 
"/test/internalerror/resource";
+    private static final String GET_505_PATH = 
"/test/unsupportedversion/resource";
+    private static final String NOK_RESPONSE = "TEST_NOK";
+    private static final String OK_RESPONSE = "TEST_OK";
+
+    private static final int MAX_RETRIES = 4;
+
+    private static int requestCount = 0;
+    private static int availableAtRequestCount = Integer.MAX_VALUE;
+
+    static {
+        System.setProperty(CONFIG_PROP_PREFIX + 
SystemPropertiesConfig.HTTP_LOG_RETRIES_PROP, "true");
+        System.setProperty(CONFIG_PROP_PREFIX + 
SystemPropertiesConfig.HTTP_DELAY_PROP, "50");
+        System.setProperty(CONFIG_PROP_PREFIX + 
SystemPropertiesConfig.HTTP_RETRIES_PROP, "4");
+        System.setProperty(CONFIG_PROP_PREFIX + HTTP_RETRIES_ERROR_CODES_PROP, 
"500,503");
+    }
+
+    @ClassRule
+    public static HttpServerRule httpServer = new HttpServerRule() {
+        @Override
+        protected void registerHandlers() throws IOException {
+            serverBootstrap.registerHandler(GET_UNAVAILABLE_PATH, (request, 
response, context) -> {
+                requestCount++;
+                if (requestCount == availableAtRequestCount) {
+                    response.setEntity(new StringEntity(OK_RESPONSE));
+                    response.setStatusCode(200);
+                } else {
+                    response.setEntity(new StringEntity(NOK_RESPONSE));
+                    response.setStatusCode(503);
+                }
+            });
+
+            serverBootstrap.registerHandler(GET_INTERNAL_ERROR_PATH, (request, 
response, context) -> {
+                requestCount++;
+                if (requestCount == availableAtRequestCount) {
+                    response.setEntity(new StringEntity(OK_RESPONSE));
+                    response.setStatusCode(200);
+                } else {
+                    response.setEntity(new StringEntity(NOK_RESPONSE));
+                    response.setStatusCode(500);
+                }
+            });
+
+            serverBootstrap.registerHandler(GET_INEXISTENT_PATH, (request, 
response, context) -> {
+                requestCount++;
+                response.setEntity(new StringEntity(NOK_RESPONSE));
+                response.setStatusCode(404);
+            });
+
+            serverBootstrap.registerHandler(GET_505_PATH, (request, response, 
context) -> {
+                requestCount++;
+                if (requestCount == availableAtRequestCount) {
+                    response.setEntity(new StringEntity(OK_RESPONSE));
+                    response.setStatusCode(200);
+                } else {
+                    response.setEntity(new StringEntity(NOK_RESPONSE));
+                    response.setStatusCode(505);
+                }
+            });
+        }
+    };
+
+    @Test
+    public void testRetryReallyUnavailable() throws Exception {
+        requestCount = 0;
+        availableAtRequestCount = Integer.MAX_VALUE; // never available
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        SlingHttpResponse slingHttpResponse = c.doGet(GET_UNAVAILABLE_PATH, 
503);
+        assertEquals(MAX_RETRIES + 1, requestCount);
+        assertEquals(NOK_RESPONSE, slingHttpResponse.getContent());
+    }
+
+    @Test
+    public void testRetryReallyInternalError() throws Exception {
+        requestCount = 0;
+        availableAtRequestCount = Integer.MAX_VALUE; // never available
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        SlingHttpResponse slingHttpResponse = c.doGet(GET_INTERNAL_ERROR_PATH, 
500);
+        assertEquals(MAX_RETRIES + 1, requestCount);
+        assertEquals(NOK_RESPONSE, slingHttpResponse.getContent());
+    }
+
+    @Test
+    public void test505ShouldNotRetry() throws Exception {
+        System.setProperty(CONFIG_PROP_PREFIX + HTTP_RETRIES_ERROR_CODES_PROP, 
"500,503");
+        requestCount = 0;
+        availableAtRequestCount = Integer.MAX_VALUE; // never 200
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        SlingHttpResponse slingHttpResponse = c.doGet(GET_505_PATH, 505);
+        assertEquals(1, requestCount);
+        assertEquals(NOK_RESPONSE, slingHttpResponse.getContent());
+    }
+
+    @Test
+    public void test505ShouldRetry() throws Exception {
+        System.setProperty(CONFIG_PROP_PREFIX + HTTP_RETRIES_ERROR_CODES_PROP, 
"500,503,505");
+        requestCount = 0;
+        availableAtRequestCount = 3;
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        SlingHttpResponse slingHttpResponse = c.doGet(GET_505_PATH, 200);
+        assertEquals(availableAtRequestCount, requestCount);
+        assertEquals(OK_RESPONSE, slingHttpResponse.getContent());
+    }
+
+    @Test
+    public void testRetryInexistent() throws Exception {
+        requestCount = 0;
+        availableAtRequestCount = Integer.MAX_VALUE; // never available
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        SlingHttpResponse slingHttpResponse = c.doGet(GET_INEXISTENT_PATH, 
404);
+        // should not retry at all
+        assertEquals(1, requestCount);
+        assertEquals(NOK_RESPONSE, slingHttpResponse.getContent());
+    }
+
+    @Test
+    public void testRetryEventuallyAvailable() throws Exception {
+        requestCount = 0;
+        availableAtRequestCount = 3;
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        SlingHttpResponse slingHttpResponse = c.doGet(GET_UNAVAILABLE_PATH, 
200);
+        assertEquals(availableAtRequestCount, requestCount);
+        assertEquals(OK_RESPONSE, slingHttpResponse.getContent());
+
+    }
+
+    @Test
+    public void testRetryEventuallyNoError() throws Exception {
+        requestCount = 0;
+        availableAtRequestCount = 3;
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        SlingHttpResponse slingHttpResponse = c.doGet(GET_INTERNAL_ERROR_PATH, 
200);
+        assertEquals(availableAtRequestCount, requestCount);
+        assertEquals(OK_RESPONSE, slingHttpResponse.getContent());
+
+    }
+
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientWaitExistsTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientWaitExistsTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/SlingClientWaitExistsTest.java
 Thu Apr  9 13:50:42 2020
@@ -0,0 +1,93 @@
+/*
+ * 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.testing.clients;
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.concurrent.TimeoutException;
+
+import static org.junit.Assert.*;
+
+public class SlingClientWaitExistsTest {
+    private static final String GET_WAIT_PATH = "/test/wait/resource";
+    private static final String OK_RESPONSE = "TEST_OK";
+    private static final String NOK_RESPONSE = "TEST_OK";
+
+    private static int waitCount = 4; // truly randomly chosen by typing with 
the eyes closed
+    private static int callCount = 0;
+
+    @ClassRule
+    public static HttpServerRule httpServer = new HttpServerRule() {
+        @Override
+        protected void registerHandlers() throws IOException {
+            serverBootstrap.registerHandler(GET_WAIT_PATH + ".json", new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context) throws HttpException, IOException {
+                    callCount++;
+                    if (callCount == waitCount) {
+                        response.setEntity(new StringEntity(OK_RESPONSE));
+                    } else {
+                        response.setEntity(new StringEntity(NOK_RESPONSE));
+                        response.setStatusCode(404);
+                    }
+                }
+            });
+        }
+    };
+
+    @Test
+    public void testWaitExists() throws Exception {
+        callCount = 0;  // reset counter
+        waitCount = 3;  // less than timeout
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        c.waitExists(GET_WAIT_PATH, 500, 10);
+        assertEquals(waitCount, callCount);
+    }
+
+    @Test
+    public void testWaitExistsTimeout() throws Exception {
+        callCount = 0;  // reset counter
+        waitCount = 40;  // to be sure we reach timeout
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        try {
+            c.waitExists(GET_WAIT_PATH, 1000, 10);
+        } catch (TimeoutException e ) {
+            assertTrue("call was executed only " + callCount + " times", 
callCount > 3);
+            return;
+        }
+
+        fail("waitExists did not timeout");
+    }
+
+    @Test
+    public void testWaitExistsOnce() throws Exception {
+        callCount = 0;  // reset counter
+        waitCount = 1;  // less than timeout
+        SlingClient c = new SlingClient(httpServer.getURI(), "user", "pass");
+        c.waitExists(GET_WAIT_PATH, -1, 10);
+        assertEquals(1, callCount);
+    }
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/indexing/IndexingClientTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/indexing/IndexingClientTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/indexing/IndexingClientTest.java
 Thu Apr  9 13:50:42 2020
@@ -0,0 +1,245 @@
+/*******************************************************************************
+ * 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
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ 
******************************************************************************/
+package org.apache.sling.testing.clients.indexing;
+
+import org.apache.http.*;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHttpEntityEnclosingRequest;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.HttpServerRule;
+import org.apache.sling.testing.clients.query.servlet.QueryServlet;
+import org.hamcrest.CoreMatchers;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeoutException;
+import java.util.concurrent.atomic.AtomicInteger;
+
+public class IndexingClientTest {
+    private static final Logger LOG = 
LoggerFactory.getLogger(IndexingClientTest.class);
+
+    private static final String EXPLAIN_RESPONSE = "{\"plan\": \"random plan 
with testIndexingLane-async and testIndexingLane-fulltext-async\",\"time\": 1}";
+    private static final String QUERY_RESPONSE = "{\"total\": 1234,\"time\": 
1}";
+
+    private static final String [] PRE_DEFINED_INDEXING_LANES = new 
String[]{"async", "fulltext-async"};
+
+    private static final AtomicInteger NUM_INDEXING_LANE_CONSOLE_CALLS = new 
AtomicInteger();
+
+    @ClassRule
+    public static HttpServerRule httpServer = new HttpServerRule() {
+        HttpRequestHandler okHandler =  new HttpRequestHandler() {
+            @Override
+            public void handle(HttpRequest request, HttpResponse response, 
HttpContext context)
+                    throws HttpException, IOException {
+                response.setStatusCode(200);
+                response.setEntity(new StringEntity("Everything's fine"));
+            }
+        };
+
+        HttpRequestHandler createdHandler =  new HttpRequestHandler() {
+            @Override
+            public void handle(HttpRequest request, HttpResponse response, 
HttpContext context)
+                    throws HttpException, IOException {
+                response.setStatusCode(201);
+                response.setEntity(new StringEntity("Created"));
+            }
+        };
+
+        @Override
+        protected void registerHandlers() throws IOException {
+            // Normal query request
+            serverBootstrap.registerHandler(QueryServlet.SERVLET_PATH, new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context)
+                        throws HttpException, IOException {
+                    List<NameValuePair> parameters = URLEncodedUtils.parse(
+                            request.getRequestLine().getUri(), 
Charset.defaultCharset());
+
+                    for (NameValuePair parameter : parameters) {
+                        if (parameter.getName().equals("explain") && 
!parameter.getValue().equals("false")) {
+                            response.setEntity(new 
StringEntity(EXPLAIN_RESPONSE));
+                            return;
+                        }
+                    }
+
+                    response.setEntity(new StringEntity(QUERY_RESPONSE));
+                }
+            });
+
+            // Install servlet
+            serverBootstrap.registerHandler("/system/console/bundles", new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context)
+                        throws HttpException, IOException {
+                    // is install (post) or checking status (get)
+                    if (request instanceof BasicHttpEntityEnclosingRequest) {
+                        response.setStatusCode(302);
+                    } else {
+                        response.setStatusCode(200);
+                    }
+                }
+            });
+
+            // Check bundle status
+            serverBootstrap.registerHandler("BUNDLE_PATH" + ".json", new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context)
+                        throws HttpException, IOException {
+                    response.setEntity(new StringEntity("JSON_BUNDLE"));
+                }
+            });
+
+            // Uninstall bundle
+            serverBootstrap.registerHandler("BUNDLE_PATH", new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context)
+                        throws HttpException, IOException {
+                    response.setStatusCode(200);
+                }
+            });
+
+            // Uninstall bundle
+            serverBootstrap.registerHandler(
+                    
"/system/console/configMgr/org.apache.jackrabbit.oak.plugins.index.AsyncIndexerService",
+                    new HttpRequestHandler() {
+                        @Override
+                        public void handle(HttpRequest request, HttpResponse 
response, HttpContext context)
+                                throws HttpException, IOException {
+                            NUM_INDEXING_LANE_CONSOLE_CALLS.incrementAndGet();
+                            response.setStatusCode(200);
+                            response.setEntity(new 
StringEntity("{\"properties\":{" +
+                                    
"\"asyncConfigs\":{\"values\":[\"async:5\",\"fulltext-async:5\"]}}}"));
+                        }
+                    }
+            );
+
+            
serverBootstrap.registerHandler("/tmp/testing/waitForAsyncIndexing/content/*", 
new HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context)
+                        throws HttpException, IOException {
+                    List<NameValuePair> params = extractParameters(request);
+
+                    for (NameValuePair param : params) {
+                        if (param.getName().equals(":operation") && 
(param.getValue().equals("delete"))) {
+                            response.setStatusCode(200);
+                            return;
+                        }
+                    }
+
+                    response.setStatusCode(201);
+                    response.setEntity(new StringEntity("Created!"));
+                }
+            });
+
+            
serverBootstrap.registerHandler("/tmp/testing/waitForAsyncIndexing/oak:index/*",
 new HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context)
+                        throws HttpException, IOException {
+                    List<NameValuePair> params = extractParameters(request);
+
+                    for (NameValuePair param : params) {
+                        if (param.getName().equals(":operation") && 
(param.getValue().equals("delete"))) {
+                            response.setStatusCode(200);
+                            return;
+                        }
+                    }
+
+                    response.setStatusCode(200);
+                    response.setEntity(new StringEntity("Created!"));
+                }
+            });
+
+            // unimportant requests
+            serverBootstrap.registerHandler("/tmp.json", okHandler);
+            serverBootstrap.registerHandler("/tmp/testing.json", okHandler);
+            
serverBootstrap.registerHandler("/tmp/testing/waitForAsyncIndexing", okHandler);
+            serverBootstrap.registerHandler("/tmp/testing", okHandler);
+            
serverBootstrap.registerHandler("/tmp/testing/waitForAsyncIndexing/oak:index", 
createdHandler);
+            
serverBootstrap.registerHandler("/tmp/testing/waitForAsyncIndexing/content", 
createdHandler);
+        }
+    };
+
+    private IndexingClient client;
+
+    public IndexingClientTest() throws ClientException {
+        NUM_INDEXING_LANE_CONSOLE_CALLS.set(0);
+        client = new IndexingClient(httpServer.getURI(), "admin", "admin");
+        //client = new 
IndexingClient(java.net.URI.create("http://localhost:4502";), "admin", "admin");
+    }
+
+    @Test
+    public void testInstall() throws ClientException {
+        client.install();
+    }
+
+    @Test
+    public void testUninstall() throws ClientException {
+        client.uninstall();
+    }
+
+    @Test
+    public void testWaitForAsyncIndexing() throws ClientException, 
TimeoutException, InterruptedException {
+        client.waitForAsyncIndexing();
+    }
+
+    @Test
+    public void testWaitForAsyncIndexingConfiguredLanes() throws 
ClientException, TimeoutException, InterruptedException {
+        client.setLaneNames(PRE_DEFINED_INDEXING_LANES);
+
+        List<String> retrievedLaneNames = client.getLaneNames();
+        Assert.assertEquals("Mismatched number of lanes", 
PRE_DEFINED_INDEXING_LANES.length, retrievedLaneNames.size());
+        Assert.assertThat(retrievedLaneNames, 
CoreMatchers.hasItems(PRE_DEFINED_INDEXING_LANES));
+
+        client.waitForAsyncIndexing();
+
+        IndexingClient otherClient = client.adaptTo(IndexingClient.class);
+
+        retrievedLaneNames = otherClient.getLaneNames();
+        Assert.assertEquals("Mismatched number of lanes", 
PRE_DEFINED_INDEXING_LANES.length, retrievedLaneNames.size());
+        Assert.assertThat(retrievedLaneNames, 
CoreMatchers.hasItems(PRE_DEFINED_INDEXING_LANES));
+
+        otherClient.waitForAsyncIndexing();
+
+        Assert.assertEquals("Must not get indexing lanes from /system/console",
+                0, NUM_INDEXING_LANE_CONSOLE_CALLS.get());
+    }
+
+    private static List<NameValuePair> extractParameters(HttpRequest request) {
+        if (request instanceof HttpEntityEnclosingRequest) {
+            HttpEntity entity = ((HttpEntityEnclosingRequest) 
request).getEntity();
+            try {
+                return URLEncodedUtils.parse(entity);
+            } catch (IOException e) {
+                LOG.error("Failed to parse entity", e);
+            }
+        }
+
+        return new ArrayList<>();
+    }
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/osgi/ServicesInfoTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/osgi/ServicesInfoTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/osgi/ServicesInfoTest.java
 Thu Apr  9 13:50:42 2020
@@ -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.testing.clients.osgi;
+
+import org.hamcrest.Matchers;
+import org.junit.Assert;
+import org.junit.Test;
+
+public class ServicesInfoTest {
+
+    @Test
+    public void testSplitPseudoJsonValueArray() {
+        Assert.assertThat(ServicesInfo.splitPseudoJsonValueArray("test"), 
Matchers.contains("test"));
+        Assert.assertThat(ServicesInfo.splitPseudoJsonValueArray("[]"), 
Matchers.contains(""));
+        Assert.assertThat(ServicesInfo.splitPseudoJsonValueArray("[one, 
two]"), Matchers.contains("one", "two"));
+        Assert.assertThat(ServicesInfo.splitPseudoJsonValueArray("[one,two]"), 
Matchers.contains("one", "two"));
+        
Assert.assertThat(ServicesInfo.splitPseudoJsonValueArray("[java.lang.Runnable, 
org.apache.sling.event.impl.jobs.queues.QueueManager, 
org.osgi.service.event.EventHandler]"),
+                Matchers.contains("java.lang.Runnable", 
"org.apache.sling.event.impl.jobs.queues.QueueManager", 
"org.osgi.service.event.EventHandler"));
+    }
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/query/QueryClientTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/query/QueryClientTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/query/QueryClientTest.java
 Thu Apr  9 13:50:42 2020
@@ -0,0 +1,167 @@
+/*
+ * 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.testing.clients.query;
+
+import org.apache.http.HttpException;
+import org.apache.http.HttpRequest;
+import org.apache.http.HttpResponse;
+import org.apache.http.NameValuePair;
+import org.apache.http.client.utils.URLEncodedUtils;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.message.BasicHttpEntityEnclosingRequest;
+import org.apache.http.protocol.HttpContext;
+import org.apache.http.protocol.HttpRequestHandler;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.HttpServerRule;
+import org.codehaus.jackson.JsonNode;
+import org.junit.Assert;
+import org.junit.ClassRule;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.nio.charset.Charset;
+import java.util.List;
+
+public class QueryClientTest {
+    private static final Logger LOG = 
LoggerFactory.getLogger(QueryClientTest.class);
+
+    private static final String QUERY_PATH = "/system/testing/query"; // same 
as in QueryServlet
+    private static final String BUNDLE_PATH = 
"/system/console/bundles/org.apache.sling.testing.clients.query";
+    private static final String QUERY_RESPONSE = "{\"total\": 1234,\"time\": 
1}";
+    private static final String EXPLAIN_RESPONSE = "{\"plan\": \"some 
plan\",\"time\": 1}";
+    private static final String JSON_BUNDLE = "{\n" +
+            "  \"status\": \"Bundle information: 546 bundles in total, 537 
bundles active, 8 bundles active fragments, 1 bundle resolved.\",\n" +
+            "  \"s\": [\n" +
+            "    546,\n" +
+            "    537,\n" +
+            "    8,\n" +
+            "    1,\n" +
+            "    0\n" +
+            "  ],\n" +
+            "  \"data\": [\n" +
+            "    {\n" +
+            "      \"id\": 560,\n" +
+            "      \"name\": \"Query servlet for testing\",\n" +
+            "      \"fragment\": false,\n" +
+            "      \"stateRaw\": 32,\n" +
+            "      \"state\": \"Active\",\n" +
+            "      \"version\": \"1.0.0\",\n" +
+            "      \"symbolicName\": 
\"org.apache.sling.testing.clients.query\",\n" +
+            "      \"category\": \"\"\n" +
+            "    }\n" +
+            "  ]\n" +
+            "}";
+
+    @ClassRule
+    public static HttpServerRule httpServer = new HttpServerRule() {
+        @Override
+        protected void registerHandlers() throws IOException {
+
+            // Normal query request
+            serverBootstrap.registerHandler(QUERY_PATH, new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context) throws HttpException, IOException {
+                        List<NameValuePair> parameters = URLEncodedUtils.parse(
+                                request.getRequestLine().getUri(), 
Charset.defaultCharset());
+
+                        for (NameValuePair parameter : parameters) {
+                            if (parameter.getName().equals("explain") && 
!parameter.getValue().equals("false")) {
+                                response.setEntity(new 
StringEntity(EXPLAIN_RESPONSE));
+                                return;
+                            }
+                        }
+
+                    response.setEntity(new StringEntity(QUERY_RESPONSE));
+                }
+            });
+
+            // Install servlet
+            serverBootstrap.registerHandler("/system/console/bundles", new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context) throws HttpException, IOException {
+                    // is install (post) or checking status (get)
+                    if (request instanceof BasicHttpEntityEnclosingRequest) {
+                        response.setStatusCode(302);
+                    } else {
+                        response.setStatusCode(200);
+                    }
+                }
+            });
+
+            // Check bundle status
+            serverBootstrap.registerHandler(BUNDLE_PATH + ".json", new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context) throws HttpException, IOException {
+                    response.setEntity(new StringEntity(JSON_BUNDLE));
+                }
+            });
+
+            // Uninstall bundle
+            serverBootstrap.registerHandler(BUNDLE_PATH, new 
HttpRequestHandler() {
+                @Override
+                public void handle(HttpRequest request, HttpResponse response, 
HttpContext context) throws HttpException, IOException {
+                        response.setStatusCode(200);
+                }
+            });
+        }
+    };
+
+    private static QueryClient client;
+
+    public QueryClientTest() throws ClientException {
+        client = new QueryClient(httpServer.getURI(), "admin", "admin");
+        // for testing an already running instance
+        // client = new 
QueryClient(java.net.URI.create("http://localhost:8080";), "admin", "admin");
+    }
+
+    @Test
+    public void testInstallServlet() throws ClientException, 
InterruptedException {
+        client.installServlet();
+    }
+
+    @Test
+    public void testDoQuery() throws ClientException, InterruptedException {
+        JsonNode response = client.doQuery("SELECT * FROM [nt:file] WHERE 
ISDESCENDANTNODE([/etc/])",
+//        JsonNode response = client.doQuery("SELECT * FROM [cq:Tag] WHERE 
ISDESCENDANTNODE([/etc/])",
+                QueryClient.QueryType.SQL2);
+        LOG.info(response.toString());
+        Assert.assertNotEquals(0, response.get("total").getLongValue());
+    }
+
+    @Test
+    public void testDoCount() throws ClientException, InterruptedException {
+        long results = client.doCount("SELECT * FROM [nt:file] WHERE 
ISDESCENDANTNODE([/etc/])",
+                QueryClient.QueryType.SQL2);
+        LOG.info("results={}", results);
+        Assert.assertNotEquals(0, results);
+    }
+
+    @Test
+    public void testGetPlan() throws ClientException, InterruptedException {
+        String plan = client.getPlan("SELECT * FROM [nt:file] WHERE 
ISDESCENDANTNODE([/etc/])",
+                QueryClient.QueryType.SQL2);
+        LOG.info("plan={}", plan);
+        Assert.assertNotEquals("", plan);
+    }
+
+    @Test
+    public void testUninstallServlet() throws ClientException {
+        client.uninstallServlet();
+    }
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/clients/util/poller/PollingTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/clients/util/poller/PollingTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/clients/util/poller/PollingTest.java
 Thu Apr  9 13:50:42 2020
@@ -0,0 +1,178 @@
+/*
+ * 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.testing.clients.util.poller;
+
+import org.apache.commons.lang3.mutable.MutableBoolean;
+import org.apache.commons.lang3.mutable.MutableInt;
+import org.junit.Assert;
+import org.junit.Test;
+import java.util.concurrent.TimeoutException;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+public class PollingTest {
+    @Test
+    public void testCallOnce() throws Exception {
+        final MutableInt callCount = new MutableInt(0);
+        Polling p = new Polling() {
+            @Override
+            public Boolean call() throws Exception {
+                callCount.increment();
+                return true;
+            }
+        };
+        p.poll(500, 10);
+
+        assertEquals(1, callCount.intValue());
+    }
+
+    @Test
+    public void testCallTwice() throws Exception {
+        final MutableInt callCount = new MutableInt(0);
+        final MutableBoolean called = new MutableBoolean(false);
+        Polling p = new Polling() {
+            @Override
+            public Boolean call() throws Exception {
+                callCount.increment();
+                boolean b = called.booleanValue();
+                called.setTrue();
+                return b;
+            }
+        };
+        p.poll(500, 10);
+
+        assertEquals(2, callCount.intValue());
+    }
+
+    @Test
+    public void testCallTimeout() throws Exception {
+        final MutableInt callCount = new MutableInt(0);
+        Polling p = new Polling() {
+            @Override
+            public Boolean call() throws Exception {
+                callCount.increment();
+                return false;
+            }
+        };
+
+        try {
+            p.poll(100, 10);
+        } catch (TimeoutException e ) {
+            assertTrue("Expected to execute call() at least 4 times, got 
instead only " + callCount.intValue() + " calls",
+                    callCount.intValue() > 5);
+            return;
+        }
+
+        fail("Did not reach timeout");
+    }
+
+    @Test
+    public void testNegativeTimeout() throws Exception {
+        final MutableInt callCount = new MutableInt(0);
+        Polling p = new Polling() {
+            @Override
+            public Boolean call() throws Exception {
+                callCount.increment();
+                return true;
+            }
+        };
+        p.poll(-1, 10);
+
+        assertEquals(1, callCount.intValue());
+    }
+
+    //
+    // Tests with Callable
+    //
+
+    @Test
+    public void testCallableOnce() throws Exception {
+        final MutableInt callCount = new MutableInt(0);
+        final MutableBoolean called = new MutableBoolean(false);
+        Polling p = new Polling(() -> {
+            callCount.increment();
+            return true;
+        });
+        p.poll(500, 10);
+
+        assertEquals(1, callCount.intValue());
+    }
+
+    @Test
+    public void testCallableTwice() throws Exception {
+        final MutableInt callCount = new MutableInt(0);
+        final MutableBoolean called = new MutableBoolean(false);
+        Polling p = new Polling(() -> {
+            callCount.increment();
+            boolean b = called.booleanValue();
+            called.setTrue();
+            return b;
+        });
+        p.poll(500, 10);
+
+        assertEquals(2, callCount.intValue());
+    }
+
+    @Test
+    public void testCallableTimeout() throws Exception {
+        final MutableInt callCount = new MutableInt(0);
+        Polling p = new Polling(() -> {
+            callCount.increment();
+            return false;
+        });
+
+        try {
+            p.poll(100, 10);
+        } catch (TimeoutException e ) {
+            assertTrue("Expected to execute call() at least 4 times, got 
instead only " + callCount.intValue() + " calls",
+                    callCount.intValue() > 5);
+            return;
+        }
+
+        fail("Did not reach timeout");
+    }
+
+
+    @Test
+    public void testCallPriority() throws Exception {
+        Polling p = new Polling(() -> false) {
+            @Override
+            public Boolean call() throws Exception {
+                return true;
+            }
+        };
+
+        // Should not reach timeout since overridden call() has priority over 
Callable param
+        p.poll(100, 10);
+    }
+
+    @Test
+    public void testCallThrowException() throws Exception {
+        Polling p = new Polling(() -> {
+          throw new RuntimeException("%Failure");
+        });
+        try {
+            p.poll(100, 10);
+        } catch (TimeoutException e) {
+            Assert.assertTrue("Timeout message should contain original 
message", e.getMessage().contains("%Failure"));
+        }
+
+    }
+
+
+}
\ No newline at end of file

Added: 
release/sling/src/test/java/org/apache/sling/testing/util/ResourceUtilTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/util/ResourceUtilTest.java 
(added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/util/ResourceUtilTest.java 
Thu Apr  9 13:50:42 2020
@@ -0,0 +1,41 @@
+/*******************************************************************************
+ * 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
+ * <p/>
+ * http://www.apache.org/licenses/LICENSE-2.0
+ * <p/>
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ 
******************************************************************************/
+package org.apache.sling.testing.util;
+
+import org.apache.sling.testing.clients.util.ResourceUtil;
+import org.junit.Test;
+
+import java.io.IOException;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+public class ResourceUtilTest {
+
+    private static final long EXPECTED_NB_CARRIAGE_RETURNS = 6;
+
+    @Test
+    public void testReadResourceAsStringNoExtraLineBreak() throws IOException {
+        String input = 
ResourceUtil.readResourceAsString("/resource-util-test-file.txt");
+        assertNotNull(input);
+        long count = input.chars().filter(ch -> ch == '\n').count();
+        assertEquals("Expecting to have 6 line break in the given text file.", 
EXPECTED_NB_CARRIAGE_RETURNS, count);
+    }
+
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/util/UniquePathsTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/util/UniquePathsTest.java 
(added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/util/UniquePathsTest.java 
Thu Apr  9 13:50:42 2020
@@ -0,0 +1,76 @@
+/*
+ * 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.testing.util;
+
+import org.apache.sling.testing.clients.util.UniquePaths;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.lang.reflect.Field;
+import java.util.concurrent.atomic.AtomicLong;
+
+import static org.junit.Assert.assertEquals;
+
+public class UniquePathsTest {
+
+    @Before
+    public void setup() throws Exception {
+        // Set known startTime and counter values for tests
+        {
+            final Field f = UniquePaths.class.getDeclaredField("startTime");
+            f.setAccessible(true);
+            f.set(UniquePaths.class, 1234L);
+        }
+        {
+            final Field f = UniquePaths.class.getDeclaredField("counter");
+            f.setAccessible(true);
+            f.set(UniquePaths.class, new AtomicLong(9362L));
+        }
+    }
+    
+    @Test
+    public void testNoUPattern() {
+        assertEquals("/tmp/UniquePathsTest_1234_9363", UniquePaths.get(this, 
"/tmp/"));
+        assertEquals("/bar/UniquePathsTest_1234_9364", UniquePaths.get(this, 
"/bar/"));
+    }
+    
+    @Test
+    public void testSingleUPattern() {
+        assertEquals("/tmp/UniquePathsTest_1234_9363/foo", 
UniquePaths.get(this, "/tmp/_UNIQ_/foo"));
+    }
+    
+    @Test
+    public void testMultipleUPattern() {
+        assertEquals(
+                
"/tmp/UniquePathsTest_1234_9363/foo/UniquePathsTest_1234_9363.html", 
+                UniquePaths.get(this, "/tmp/_UNIQ_/foo/_UNIQ_.html"));
+    }
+    
+    @Test
+    public void testNullPattern() {
+        assertEquals(
+                "UniquePathsTest_1234_9363", 
+                UniquePaths.get(this, null));
+    }
+    
+    @Test
+    public void testNoPattern() {
+        assertEquals(
+                "UniquePathsTest_1234_9363", 
+                UniquePaths.get(this));
+    }
+}

Added: 
release/sling/src/test/java/org/apache/sling/testing/util/poller/AbstractPollerTest.java
==============================================================================
--- 
release/sling/src/test/java/org/apache/sling/testing/util/poller/AbstractPollerTest.java
 (added)
+++ 
release/sling/src/test/java/org/apache/sling/testing/util/poller/AbstractPollerTest.java
 Thu Apr  9 13:50:42 2020
@@ -0,0 +1,107 @@
+/*
+ * 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.testing.util.poller;
+
+import org.apache.sling.testing.clients.util.poller.AbstractPoller;
+import org.junit.Assert;
+import org.junit.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AbstractPollerTest {
+    private static final Logger LOG = 
LoggerFactory.getLogger(AbstractPollerTest.class);
+
+    @Test
+    public void testCallAndWaitSuccess() throws InterruptedException {
+        AbstractPoller poller = new AbstractPoller(100, 5) {
+            int callNumber = 0;
+
+            @Override
+            public boolean call() {
+                return true;
+            }
+
+            @Override
+            public boolean condition() {
+                callNumber += 1;
+                LOG.debug("Call nr " + callNumber);
+                if (callNumber == 4) {
+                    return true;
+                }
+                return false;
+            }
+        };
+        Assert.assertTrue(poller.callAndWait());
+    }
+
+    @Test
+    public void testCallAndWaitFailure() throws InterruptedException {
+        AbstractPoller poller = new AbstractPoller(100, 5) {
+            @Override
+            public boolean call() {
+                return true;
+            }
+
+            @Override
+            public boolean condition() {
+                return false;
+            }
+        };
+        Assert.assertFalse(poller.callAndWait());
+    }
+
+    @Test
+    public void testCallUntilSuccess() throws InterruptedException {
+        AbstractPoller poller = new AbstractPoller(100, 5) {
+            int callNumber = 0;
+
+            @Override
+            public boolean call() {
+                callNumber += 1;
+                LOG.debug("Call nr " + callNumber);
+                return true;
+            }
+
+            @Override
+            public boolean condition() {
+                if (callNumber == 4) {
+                    return true;
+                }
+                return false;
+            }
+        };
+        Assert.assertTrue(poller.callUntilCondition());
+    }
+
+    @Test
+    public void testCallUntilFailure() throws InterruptedException {
+        AbstractPoller poller = new AbstractPoller(100, 5) {
+            @Override
+            public boolean call() {
+                return true;
+            }
+
+            @Override
+            public boolean condition() {
+                return false;
+            }
+        };
+        Assert.assertFalse(poller.callUntilCondition());
+    }
+
+
+}

Added: release/sling/src/test/resources/resource-util-test-file.txt
==============================================================================
--- release/sling/src/test/resources/resource-util-test-file.txt (added)
+++ release/sling/src/test/resources/resource-util-test-file.txt Thu Apr  9 
13:50:42 2020
@@ -0,0 +1,7 @@
+Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor 
incididunt ut labore et dolore magna aliqua.
+Odio facilisis mauris sit amet massa vitae. Ipsum a arcu cursus vitae congue 
mauris rhoncus.
+Sit amet consectetur adipiscing elit ut aliquam purus sit. Non pulvinar neque 
laoreet suspendisse interdum consectetur.
+Quisque id diam vel quam elementum pulvinar. Cras pulvinar mattis nunc sed 
blandit libero volutpat sed.
+Accumsan lacus vel facilisis volutpat est velit egestas dui id. Vivamus at 
augue eget arcu dictum.
+Turpis egestas integer eget aliquet nibh. Lobortis feugiat vivamus at augue 
eget arcu dictum.
+Lorem ipsum dolor sit amet consectetur adipiscing elit pellentesque.
\ No newline at end of file


Reply via email to