SLING-5681 Verify that a bundle is started correctly after deployment

git-svn-id: https://svn.apache.org/repos/asf/sling/trunk@1784084 13f79535-47bb-0310-9956-ffa450edef68
diff --git a/pom.xml b/pom.xml
index 335bf31..e102590 100644
--- a/pom.xml
+++ b/pom.xml
@@ -84,6 +84,18 @@
       <artifactId>org.apache.sling.junit.core</artifactId>
       <version>1.0.24-SNAPSHOT</version>
     </dependency>
+    <!-- JSR 353 (JSON-P 1.0) implementation -->
+    <dependency>
+      <groupId>org.apache.johnzon</groupId>
+      <artifactId>johnzon-core</artifactId>
+      <version>1.0.0</version>
+    </dependency>
+    <!-- JSR 353 API -->
+    <dependency>
+       <groupId>org.apache.geronimo.specs</groupId>
+       <artifactId>geronimo-json_1.0_spec</artifactId>
+       <version>1.0-alpha-1</version>
+    </dependency>
     <dependency>
       <groupId>commons-io</groupId>
       <artifactId>commons-io</artifactId>
diff --git a/src/main/java/org/apache/sling/testing/teleporter/client/TeleporterHttpClient.java b/src/main/java/org/apache/sling/testing/teleporter/client/TeleporterHttpClient.java
index 7aa1810..e06617d 100644
--- a/src/main/java/org/apache/sling/testing/teleporter/client/TeleporterHttpClient.java
+++ b/src/main/java/org/apache/sling/testing/teleporter/client/TeleporterHttpClient.java
@@ -21,17 +21,29 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.ObjectInputStream;
+import java.io.StringReader;
+import java.io.StringWriter;
 import java.net.HttpURLConnection;
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLConnection;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonException;
+import javax.json.JsonObject;
+import javax.json.JsonReader;
+import javax.json.JsonString;
+import javax.json.JsonValue;
+import javax.json.JsonValue.ValueType;
 import javax.xml.bind.DatatypeConverter;
 
+import org.apache.commons.io.IOUtils;
 import org.junit.runner.Result;
 import org.junit.runner.notification.Failure;
 import org.junit.runners.model.MultipleFailureException;
@@ -46,6 +58,23 @@
     private String credentials = null;
     private final String testServletPath;
     
+    static final class SimpleHttpResponse {
+        private final int status;
+        private final String body;
+
+        public SimpleHttpResponse(int status, String body) {
+            super();
+            this.status = status;
+            this.body = body;
+        }
+        public int getStatus() {
+            return status;
+        }
+        public String getBody() {
+            return body;
+        }
+    }
+    
     TeleporterHttpClient(String baseUrl, String testServletPath) {
         this.baseUrl = baseUrl;
         if(!testServletPath.endsWith("/")) {
@@ -66,16 +95,16 @@
     }
 
     /** Wait until specified URL returns specified status */
-    public void waitForStatus(String url, int expectedStatus, int timeoutMsec) throws IOException {
+    public String waitForStatus(String url, int expectedStatus, int timeoutMsec) throws IOException {
         final long end = System.currentTimeMillis() + timeoutMsec;
         final Set<Integer> statusSet = new HashSet<Integer>();
         final ExponentialBackoffDelay d = new ExponentialBackoffDelay(50,  250);
         while(System.currentTimeMillis() < end) {
             try {
-                final int status = getHttpGetStatus(url);
-                statusSet.add(status);
-                if(status == expectedStatus) {
-                    return;
+                final SimpleHttpResponse response = getHttpGetStatus(url);
+                statusSet.add(response.getStatus());
+                if(response.getStatus() == expectedStatus) {
+                    return response.getBody();
                 }
                 d.waitNextDelay();
             } catch(Exception ignore) {
@@ -108,6 +137,52 @@
             cleanup(c);
         }
     }
+    
+    void verifyCorrectBundleState(String bundleSymbolicName, int webConsoleReadyTimeoutSeconds) throws IOException {
+        final String url = baseUrl + "/system/console/bundles/" + bundleSymbolicName + ".json";
+        
+        String jsonBody = waitForStatus(url, 200, webConsoleReadyTimeoutSeconds * 1000);
+        // deserialize json (https://issues.apache.org/jira/browse/SLING-6536)
+        try {
+            JsonReader jsonReader = Json.createReader(new StringReader(jsonBody));
+            // extract state
+            JsonArray jsonArray = jsonReader.readObject().getJsonArray("data");
+            if (jsonArray == null) {
+                throw new JsonException("Could not find 'data' array");
+            }
+            JsonObject bundleObject = jsonArray.getJsonObject(0);
+            String state = bundleObject.getString("state");
+            if ("Active".equals(state)) {
+                return;
+            }
+            // otherwise evaluate the import section
+            JsonArray propsArray = bundleObject.getJsonArray("props");
+            if (propsArray == null) {
+                throw new JsonException("Could not find 'props' object");
+            }
+            // iterate through all of them until key="Imported Packages" is found
+            for (JsonValue propValue : propsArray) {
+                if (propValue.getValueType().equals(ValueType.OBJECT)) {
+                    JsonObject propObject = (JsonObject)propValue;
+                    if ("Imported Packages".equals(propObject.getString("key"))) {
+                        JsonArray importedPackagesArray = propObject.getJsonArray("value");
+                        String reason = "Unknown";
+                        for (JsonValue importedPackageValue : importedPackagesArray) {
+                            if (importedPackageValue.getValueType().equals(ValueType.STRING)) {
+                                String importedPackage = ((JsonString)importedPackageValue).getString();
+                                if (importedPackage.startsWith("ERROR:")) {
+                                    reason = importedPackage;
+                                }
+                            }
+                        }
+                        throw new IllegalStateException("The test bundle is in state " + state +". Most probably this is due to unresolved import-packages: " + reason);
+                    }
+                }
+            }
+        } catch (JsonException|IndexOutOfBoundsException e) {
+            throw new IllegalArgumentException("Test bundle '" + bundleSymbolicName +"' not correctly installed. Could not parse JSON response though to expose further information: " + jsonBody, e);
+        }
+    }
 
     void uninstallBundle(String bundleSymbolicName, int webConsoleReadyTimeoutSeconds) throws MalformedURLException, IOException {
         // equivalent of
@@ -131,7 +206,7 @@
         }
     }
     
-    public int getHttpGetStatus(String url) throws MalformedURLException, IOException {
+    public SimpleHttpResponse getHttpGetStatus(String url) throws MalformedURLException, IOException {
         final HttpURLConnection c = (HttpURLConnection)new URL(url).openConnection();
         setConnectionCredentials(c);
         c.setUseCaches(false);
@@ -139,16 +214,22 @@
         c.setDoInput(true);
         c.setInstanceFollowRedirects(false);
         boolean gotStatus = false;
-        int result = 0;
+        int status = 0;
         try {
-            result = c.getResponseCode();
+            status = c.getResponseCode();
             gotStatus = true;
+            //4xx: client error, 5xx: server error. See: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html.
+            boolean isError = status >= 400;
+            //In HTTP error cases, HttpURLConnection only gives you the input stream via #getErrorStream().
+            InputStream is = isError ? c.getErrorStream() : c.getInputStream();
+            StringWriter writer = new StringWriter();
+            IOUtils.copy(is, writer, StandardCharsets.UTF_8);
+            return new SimpleHttpResponse(status, writer.toString());
         } finally {
             // If we didn't get a status, do not attempt
             // to get input streams as this would retry connecting
             cleanup(c, gotStatus);
         }
-        return result;
     }
 
     void runTests(String testSelectionPath, int testReadyTimeoutSeconds) throws MalformedURLException, IOException, MultipleFailureException {
@@ -158,7 +239,7 @@
         final long timeout = System.currentTimeMillis() + (testReadyTimeoutSeconds * 1000L);
         final ExponentialBackoffDelay delay = new ExponentialBackoffDelay(25, 1000);
         while(true) {
-            if(getHttpGetStatus(testUrl) == 200) {
+            if(getHttpGetStatus(testUrl).getStatus() == 200) {
                 break;
             }
             if(System.currentTimeMillis() > timeout) {
diff --git a/src/test/java/org/apache/sling/testing/teleporter/client/TeleporterHttpClientTest.java b/src/test/java/org/apache/sling/testing/teleporter/client/TeleporterHttpClientTest.java
index d40981f..1cdcb48 100644
--- a/src/test/java/org/apache/sling/testing/teleporter/client/TeleporterHttpClientTest.java
+++ b/src/test/java/org/apache/sling/testing/teleporter/client/TeleporterHttpClientTest.java
@@ -23,12 +23,17 @@
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.fail;
 
+import java.io.FileReader;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.net.MalformedURLException;
+import java.nio.charset.StandardCharsets;
 import java.util.Timer;
 import java.util.TimerTask;
 import java.util.UUID;
 
+import org.apache.commons.io.IOUtils;
 import org.apache.sling.testing.clients.util.TimeoutsProvider;
 import org.junit.Rule;
 import org.junit.Test;
@@ -58,10 +63,10 @@
         final TeleporterHttpClient client = new TeleporterHttpClient(baseUrl, TEST_PATH);
         final String testUrl = baseUrl + TEST_PATH;
         
-        assertEquals(404, client.getHttpGetStatus(baseUrl + TEST_PATH));
+        assertEquals(404, client.getHttpGetStatus(baseUrl + TEST_PATH).getStatus());
         activateLater(TEST_PATH, 1000);
         client.waitForStatus(testUrl, 200, TimeoutsProvider.getInstance().getTimeout(2000));
-        assertEquals(200, client.getHttpGetStatus(baseUrl + TEST_PATH));
+        assertEquals(200, client.getHttpGetStatus(baseUrl + TEST_PATH).getStatus());
     }
     
     @Test
@@ -69,7 +74,7 @@
         final TeleporterHttpClient client = new TeleporterHttpClient(baseUrl, TEST_PATH);
         final String testUrl = baseUrl + TEST_PATH;
         
-        assertEquals(404, client.getHttpGetStatus(baseUrl + TEST_PATH));
+        assertEquals(404, client.getHttpGetStatus(baseUrl + TEST_PATH).getStatus());
         activateLater(TEST_PATH, 1000);
         
         try {
@@ -91,11 +96,35 @@
         int status = 0;
         for(int i=0; i < N; i++) {
             try {
-                status = client.getHttpGetStatus(testUrl);
+                status = client.getHttpGetStatus(testUrl).getStatus();
             } catch(Exception e) {
                 fail("Exception at index " + i + ":" + e);
             }
             assertEquals("Expecting status 200 at index " + i, 200, status);
         }
     }
+    
+    @Test(expected=IllegalStateException.class)
+    public void testVerifyCorrectBundleStateForInactiveBundle() throws IOException {
+        final TeleporterHttpClient client = new TeleporterHttpClient(baseUrl, "invalid");
+        String bundleSymbolicName = "testBundle1";
+        // open resource
+        try (InputStream inputStream = this.getClass().getResourceAsStream("/bundle-not-active.json")) {
+            String body = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+            givenThat(get(urlEqualTo("/system/console/bundles/" + bundleSymbolicName + ".json")).willReturn(aResponse().withStatus(200).withBody(body)));
+        }
+        client.verifyCorrectBundleState(bundleSymbolicName, 1);
+    }
+    
+    @Test
+    public void testVerifyCorrectBundleStateForActiveBundle() throws IOException {
+        final TeleporterHttpClient client = new TeleporterHttpClient(baseUrl, "invalid");
+        String bundleSymbolicName = "testBundle2";
+        // open resource
+        try (InputStream inputStream = this.getClass().getResourceAsStream("/bundle-active.json")) {
+            String body = IOUtils.toString(inputStream, StandardCharsets.UTF_8);
+            givenThat(get(urlEqualTo("/system/console/bundles/" + bundleSymbolicName + ".json")).willReturn(aResponse().withStatus(200).withBody(body)));
+        }
+        client.verifyCorrectBundleState(bundleSymbolicName, 1);
+    }
 }
\ No newline at end of file
diff --git a/src/test/resources/bundle-active.json b/src/test/resources/bundle-active.json
new file mode 100644
index 0000000..5072973
--- /dev/null
+++ b/src/test/resources/bundle-active.json
@@ -0,0 +1 @@
+{"status":"Bundle information: 148 bundles in total - all 148 bundles active.","s":[148,144,4,0,0],"data":[{"id":147,"name":"ClientSideTeleporter.ValidationServiceIT.18-27-36.cd5a3250-766b-47a0-af9a-b5e56c0079c0","fragment":false,"stateRaw":32,"state":"Active","version":"0","symbolicName":"ClientSideTeleporter.ValidationServiceIT.18-27-36.cd5a3250-766b-47a0-af9a-b5e56c0079c0","category":"","props":[{"key":"Symbolic Name","value":"ClientSideTeleporter.ValidationServiceIT.18-27-36.cd5a3250-766b-47a0-af9a-b5e56c0079c0"},{"key":"Version","value":"0"},{"key":"Bundle Location","value":"inputstream:ClientSideTeleporter.ValidationServiceIT.18-27-36.cd5a3250-766b-47a0-af9a-b5e56c0079c0.jar"},{"key":"Last Modification","value":"Wed Feb 22 18:27:37 CET 2017"},{"key":"Start Level","value":20},{"key":"Exported Packages","value":"---"},{"key":"Imported Packages","value":["org.apache.sling.api.resource,version=2.10.0 from <a href='/system/console/bundles/94'>org.apache.sling.api (94)<\/a>","org.apache.sling.api.wrappers,version=2.6.0 from <a href='/system/console/bundles/94'>org.apache.sling.api (94)<\/a>","org.apache.sling.commons.json,version=2.0.4 from <a href='/system/console/bundles/38'>org.apache.sling.commons.json (38)<\/a>","org.apache.sling.junit.rules,version=1.3.0 from <a href='/system/console/bundles/122'>org.apache.sling.junit.core (122)<\/a>","org.apache.sling.validation,version=1.0.0 from <a href='/system/console/bundles/145'>org.apache.sling.validation.api (145)<\/a>","org.apache.sling.validation.model,version=1.0.0 from <a href='/system/console/bundles/145'>org.apache.sling.validation.api (145)<\/a>","org.junit,version=4.12.0 from <a href='/system/console/bundles/122'>org.apache.sling.junit.core (122)<\/a>"]},{"key":"Manifest Headers","value":["Bnd-LastModified: 1487784456896","Built-By: konradwindszus","Bundle-ManifestVersion: 2","Bundle-Name: ClientSideTeleporter.ValidationServiceIT.18-27-36.cd5a3250-766b-47a0-af9a-b5e56c0079c0","Bundle-SymbolicName: ClientSideTeleporter.ValidationServiceIT.18-27-36.cd5a3250-766b-47a0-af9a-b5e56c0079c0","Bundle-Version: 0","Created-By: 1.8.0_66 (Oracle Corporation)","Import-Package: org.apache.sling.api.resource, org.apache.sling.api.wrappers, org.apache.sling.commons.json, org.apache.sling.junit.rules, org.apache.sling.validation, org.apache.sling.validation.model, org.junit","Manifest-Version: 1.0","Originally-Created-By: pax-tinybundles-2.0.0","Private-Package: SLING-CONTENT.apps.sling.validation, SLING-CONTENT.apps.sling.validation.models, org.apache.sling.validation.impl","Sling-Test-Regexp: org.apache.sling.validation.impl.ValidationServiceIT.*","Sling-Test-WaitForService-Timeout: 10","TinybundlesVersion: pax-tinybundles-2.0.0","Tool: Bnd-2.1.0.20130426-122213"]},{"key":"nfo","value":{}}]}]}
\ No newline at end of file
diff --git a/src/test/resources/bundle-not-active.json b/src/test/resources/bundle-not-active.json
new file mode 100644
index 0000000..17027bd
--- /dev/null
+++ b/src/test/resources/bundle-not-active.json
@@ -0,0 +1 @@
+{"status":"Bundle information: 147 bundles in total, 142 bundles active, 4 bundles active fragments, 1 bundle installed.","s":[147,142,4,0,1],"data":[{"id":147,"name":"ClientSideTeleporter.ValidationServiceIT.12-39-11.de72ad9c-a053-4767-80b1-5e3c25ed4cee","fragment":false,"stateRaw":2,"state":"Installed","version":"0","symbolicName":"ClientSideTeleporter.ValidationServiceIT.12-39-11.de72ad9c-a053-4767-80b1-5e3c25ed4cee","category":"","props":[{"key":"Symbolic Name","value":"ClientSideTeleporter.ValidationServiceIT.12-39-11.de72ad9c-a053-4767-80b1-5e3c25ed4cee"},{"key":"Version","value":"0"},{"key":"Bundle Location","value":"inputstream:ClientSideTeleporter.ValidationServiceIT.12-39-11.de72ad9c-a053-4767-80b1-5e3c25ed4cee.jar"},{"key":"Last Modification","value":"Wed Feb 22 12:45:51 CET 2017"},{"key":"Start Level","value":20},{"key":"Imported Packages","value":["org.apache.sling.api.resource from <a href='/system/console/bundles/94'>org.apache.sling.api (94)<\/a>","org.apache.sling.api.wrappers from <a href='/system/console/bundles/94'>org.apache.sling.api (94)<\/a>","org.apache.sling.commons.json from <a href='/system/console/bundles/38'>org.apache.sling.commons.json (38)<\/a>","org.apache.sling.junit.rules from <a href='/system/console/bundles/122'>org.apache.sling.junit.core (122)<\/a>","ERROR: org.apache.sling.validation.model -- Cannot be resolved","org.junit from <a href='/system/console/bundles/122'>org.apache.sling.junit.core (122)<\/a>"]},{"key":"Manifest Headers","value":["Bnd-LastModified: 1487763552010","Built-By: konradwindszus","Bundle-ManifestVersion: 2","Bundle-Name: ClientSideTeleporter.ValidationServiceIT.12-39-11.de72ad9c-a053-4767-80b1-5e3c25ed4cee","Bundle-SymbolicName: ClientSideTeleporter.ValidationServiceIT.12-39-11.de72ad9c-a053-4767-80b1-5e3c25ed4cee","Bundle-Version: 0","Created-By: 1.8.0_66 (Oracle Corporation)","Import-Package: org.apache.sling.api.resource, org.apache.sling.api.wrappers, org.apache.sling.commons.json, org.apache.sling.junit.rules, org.apache.sling.validation, org.apache.sling.validation.model, org.junit","Manifest-Version: 1.0","Originally-Created-By: pax-tinybundles-2.0.0","Private-Package: SLING-CONTENT.apps.sling.validation, SLING-CONTENT.apps.sling.validation.models, org.apache.sling.validation.impl","Sling-Test-Regexp: org.apache.sling.validation.impl.ValidationServiceIT.*","Sling-Test-WaitForService-Timeout: 10","TinybundlesVersion: pax-tinybundles-2.0.0","Tool: Bnd-2.1.0.20130426-122213"]},{"key":"nfo","value":{}}]}]}
\ No newline at end of file