Add test infrastructure for cloudant
diff --git a/.gitignore b/.gitignore
index c95e0ad..7f6823b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,2 @@
-/node_modules/
-*.log
+.gradle
+build/
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 0000000..4398fee
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1,14 @@
+include 'tests', ':WhiskTest', ':common:scala', ':core:invoker', ':core:controller'
+
+rootProject.name = 'openwhisk-cloudant-trigger'
+def whiskhome = "$System.env.OPENWHISK_HOME"
+
+project(':WhiskTest').projectDir = new File(whiskhome, 'tests')
+project(':common:scala').projectDir = new File(whiskhome + '/common', 'scala')
+project(':core:invoker').projectDir = new File(whiskhome + '/core', 'invoker')
+project(':core:controller').projectDir = new File(whiskhome + '/core', 'controller')
+
+gradle.ext.scala = [
+        version: '2.11.8',
+        compileFlags: ['-feature', '-unchecked', '-deprecation', '-Xfatal-warnings', '-Ywarn-unused-import']
+]
\ No newline at end of file
diff --git a/tests/build.gradle b/tests/build.gradle
new file mode 100644
index 0000000..78e38eb
--- /dev/null
+++ b/tests/build.gradle
@@ -0,0 +1,69 @@
+def whiskhome = "$System.env.OPENWHISK_HOME"
+
+apply plugin: 'scala'
+apply plugin: 'eclipse'
+compileTestScala.options.encoding = 'UTF-8'
+
+repositories {
+    mavenCentral()
+    maven {
+        url 'https://oss.sonatype.org/content/repositories/snapshots/'
+    }
+}
+
+sourceSets {
+    test {
+        scala {
+            srcDirs = ['src/']
+        }
+    }
+}
+
+test {
+    systemProperty 'testthreads', System.getProperty('testthreads', '1')
+    testLogging {
+        events "passed", "skipped", "failed"
+        showStandardStreams = true
+        exceptionFormat = 'full'
+    }
+    outputs.upToDateWhen { false } // force tests to run everytime
+}
+
+dependencies {
+    testCompile "org.scala-lang:scala-library:${gradle.scala.version}"
+    testCompile 'org.apache.commons:commons-exec:1.1'
+    testCompile 'org.apache.commons:commons-lang3:3.3.2'
+    testCompile 'commons-logging:commons-logging:1.1.3'
+    testCompile 'org.codehaus.groovy:groovy:2.4.3'
+    testCompile 'org.codehaus.groovy:groovy-json:2.4.3'
+    testCompile 'org.codehaus.groovy:groovy-xml:2.4.3'
+    testCompile 'com.google.guava:guava:18.0'
+    testCompile 'org.hamcrest:hamcrest-core:1.3'
+    testCompile 'org.apache.httpcomponents:httpmime:4.3.6'
+    testCompile 'junit:junit:4.11'
+    testCompile 'com.jayway.restassured:rest-assured:2.4.1'
+    testCompile 'org.scalatest:scalatest_2.11:2.2.4'
+    testCompile 'org.seleniumhq.selenium:selenium-java:2.45.0'
+    testCompile 'io.spray:spray-testkit_2.11:1.3.3'
+    testCompile 'io.spray:spray-json_2.11:1.3.2'
+    testCompile 'com.google.code.tempus-fugit:tempus-fugit:1.2-SNAPSHOT'
+    testCompile 'com.google.code.gson:gson:2.3.1'
+    testCompile 'com.cloudant:cloudant-client:1.0.1'
+    testCompile project(':WhiskTest').sourceSets.test.output
+}
+
+tasks.withType(ScalaCompile) {
+    scalaCompileOptions.additionalParameters = gradle.scala.compileFlags
+}
+
+def keystorePath = new File(buildDir, 'classes/test/keystore')
+task deleteKeystore(type: Delete) {
+    delete keystorePath
+}
+task createKeystore(dependsOn: deleteKeystore) << {
+    Properties props = new Properties()
+    props.load(new FileInputStream(file(whiskhome+'/whisk.properties')))
+    def cmd = ['keytool', '-import', '-alias', 'Whisk', '-noprompt', '-trustcacerts', '-file', file(props['whisk.ssl.cert']), '-keystore', keystorePath, '-storepass', 'openwhisk']
+    cmd.execute().waitForProcessOutput(System.out, System.err)
+}
+compileTestScala.finalizedBy createKeystore
diff --git a/tests/dat/attach.txt b/tests/dat/attach.txt
new file mode 100644
index 0000000..630ac45
--- /dev/null
+++ b/tests/dat/attach.txt
@@ -0,0 +1 @@
+My hovercraft is full of eels!
diff --git a/tests/dat/indexdesigndoc.txt b/tests/dat/indexdesigndoc.txt
new file mode 100644
index 0000000..0ea3d2b
--- /dev/null
+++ b/tests/dat/indexdesigndoc.txt
@@ -0,0 +1,8 @@
+{
+  "ddoc":"test-query-index",
+  "name": "test-query-index",
+  "type": "json", 
+  "index": {
+    "fields": ["date"]
+  }
+}
diff --git a/tests/dat/searchdesigndoc.txt b/tests/dat/searchdesigndoc.txt
new file mode 100644
index 0000000..833827c
--- /dev/null
+++ b/tests/dat/searchdesigndoc.txt
@@ -0,0 +1,15 @@
+{
+  "_id": "_design/test_design",
+  "views": {
+    "test_view": {
+      "map": "function (doc) {\n  emit(doc._id, 1);\n}"
+    }
+  },
+  "language": "javascript",
+  "indexes": {
+    "test_search": {
+      "analyzer": "standard",
+      "index": "function (doc) {\n  index(\"date\", doc.date);\n}"
+    }
+  }
+}
diff --git a/tests/src/catalog/CloudantUtil.java b/tests/src/catalog/CloudantUtil.java
new file mode 100755
index 0000000..e4fd188
--- /dev/null
+++ b/tests/src/catalog/CloudantUtil.java
@@ -0,0 +1,373 @@
+/*
+ * Copyright 2017 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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 catalog;
+
+import com.cloudant.client.api.CloudantClient;
+import com.cloudant.client.api.Database;
+import com.google.gson.*;
+import com.jayway.restassured.response.Response;
+import common.Pair;
+import common.TestUtils;
+
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.util.Date;
+import java.util.Map;
+import java.util.Properties;
+import java.util.UUID;
+
+import static com.jayway.restassured.RestAssured.given;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * Basic tests of the cloudant trigger function.
+ */
+public class CloudantUtil {
+    public static final String USER_PROPERTY = "user";
+    public static final String PWD_PROPERTY = "password";
+    public static final String DBNAME_PROPERTY = "dbname";
+    public static final String DOC_ID = "testId";
+    public static final File ATTACHMENT_FILE_PATH = getFileRelativeToCloudantHome("tests/dat/attach.txt");
+    public static final File INDEX_DDOC_PATH = getFileRelativeToCloudantHome("tests/dat/indexdesigndoc.txt");
+    public static final File VIEW_AND_SEARCH_DDOC_PATH = getFileRelativeToCloudantHome("tests/dat/searchdesigndoc.txt");
+
+    /**
+     * The root of the Cloudant installation.
+     */
+    private static final String cloudantHome = getCloudantHome();
+    private static final String CLOUDANT_INSTALL_FILE = "installCatalog.sh";
+
+    private static Gson gson = new Gson();
+
+    public static class Credential {
+        public final String user;
+        public final String password;
+        public final String dbname;
+
+        public String host() {
+            return user + ".cloudant.com";
+        }
+
+        public Credential(String user, String password, String dbname) {
+            this.user = user;
+            this.password = password;
+            this.dbname = dbname;
+        }
+
+        public Credential(Properties props) {
+            this(props.getProperty(USER_PROPERTY), props.getProperty(PWD_PROPERTY), props.getProperty(DBNAME_PROPERTY));
+        }
+
+        public static Credential makeFromVCAPFile(String vcapService, String dbNamePrefix) {
+            // Create database name using dbNamePrefix and generated uuid
+            String uniqueSuffix = UUID.randomUUID().toString().replace("-", "");
+            String dbname = dbNamePrefix.toLowerCase() + "-" + uniqueSuffix;
+
+            Map<String,String> credentials = TestUtils.getVCAPcredentials(vcapService);
+            String username = credentials.get("username");
+            String password = credentials.get("password");
+            Properties props = new Properties();
+            props.setProperty(USER_PROPERTY, username);
+            props.setProperty(PWD_PROPERTY, password);
+            props.setProperty(DBNAME_PROPERTY, dbname);
+            return new Credential(props);
+        }
+
+    }
+
+    public static void setUp(Credential credential) throws Exception {
+        deleteTestDatabase(credential);
+        for (int i = 0; i < 5; i++) {
+            try {
+                Pair<Integer, JsonObject> response = CloudantUtil.createTestDatabase(credential, false);
+                if (response.fst == 201)
+                    return;
+                // respond code is sometimes not 201 but still ok
+                // (might be 200 or 202)
+                if (response.snd.has("ok")) {
+                    if (response.snd.get("ok").getAsBoolean())
+                        return;
+                }
+                if (response.snd.has("reason")) {
+                    String reason = response.snd.get("reason").getAsString();
+                    if (reason.contains("exists"))
+                        return;
+                }
+            } catch (Throwable t) {
+                Thread.sleep(1000);
+            }
+        }
+        assertTrue("failed to create database " + credential.dbname, false);
+    }
+
+    public static void unsetUp(Credential credential) throws Exception {
+        deleteTestDatabase(credential);
+    }
+
+    /**
+     * Delete a user-specific Cloudant database.
+     *
+     * @throws UnsupportedEncodingException
+     * @throws InterruptedException
+     */
+    public static JsonObject deleteTestDatabase(Credential credential) throws UnsupportedEncodingException, InterruptedException {
+        return deleteTestDatabase(credential, null);
+    }
+
+    public static JsonObject deleteTestDatabase(Credential credential, String dbName) throws UnsupportedEncodingException, InterruptedException {
+        // Use DELETE to delete the database.
+        // This could fail if the database already exists, but that's ok.
+        Response response = null;
+        String db = (dbName != null && !dbName.isEmpty()) ? dbName : credential.dbname;
+        assertTrue("failed to determine database name", db != null && !db.isEmpty());
+        response = given().port(443).baseUri(cloudantAccount(credential.user)).auth().basic(credential.user, credential.password).when().delete("/" + db);
+        System.out.format("Response of delete database %s: %s\n", db, response.asString());
+        return (JsonObject) new JsonParser().parse(response.asString());
+    }
+
+    /**
+     * Create a user-specific Cloudant database that will be used for this test.
+     *
+     * @throws UnsupportedEncodingException
+     */
+    public static Pair<Integer, JsonObject> createTestDatabase(Credential credential) throws UnsupportedEncodingException {
+        return createTestDatabase(credential, true);
+    }
+
+    private static Pair<Integer, JsonObject> createTestDatabase(Credential credential, boolean failIfCannotCreate) throws UnsupportedEncodingException {
+        // Use PUT to create the database.
+        // This could fail if the database already exists, but that's ok.
+        String dbName = credential.dbname;
+        assertTrue("failed to determine database name", dbName != null && !dbName.isEmpty());
+        Response response = given().port(443).baseUri(cloudantAccount(credential.user)).auth().basic(credential.user, credential.password).when().put("/" + dbName);
+        System.out.format("Response of create database %s: %s\n", dbName, response.asString());
+        if (failIfCannotCreate)
+            assertTrue("failed to create database " + dbName, response.statusCode() == 201 || response.statusCode() == 202);
+        return Pair.make(response.statusCode(), (JsonObject) new JsonParser().parse(response.asString()));
+    }
+
+    /**
+     * read a user-specific Cloudant database to verify database action test
+     * cases.
+     *
+     * @throws UnsupportedEncodingException
+     */
+    public static Response readTestDatabase(Credential credential) {
+        return readTestDatabase(credential, null);
+    }
+
+    public static Response readTestDatabase(Credential credential, String dbName) {
+        try {
+            Response response = null;
+            String db = (dbName != null && !dbName.isEmpty()) ? dbName : credential.dbname;
+            response = given().port(443).baseUri(cloudantAccount(credential.user)).auth().basic(credential.user, credential.password).when().get("/" + db);
+            System.out.format("Response of HTTP GET for database %s: %s\n", credential.dbname, response.asString());
+            return response;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    /**
+     * create a document in the cloudant database
+     *
+     * @throws UnsupportedEncodingException
+     */
+    public static JsonObject createDocument(Credential credential, String jsonObj) throws UnsupportedEncodingException {
+        JsonObject obj = new JsonParser().parse(jsonObj).getAsJsonObject();
+
+        CloudantClient client = new CloudantClient(credential.user, credential.user, credential.password);
+        Database db = client.database(credential.dbname, false);
+        com.cloudant.client.api.model.Response res = db.post(obj);
+        client.shutdown();
+
+        JsonObject ret = new JsonObject();
+        ret.addProperty("ok", true);
+        ret.addProperty("id", res.getId());
+        ret.addProperty("rev", res.getRev());
+        return ret;
+    }
+
+    /**
+     * get a document from the cloudant database
+     *
+     * @throws UnsupportedEncodingException
+     */
+    public static JsonObject getDocument(Credential credential, String docId) throws UnsupportedEncodingException {
+        // use GET to get the document
+        Response response = given().port(443).baseUri(cloudantAccount(credential.user)).auth().basic(credential.user, credential.password).get("/" + credential.dbname + "/" + docId);
+        String responseStr = response.asString();
+        if (responseStr.length() > 500)
+            responseStr = responseStr.substring(0, 500);
+        System.out.format("Response of get document from database %s: %s\n", credential.dbname, responseStr);
+        return (JsonObject) new JsonParser().parse(response.asString());
+    }
+
+    /**
+     * delete a document from the cloudant database
+     *
+     * @throws UnsupportedEncodingException
+     */
+    public static JsonObject deleteDocument(Credential credential, String docId) throws UnsupportedEncodingException {
+        // use GET to get the document
+        Response response = given().port(443).baseUri(cloudantAccount(credential.user)).auth().basic(credential.user, credential.password).delete("/" + credential.dbname + "/" + docId);
+        System.out.format("Response of delete document in database %s: %s\n", credential.dbname, response.asString());
+        assertTrue("failed to delete document in database " + credential.dbname, response.statusCode() == 200 || response.statusCode() == 202);
+        return (JsonObject) new JsonParser().parse(response.asString());
+    }
+
+    /**
+     * Read bulk documents from the cloudant database
+     *
+     * @throws UnsupportedEncodingException
+     */
+    public static JsonArray bulkDocuments(Credential credential, JsonArray bulkDocs) throws UnsupportedEncodingException {
+        JsonObject docs = new JsonObject();
+        docs.add("docs", bulkDocs);
+        // use GET to get the document
+        String dbname = credential.dbname;
+        Response response = given().port(443).baseUri(cloudantAccount(credential.user)).auth().basic(credential.user, credential.password).contentType("application/json").body(docs).post("/" + credential.dbname + "/_bulk_docs?include_docs=true");
+        String responseStr = response.asString();
+        if (responseStr.length() > 500)
+            responseStr = responseStr.substring(0, 500);
+        System.out.format("Response of get document from database %s: %s\n", dbname, responseStr);
+        return (JsonArray) new JsonParser().parse(response.asString());
+    }
+
+    public static JsonObject createDocParameterForWhisk() {
+        return createDocParameterForWhisk(null);
+    }
+
+    public static JsonObject createDocParameterForWhisk(String doc) {
+        JsonObject cloudantDoc = new JsonObject();
+        String now = new Date().toString();
+        cloudantDoc.addProperty("_id", DOC_ID);
+        cloudantDoc.addProperty("date", now);
+        // Create JSON object that will be passed as an argument to whisk cli
+        JsonObject param = new JsonObject();
+        if (doc != null && !doc.isEmpty()) {
+            param.addProperty("doc", doc);
+        } else {
+            param.addProperty("doc", cloudantDoc.toString());
+        }
+        return param;
+    }
+
+    public static JsonArray createDocumentArray(int numDocs) {
+        // Array of docs for bulk
+        JsonArray bulkDocs = new JsonArray();
+        for (int i = 1; i <= numDocs; i++) {
+            JsonObject cloudantDoc = new JsonObject();
+            String now = new Date().toString();
+            cloudantDoc.addProperty("_id", CloudantUtil.DOC_ID + i);
+            cloudantDoc.addProperty("date", now);
+            bulkDocs.add(cloudantDoc);
+        }
+        return bulkDocs;
+    }
+
+    /**
+     * Only keep _id and _rev for each document in the JSON array.
+     */
+    public static JsonArray updateDocsWithOnlyIdAndRev(JsonArray docs) {
+        for (int i = 0; i < docs.size(); i++) {
+            JsonElement id = docs.get(i).getAsJsonObject().get("id");
+            JsonElement rev = docs.get(i).getAsJsonObject().get("rev");
+            docs.get(i).getAsJsonObject().add("_id", id);
+            docs.get(i).getAsJsonObject().add("_rev", rev);
+        }
+        return docs;
+    }
+
+    public static JsonArray addDeletedPropertyToDocs(JsonArray docs) {
+        for (int i = 0; i < docs.size(); i++) {
+            JsonElement id = docs.get(i).getAsJsonObject().get("id");
+            JsonElement rev = docs.get(i).getAsJsonObject().get("rev");
+            docs.get(i).getAsJsonObject().add("_id", id);
+            docs.get(i).getAsJsonObject().add("_rev", rev);
+            docs.get(i).getAsJsonObject().addProperty("_deleted", true);
+        }
+        return docs;
+    }
+
+    public static JsonObject createDesignFromFile(File jsonFile) throws JsonSyntaxException, IOException {
+        return gson.fromJson(readFile(jsonFile), JsonObject.class);
+    }
+
+    public static String readFile(File jsonFile) throws IOException {
+        return new String(Files.readAllBytes(jsonFile.toPath()), StandardCharsets.UTF_8);
+    }
+
+    /**
+     * Create an index in the cloudant database
+     *
+     * @throws UnsupportedEncodingException
+     */
+    public static JsonObject createIndex(Credential credential, String jsonObj) throws UnsupportedEncodingException {
+        Response response = given().port(443).baseUri(cloudantAccount(credential.user)).auth().basic(credential.user, credential.password).contentType("application/json").body(jsonObj).when().post("/" + credential.dbname + "/_index");
+        System.out.format("Response of create document in database %s: %s\n", credential.dbname, response.asString());
+        assertTrue("failed to create index in database " + credential.dbname, response.statusCode() == 200);
+        return (JsonObject) new JsonParser().parse(response.asString());
+    }
+
+    /**
+     * Create a document with attachment in a cloudant database
+     *
+     * @throws UnsupportedEncodingException
+     * @throws FileNotFoundException
+     */
+    public static com.cloudant.client.api.model.Response createDocumentWithAttachment(Credential credential, File attachmentFilePath) throws UnsupportedEncodingException, FileNotFoundException {
+        InputStream attachStream = new FileInputStream(attachmentFilePath);
+        String contentType = "text/plain";
+
+        CloudantClient client = new CloudantClient(credential.user, credential.user, credential.password);
+        Database db = client.database(credential.dbname, false);
+        return db.saveAttachment(attachStream, attachmentFilePath.getName(), contentType);
+    }
+
+    private static String cloudantAccount(String user) {
+        return "https://" + user + ".cloudant.com";
+    }
+
+    public static File getFileRelativeToCloudantHome(String name) {
+        return new File(cloudantHome, name);
+    }
+
+    private static String getCloudantHome() {
+        String dir = System.getProperty("user.dir");
+
+        if (dir != null) {
+            // Look in the directory tree recursively.
+            File propfile = findFileRecursively(dir, CLOUDANT_INSTALL_FILE);
+            return propfile != null ? propfile.getParent() : null;
+        } else return null;
+    }
+
+    private static File findFileRecursively(String dir, String needle) {
+        if (dir != null) {
+            File base = new File(dir);
+            File file = new File(base, needle);
+            if (file.exists()) {
+                return file;
+            } else {
+                return findFileRecursively(base.getParent(), needle);
+            }
+        } else return null;
+    }
+
+}
diff --git a/tests/src/catalog/cloudant/CloudantBindingTests.scala b/tests/src/catalog/cloudant/CloudantBindingTests.scala
new file mode 100644
index 0000000..812de49
--- /dev/null
+++ b/tests/src/catalog/cloudant/CloudantBindingTests.scala
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2017 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * 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 catalog.cloudant
+
+import org.junit.runner.RunWith
+import org.scalatest.FlatSpec
+import org.scalatest.junit.JUnitRunner
+
+import catalog.CloudantUtil
+import common.JsHelpers
+import common.TestHelpers
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json.DefaultJsonProtocol.StringJsonFormat
+import spray.json.JsObject
+import spray.json.pimpAny
+
+@RunWith(classOf[JUnitRunner])
+class CloudantBindingTests extends FlatSpec
+    with TestHelpers
+    with WskTestHelpers
+    with JsHelpers {
+
+    val wskprops = WskProps()
+    val wsk = new Wsk
+
+    val myCloudantCreds = CloudantUtil.Credential.makeFromVCAPFile("cloudantNoSQLDB", this.getClass.getSimpleName)
+
+    behavior of "Cloudant binding"
+
+    /**
+     * Simulate bluemix package binding by supplying a "url" parameter.
+     * Additionally, leave out other key parameters (e.g. username, password)
+     * to ensure that the action uses the "url" bound parameter.
+     */
+    it should """Use "url" property if it is available""" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            implicit val wskprops = wp // shadow global props and make implicit
+            val packageName = "cloudantBindingWithURL"
+
+            try {
+                CloudantUtil.setUp(myCloudantCreds)
+
+                val packageGetResult = wsk.pkg.get("/whisk.system/cloudant")
+                println("Fetching cloudant package.")
+                packageGetResult.stdout should include("ok")
+
+                println("""Creating cloudant package binding with only a "url" parameter.""")
+                assetHelper.withCleaner(wsk.pkg, packageName) {
+                    (pkg, name) =>
+                        pkg.bind("/whisk.system/cloudant", name,
+                            Map("url" -> s"https://${myCloudantCreds.user}:${myCloudantCreds.password}@${myCloudantCreds.host}".toJson))
+                }
+
+                println("Invoking the document-create action.")
+                withActivation(wsk.activation, wsk.action.invoke(s"${packageName}/create-document",
+                    Map(
+                        "dbname" -> myCloudantCreds.dbname.toJson,
+                        "doc" -> JsObject("message" -> "I used the url parameter.".toJson)))) {
+                    activation =>
+                        activation.response.success shouldBe true
+                        activation.response.result.get.fields.get("id") shouldBe defined
+                }
+            } finally {
+                CloudantUtil.unsetUp(myCloudantCreds)
+            }
+    }
+
+    /**
+     * Simulate a user creating their own binding with "username", "password", and "host".
+     * Do not include "url" in the binding to ensure the package uses the other bound properties.
+     */
+    it should """Use "username", "password", and "host" if "url" is not available""" in withAssetCleaner(wskprops) {
+        (wp, assetHelper) =>
+            implicit val wskprops = wp // shadow global props and make implicit
+            val packageName = "cloudantBindingWithoutURL"
+
+            try {
+                CloudantUtil.setUp(myCloudantCreds)
+
+                val packageGetResult = wsk.pkg.get("/whisk.system/cloudant")
+                println("Fetching cloudant package.")
+                packageGetResult.stdout should include("ok")
+
+                println("""Creating cloudant package binding with "username", "pasword" and "host".""")
+                assetHelper.withCleaner(wsk.pkg, packageName) {
+                    (pkg, name) =>
+                        pkg.bind("/whisk.system/cloudant", name,
+                            Map("username" -> myCloudantCreds.user.toJson,
+                                "password" -> myCloudantCreds.password.toJson,
+                                "host" -> myCloudantCreds.host().toJson))
+                }
+
+                println("Invoking the document-create action.")
+                withActivation(wsk.activation, wsk.action.invoke(s"${packageName}/create-document",
+                    Map(
+                        "dbname" -> myCloudantCreds.dbname.toJson,
+                        "doc" -> JsObject("message" -> "This time I didn't use the URL param.".toJson)))) {
+                    activation =>
+                        activation.response.success shouldBe true
+                        activation.response.result.get.fields.get("id") shouldBe defined
+                }
+            } finally {
+                CloudantUtil.unsetUp(myCloudantCreds)
+            }
+    }
+}