SLING-9512 - add integration tests
diff --git a/pom.xml b/pom.xml
index 6da5b67..e992499 100644
--- a/pom.xml
+++ b/pom.xml
@@ -23,7 +23,7 @@
     <parent>
         <groupId>org.apache.sling</groupId>
         <artifactId>sling-bundle-parent</artifactId>
-        <version>35</version>
+        <version>38</version>
         <relativePath />
     </parent>
 
@@ -34,16 +34,26 @@
     <name>Apache Sling Servlet Helpers</name>
     <description>Mock implementations of SlingHttpServletRequest, SlingHttpServletResponse and related classes.</description>
 
+    <properties>
+        <org.ops4j.pax.exam.version>4.13.3</org.ops4j.pax.exam.version>
+        <!-- To debug the pax process, override this with -D -->
+        <pax.vm.options>-Xmx512M</pax.vm.options>
+    </properties>
+
     <scm>
         <connection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-servlet-helpers.git</connection>
         <developerConnection>scm:git:https://gitbox.apache.org/repos/asf/sling-org-apache-sling-servlet-helpers.git</developerConnection>
         <url>https://gitbox.apache.org/repos/asf?p=sling-org-apache-sling-servlet-helpers.git</url>
       <tag>HEAD</tag>
-  </scm>
+    </scm>
 
     <build>
         <plugins>
             <plugin>
+                <groupId>org.apache.servicemix.tooling</groupId>
+                <artifactId>depends-maven-plugin</artifactId>
+            </plugin>
+            <plugin>
                 <groupId>biz.aQute.bnd</groupId>
                 <artifactId>bnd-maven-plugin</artifactId>
             </plugin>
@@ -51,6 +61,39 @@
                 <groupId>biz.aQute.bnd</groupId>
                 <artifactId>bnd-baseline-maven-plugin</artifactId>
             </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-failsafe-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>integration-test</id>
+                        <phase>integration-test</phase>
+                        <goals>
+                            <goal>integration-test</goal>
+                        </goals>
+                    </execution>
+                    <execution>
+                        <id>verify</id>
+                        <phase>integration-test</phase>
+                        <goals>
+                            <goal>verify</goal>
+                        </goals>
+                    </execution>
+                </executions>
+                <configuration>
+                    <redirectTestOutputToFile>true</redirectTestOutputToFile>
+                    <systemProperties>
+                        <property>
+                            <name>bundle.filename</name>
+                            <value>${basedir}/target/${project.build.finalName}.jar</value>
+                        </property>
+                        <property>
+                            <name>pax.vm.options</name>
+                            <value>${pax.vm.options}</value>
+                        </property>
+                    </systemProperties>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 
@@ -113,11 +156,69 @@
             <scope>test</scope>
         </dependency>
         <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>osgi.cmpn</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.service.component.annotations</artifactId>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
             <groupId>org.mockito</groupId>
             <artifactId>mockito-core</artifactId>
             <version>3.1.0</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-cm</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-container-forked</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-junit4</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.ops4j.pax.exam</groupId>
+            <artifactId>pax-exam-link-mvn</artifactId>
+            <version>${org.ops4j.pax.exam.version}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.geronimo.specs</groupId>
+            <artifactId>geronimo-atinject_1.0_spec</artifactId>
+            <version>1.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.paxexam</artifactId>
+            <version>3.1.0</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.framework</artifactId>
+            <version>6.0.3</version>
+            <scope>test</scope>
+        </dependency>
 
     </dependencies>
 
diff --git a/src/main/java/org/apache/sling/servlethelpers/internalrequests/InternalRequest.java b/src/main/java/org/apache/sling/servlethelpers/internalrequests/InternalRequest.java
index 8822e05..d1ca7e5 100644
--- a/src/main/java/org/apache/sling/servlethelpers/internalrequests/InternalRequest.java
+++ b/src/main/java/org/apache/sling/servlethelpers/internalrequests/InternalRequest.java
@@ -36,6 +36,7 @@
 import org.apache.sling.servlethelpers.MockRequestPathInfo;
 import org.apache.sling.servlethelpers.MockSlingHttpServletRequest;
 import org.apache.sling.servlethelpers.MockSlingHttpServletResponse;
+import org.jetbrains.annotations.NotNull;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.slf4j.MDC;
@@ -72,11 +73,23 @@
     public static final String MDC_KEY = "sling." + InternalRequest.class.getSimpleName();
 
     /** Clients use subclasses of this one  */
-    protected InternalRequest(ResourceResolver resourceResolver, String path) {
+    protected InternalRequest(@NotNull ResourceResolver resourceResolver, @NotNull String path) {
+        checkNotNull(ResourceResolver.class, resourceResolver);
+        checkNotNull("path", path);
         this.resourceResolver = resourceResolver;
         this.path = path;
     }
 
+    protected void checkNotNull(String info, Object candidate) {
+        if(candidate == null) {
+            throw new IllegalArgumentException(info + " is null");
+        }
+    }
+
+    protected void checkNotNull(Class<?> clazz, Object candidate) {
+        checkNotNull(clazz.getSimpleName(), candidate);
+    }
+
     /** Set the HTTP request method to use - defaults to GET */
     public InternalRequest withRequestMethod(String method) {
         this.requestMethod = method.toUpperCase();
diff --git a/src/main/java/org/apache/sling/servlethelpers/internalrequests/ServletInternalRequest.java b/src/main/java/org/apache/sling/servlethelpers/internalrequests/ServletInternalRequest.java
index f94764e..7c3bbf3 100644
--- a/src/main/java/org/apache/sling/servlethelpers/internalrequests/ServletInternalRequest.java
+++ b/src/main/java/org/apache/sling/servlethelpers/internalrequests/ServletInternalRequest.java
@@ -51,6 +51,8 @@
      */
     public ServletInternalRequest(@NotNull ServletResolver servletResolver, @NotNull Resource resource) {
         super(resource.getResourceResolver(), resource.getPath());
+        checkNotNull(ServletResolver.class, servletResolver);
+        checkNotNull(Resource.class, resource);
         this.resource = resource;
         this.servletResolver = servletResolver;
     }
diff --git a/src/main/java/org/apache/sling/servlethelpers/internalrequests/SlingInternalRequest.java b/src/main/java/org/apache/sling/servlethelpers/internalrequests/SlingInternalRequest.java
index 7b2dc90..9057786 100644
--- a/src/main/java/org/apache/sling/servlethelpers/internalrequests/SlingInternalRequest.java
+++ b/src/main/java/org/apache/sling/servlethelpers/internalrequests/SlingInternalRequest.java
@@ -51,6 +51,7 @@
     /** Setup an internal request that uses a SlingRequestProcessor */
     public SlingInternalRequest(@NotNull ResourceResolver resourceResolver, @NotNull SlingRequestProcessor p, @NotNull String path) {
         super(resourceResolver, path);
+        checkNotNull(SlingRequestProcessor.class, p);
         this.processor = p;
     }
 
diff --git a/src/test/java/org/apache/sling/servlethelpers/internalrequests/RequestInfoServlet.java b/src/test/java/org/apache/sling/servlethelpers/internalrequests/RequestInfoServlet.java
index 2db8708..d3aba8b 100644
--- a/src/test/java/org/apache/sling/servlethelpers/internalrequests/RequestInfoServlet.java
+++ b/src/test/java/org/apache/sling/servlethelpers/internalrequests/RequestInfoServlet.java
@@ -18,6 +18,8 @@
  */
 package org.apache.sling.servlethelpers.internalrequests;
 
+import static org.mockito.ArgumentMatchers.nullable;
+
 import java.io.BufferedReader;
 import java.io.IOException;
 import java.util.Arrays;
@@ -52,20 +54,22 @@
             sb.append(" RT_").append(resolutionRequest.getResource().getResourceType());
             sb.append(" RST_").append(resolutionRequest.getResource().getResourceSuperType());
         }
-        sb.append(" RRA_").append(resolutionRequest.getResource().getResourceResolver().getAttribute("testAttribute"));
+        if(resolutionRequest.getResource().getResourceResolver() != null) {
+            sb.append(" RRA_").append(resolutionRequest.getResource().getResourceResolver().getAttribute("testAttribute"));
+        }
 
         resolutionInfo = sb.toString();
     }
 
     @Override
     public void service(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException,ServletException {
-        if(request.getResource().getPath().equals("/EXCEPTION")) {
+        if("/EXCEPTION".equals(request.getResource().getPath())) {
             throw new IOException("Failing as designed");
         }
-        if(request.getResource().getPath().equals("/SERVLET-EXCEPTION")) {
+        if("/SERVLET-EXCEPTION".equals(request.getResource().getPath())) {
             throw new ServletException("Failing as designed");
         }
-        if(request.getMethod().equals("STATUS")) {
+        if("STATUS".equals(request.getMethod())) {
             response.setContentType("farenheit");
             response.sendError(451);
             return;
diff --git a/src/test/java/org/apache/sling/servlethelpers/it/ServletHelpersTestSupport.java b/src/test/java/org/apache/sling/servlethelpers/it/ServletHelpersTestSupport.java
new file mode 100644
index 0000000..e43963f
--- /dev/null
+++ b/src/test/java/org/apache/sling/servlethelpers/it/ServletHelpersTestSupport.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.servlethelpers.it;
+
+import org.apache.sling.api.resource.Resource;
+import org.apache.sling.api.resource.ResourceResolver;
+import org.apache.sling.api.resource.ResourceResolverFactory;
+import org.apache.sling.engine.SlingRequestProcessor;
+import org.apache.sling.servlethelpers.internalrequests.SlingInternalRequest;
+import org.apache.sling.testing.paxexam.TestSupport;
+import org.junit.Before;
+import org.ops4j.pax.exam.Configuration;
+import org.ops4j.pax.exam.Option;
+
+import static org.apache.sling.testing.paxexam.SlingOptions.slingQuickstartOakTar;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.fail;
+import static org.apache.sling.testing.paxexam.SlingOptions.logback;
+import static org.ops4j.pax.exam.CoreOptions.composite;
+import static org.ops4j.pax.exam.CoreOptions.junitBundles;
+import static org.ops4j.pax.exam.CoreOptions.options;
+import static org.ops4j.pax.exam.CoreOptions.vmOption;
+import static org.ops4j.pax.exam.cm.ConfigurationAdminOptions.newConfiguration;
+
+import java.time.Duration;
+import java.time.Instant;
+import java.util.ArrayList;
+import java.util.List;
+
+import javax.inject.Inject;
+
+public abstract class ServletHelpersTestSupport extends TestSupport {
+
+    private final static int STARTUP_WAIT_SECONDS = 30;
+    protected Resource rootResource;
+    protected ResourceResolver adminResourceResolver;
+
+    @Inject
+    protected ResourceResolverFactory rrFactory;
+
+    @Inject
+    protected SlingRequestProcessor slingRequestProcessor;
+
+    @Configuration
+    public Option[] configuration() {
+        return options(
+            composite(
+                super.baseConfiguration(),
+                vmOption(System.getProperty("pax.vm.options")),
+                slingQuickstart(),
+                logback(),
+                junitBundles(),
+                testBundle("bundle.filename"),
+                buildBundleWithBnd(TestServlet.class),
+                newConfiguration("org.apache.sling.jcr.base.internal.LoginAdminWhitelist")
+                    .put("whitelist.bundles.regexp", "^PAXEXAM.*$")
+                    .asOption()
+            )
+        );
+    }
+
+    @Before
+    public void setup() throws Exception {
+        adminResourceResolver = rrFactory.getAdministrativeResourceResolver(null);
+        rootResource = adminResourceResolver.getResource("/");
+        assertNotNull("Expecting root resource", rootResource);
+
+        // Wait for Sling to be ready
+        // detecting services would be more elegant...but this is very reliable
+        final Instant endTime = Instant.now().plus(Duration.ofSeconds(STARTUP_WAIT_SECONDS));
+        final int expectedStatus = 200;
+        final String path ="/";
+        final List<Integer> statuses = new ArrayList<>();
+        boolean ok = false;
+        while(Instant.now().isBefore(endTime)) {
+            final int status = new SlingInternalRequest(adminResourceResolver, slingRequestProcessor, path)
+                .withExtension("json")
+                .execute()
+                .checkStatus()
+                .getStatus()
+            ;
+            statuses.add(status);
+            if(status == expectedStatus) {
+                ok = true;
+                break;
+            }
+            Thread.sleep(250);
+        }
+
+        if(!ok) {
+            fail("Did not get a " + expectedStatus + " status at " + path + " got " + statuses);
+        }
+    }
+
+    protected Option slingQuickstart() {
+        final String workingDirectory = workingDirectory();
+        final int httpPort = findFreePort();
+        return composite(
+            slingQuickstartOakTar(workingDirectory, httpPort)
+        );
+    }
+}
diff --git a/src/test/java/org/apache/sling/servlethelpers/it/ServletInternalRequestIT.java b/src/test/java/org/apache/sling/servlethelpers/it/ServletInternalRequestIT.java
new file mode 100644
index 0000000..6f52c0a
--- /dev/null
+++ b/src/test/java/org/apache/sling/servlethelpers/it/ServletInternalRequestIT.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.servlethelpers.it;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import javax.inject.Inject;
+
+import org.apache.sling.api.servlets.ServletResolver;
+import org.apache.sling.servlethelpers.internalrequests.ServletInternalRequest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class ServletInternalRequestIT extends ServletHelpersTestSupport {
+    @Inject
+    private ServletResolver servletResolver;
+
+    @Test
+    public void testRequest() throws IOException {
+        final String content = new ServletInternalRequest(servletResolver, rootResource)
+            .withExtension("TestServlet")
+            .execute()
+            .checkStatus(200)
+            .getResponseAsString()
+        ;
+        assertEquals(TestServlet.class.getName(), content);
+    }
+}
diff --git a/src/test/java/org/apache/sling/servlethelpers/it/SlingInternalRequestIT.java b/src/test/java/org/apache/sling/servlethelpers/it/SlingInternalRequestIT.java
new file mode 100644
index 0000000..de629fb
--- /dev/null
+++ b/src/test/java/org/apache/sling/servlethelpers/it/SlingInternalRequestIT.java
@@ -0,0 +1,43 @@
+/*
+ * 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.servlethelpers.it;
+
+import static org.junit.Assert.assertEquals;
+
+import java.io.IOException;
+
+import org.apache.sling.servlethelpers.internalrequests.SlingInternalRequest;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.ops4j.pax.exam.junit.PaxExam;
+import org.ops4j.pax.exam.spi.reactors.ExamReactorStrategy;
+import org.ops4j.pax.exam.spi.reactors.PerClass;
+
+@RunWith(PaxExam.class)
+@ExamReactorStrategy(PerClass.class)
+public class SlingInternalRequestIT extends ServletHelpersTestSupport {
+    @Test
+    public void testRequest() throws IOException {
+        final String content = new SlingInternalRequest(adminResourceResolver, slingRequestProcessor, "/")
+            .withExtension("TestServlet")
+            .execute()
+            .checkStatus(200)
+            .getResponseAsString()
+        ;
+        assertEquals(TestServlet.class.getName(), content);
+    }
+}
diff --git a/src/test/java/org/apache/sling/servlethelpers/it/TestServlet.java b/src/test/java/org/apache/sling/servlethelpers/it/TestServlet.java
new file mode 100644
index 0000000..6b47d42
--- /dev/null
+++ b/src/test/java/org/apache/sling/servlethelpers/it/TestServlet.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.servlethelpers.it;
+
+import java.io.IOException;
+
+import javax.servlet.Servlet;
+
+import org.apache.sling.api.SlingHttpServletRequest;
+import org.apache.sling.api.SlingHttpServletResponse;
+import org.apache.sling.api.servlets.SlingAllMethodsServlet;
+import org.osgi.service.component.annotations.Component;
+
+/** Not all servlets can be called directly from a {@link ServletInternalRequest},
+ *  depending on the environment that they expect, request attributes etc.
+ * 
+ *  This is a test one with zero environment requirements.
+ */
+@Component(
+    service=Servlet.class,
+    property = {
+        "sling.servlet.resourceTypes=sling/servlet/default",
+        "sling.servlet.extensions=TestServlet"
+    })
+public class TestServlet extends SlingAllMethodsServlet {
+    private static final long serialVersionUID = 1L;
+
+    @Override
+    public void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
+        response.getWriter().write(getClass().getName());
+    }
+}
\ No newline at end of file
diff --git a/src/test/resources/logback.xml b/src/test/resources/logback.xml
new file mode 100644
index 0000000..a603c15
--- /dev/null
+++ b/src/test/resources/logback.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    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.
+-->
+<configuration>
+  <appender name="file" class="ch.qos.logback.core.FileAppender">
+    <file>target/test.log</file>
+    <append>true</append>
+    <encoder>
+      <pattern>%date level=%level thread=%thread logger=%logger sourcefile=%file line=%line %mdc message=%msg%n</pattern>
+    </encoder>
+  </appender>
+  
+  <root level="info">
+    <appender-ref ref="file"/>
+  </root>
+</configuration>