You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@openwhisk.apache.org by cs...@apache.org on 2017/06/26 15:59:41 UTC
[incubator-openwhisk-cli] 01/36: Clean up test project (#1960)
This is an automated email from the ASF dual-hosted git repository.
csantanapr pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk-cli.git
commit 9f09096661f16557a3aebf82bffb475b5ae984b4
Author: Markus Thömmes <ma...@me.com>
AuthorDate: Tue Mar 7 15:35:29 2017 +0100
Clean up test project (#1960)
- Remove unused dependencies
- Define dependencies as compile time to be able to include them from other projects
- Standardize layout
* Bumping restassured version
* Update gitignore to include .cache-tests
---
.../apigw/healthtests/ApiGwEndToEndTests.scala | 120 ++
.../src/test/scala/system/basic/CLIJavaTests.scala | 105 ++
.../test/scala/system/basic/CLIPythonTests.scala | 108 ++
.../src/test/scala/system/basic/ConsoleTests.scala | 98 ++
.../src/test/scala/system/basic/PackageTests.scala | 146 +++
.../system/basic/Swift3WhiskObjectTests.scala | 99 ++
.../test/scala/system/basic/WskActionTests.scala | 293 +++++
.../test/scala/system/basic/WskBasicTests.scala | 739 ++++++++++++
.../src/test/scala/system/basic/WskRuleTests.scala | 360 ++++++
.../src/test/scala/system/basic/WskSdkTests.scala | 110 ++
.../test/scala/system/basic/WskSequenceTests.scala | 528 +++++++++
.../scala/whisk/core/admin/WskAdminTests.scala | 85 ++
.../actions/test/ApiGwRoutemgmtActionTests.scala | 332 ++++++
.../scala/whisk/core/cli/test/ApiGwTests.scala | 511 ++++++++
.../core/cli/test/WskActionSequenceTests.scala | 85 ++
.../whisk/core/cli/test/WskBasicUsageTests.scala | 1245 ++++++++++++++++++++
.../whisk/core/cli/test/WskEntitlementTests.scala | 376 ++++++
.../whisk/core/cli/test/WskWebActionsTests.scala | 117 ++
18 files changed, 5457 insertions(+)
diff --git a/tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala b/tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala
new file mode 100644
index 0000000..1833d43
--- /dev/null
+++ b/tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2015-2016 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 apigw.healthtests
+
+import java.io.BufferedWriter
+import java.io.File
+import java.io.FileWriter
+
+import scala.concurrent.duration.DurationInt
+
+import org.junit.runner.RunWith
+import org.scalatest.FlatSpec
+import org.scalatest.Matchers
+import org.scalatest.junit.JUnitRunner
+
+import com.jayway.restassured.RestAssured
+
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils._
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import system.rest.RestUtil
+
+/**
+ * Basic tests of the download link for Go CLI binaries
+ */
+@RunWith(classOf[JUnitRunner])
+class ApiGwEndToEndTests extends FlatSpec with Matchers with RestUtil with TestHelpers with WskTestHelpers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val (cliuser, clinamespace) = WskAdmin.getUser(wskprops.authKey)
+
+ it should s"create an API and successfully invoke that API" in {
+ val testName = "APIGW_HEALTHTEST1"
+ val testbasepath = "/" + testName + "_bp"
+ val testrelpath = "/path"
+ val testurlop = "get"
+ val testapiname = testName + " API Name"
+ val actionName = "echo"
+ val urlqueryparam = "name"
+ val urlqueryvalue = "test"
+
+ try {
+ println("cli user: " + cliuser + "; cli namespace: " + clinamespace)
+
+ // Create the action for the API
+ val file = TestUtils.getTestActionFilename(s"echo.js")
+ wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT)
+
+ // Create the API
+ var rr = wsk.api.create(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ val apiurl = rr.stdout.split("\n")(1)
+ println(s"apiurl: '${apiurl}'")
+
+ // Validate the API was successfully created
+ // List result will look like:
+ // ok: APIs
+ // Action Verb API Name URL
+ // /_//whisk.system/utils/echo get APIGW_HEALTHTEST1 API Name http://172.17.0.1:9001/api/ab9082cd-ea8e-465a-8a65-b491725cc4ef/APIGW_HEALTHTEST1_bp/path
+ rr = wsk.api.list(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+
+ // Recreate the API using a JSON swagger file
+ rr = wsk.api.get(basepathOrApiName = Some(testbasepath))
+ val swaggerfile = File.createTempFile("api", ".json")
+ swaggerfile.deleteOnExit()
+ val bw = new BufferedWriter(new FileWriter(swaggerfile))
+ bw.write(rr.stdout)
+ bw.close()
+
+ // Delete API to that it can be recreated again using the generated swagger file
+ val deleteApiResult = wsk.api.delete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+
+ // Create the API again, but use the swagger file this time
+ rr = wsk.api.create(swagger = Some(swaggerfile.getAbsolutePath()))
+ rr.stdout should include("ok: created API")
+ val swaggerapiurl = rr.stdout.split("\n")(1)
+ println(s"apiurl: '${swaggerapiurl}'")
+
+ // Call the API URL and validate the results
+ val response = whisk.utils.retry({
+ val response = RestAssured.given().config(sslconfig).get(s"$swaggerapiurl?$urlqueryparam=$urlqueryvalue")
+ response.statusCode should be(200)
+ response
+ }, 5, Some(1.second))
+ val responseString = response.body.asString
+ println("URL invocation response: " + responseString)
+ responseString.parseJson.asJsObject.fields(urlqueryparam).convertTo[String] should be(urlqueryvalue)
+
+ } finally {
+ println("Deleting action: " + actionName)
+ val finallydeleteActionResult = wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT)
+ println("Deleting API: " + testbasepath)
+ val finallydeleteApiResult = wsk.api.delete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+}
diff --git a/tests/src/test/scala/system/basic/CLIJavaTests.scala b/tests/src/test/scala/system/basic/CLIJavaTests.scala
new file mode 100644
index 0000000..1f7ccef
--- /dev/null
+++ b/tests/src/test/scala/system/basic/CLIJavaTests.scala
@@ -0,0 +1,105 @@
+/*
+ * Copyright 2015-2016 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 system.basic
+
+import scala.concurrent.duration.DurationInt
+
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils.ANY_ERROR_EXIT
+import common.WskTestHelpers
+import common.WskProps
+import common.Wsk
+
+import org.junit.runner.RunWith
+import org.scalatest.Matchers
+import org.scalatest.junit.JUnitRunner
+
+import spray.json.JsString
+
+@RunWith(classOf[JUnitRunner])
+class CLIJavaTests
+ extends TestHelpers
+ with WskTestHelpers
+ with Matchers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val expectedDuration = 120.seconds
+ val activationPollDuration = 60.seconds
+
+ behavior of "Java Actions"
+
+ /**
+ * Test the Java "hello world" demo sequence
+ */
+ it should "Invoke a java action" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "helloJava"
+ val file = Some(TestUtils.getTestActionFilename("helloJava.jar"))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, file, main = Some("hello.HelloJava"))
+ }
+
+ val start = System.currentTimeMillis()
+ withActivation(wsk.activation, wsk.action.invoke(name), totalWait = activationPollDuration) {
+ _.response.result.get.toString should include("Hello stranger!")
+ }
+
+ withActivation(wsk.activation, wsk.action.invoke(name, Map("name" -> JsString("Sir"))), totalWait = activationPollDuration) {
+ _.response.result.get.toString should include("Hello Sir!")
+ }
+
+ withClue("Test duration exceeds expectation (ms)") {
+ val duration = System.currentTimeMillis() - start
+ duration should be <= expectedDuration.toMillis
+ }
+ }
+
+ /*
+ * Example from the docs.
+ */
+ it should "Invoke a Java action where main is in the default package" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "helloJavaDefaultPkg"
+ val file = Some(TestUtils.getTestActionFilename("helloJavaDefaultPackage.jar"))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, file, main = Some("Hello"))
+ }
+
+ withActivation(wsk.activation, wsk.action.invoke(name, Map()), totalWait = activationPollDuration) {
+ _.response.result.get.toString should include("Hello stranger!")
+ }
+ }
+
+ it should "Ensure that Java actions cannot be created without a specified main method" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "helloJavaWithNoMainSpecified"
+ val file = Some(TestUtils.getTestActionFilename("helloJava.jar"))
+
+ val createResult = assetHelper.withCleaner(wsk.action, name, confirmDelete = false) {
+ (action, _) =>
+ action.create(name, file, expectedExitCode = ANY_ERROR_EXIT)
+ }
+
+ val output = s"${createResult.stdout}\n${createResult.stderr}"
+
+ output should include("main")
+ }
+}
diff --git a/tests/src/test/scala/system/basic/CLIPythonTests.scala b/tests/src/test/scala/system/basic/CLIPythonTests.scala
new file mode 100644
index 0000000..ff7ea82
--- /dev/null
+++ b/tests/src/test/scala/system/basic/CLIPythonTests.scala
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2015-2016 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 system.basic;
+
+import org.junit.runner.RunWith
+import org.scalatest.Matchers
+import org.scalatest.junit.JUnitRunner
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import spray.json._
+import spray.json.DefaultJsonProtocol.StringJsonFormat
+import common.TestHelpers
+import common.WskTestHelpers
+import common.WskProps
+import common.WhiskProperties
+
+@RunWith(classOf[JUnitRunner])
+class CLIPythonTests
+ extends TestHelpers
+ with WskTestHelpers
+ with Matchers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+
+ behavior of "Native Python Action"
+
+ it should "invoke an action and get the result" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "basicInvoke"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("hello.py")))
+ }
+
+ withActivation(wsk.activation, wsk.action.invoke(name, Map("name" -> "Prince".toJson))) {
+ _.response.result.get.toString should include("Prince")
+ }
+ }
+
+ it should "invoke an action with a non-default entry point" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "nonDefaultEntryPoint"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("niam.py")), main = Some("niam"))
+ }
+
+ withActivation(wsk.activation, wsk.action.invoke(name, Map())) {
+ _.response.result.get.fields.get("greetings") should be(Some(JsString("Hello from a non-standard entrypoint.")))
+ }
+ }
+
+ it should "invoke an action from a zip file with a non-default entry point" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "pythonZipWithNonDefaultEntryPoint"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("python.zip")), main = Some("niam"), kind = Some("python"))
+ }
+
+ withActivation(wsk.activation, wsk.action.invoke(name, Map("name" -> "Prince".toJson))) {
+ _.response.result.get shouldBe JsObject("greeting" -> JsString("Hello Prince!"))
+ }
+ }
+
+ it should "invoke an action and confirm expected environment is defined" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "stdenv"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("stdenv.py")))
+ }
+
+ withActivation(wsk.activation, wsk.action.invoke(name)) {
+ activation =>
+ val result = activation.response.result.get
+ result.fields.get("error") shouldBe empty
+ result.fields.get("auth") shouldBe Some(JsString(WhiskProperties.readAuthKey(WhiskProperties.getAuthFileForTesting)))
+ result.fields.get("edge").toString.trim should not be empty
+ }
+ }
+
+ it should "invoke an invalid action and get error back" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "bad code"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("malformed.py")))
+ }
+
+ withActivation(wsk.activation, wsk.action.invoke(name)) {
+ activation =>
+ activation.response.result.get.fields.get("error") shouldBe Some(JsString("The action failed to generate or locate a binary. See logs for details."))
+ activation.logs.get.mkString("\n") should { not include ("pythonaction.py") and not include ("flask") }
+ }
+ }
+}
diff --git a/tests/src/test/scala/system/basic/ConsoleTests.scala b/tests/src/test/scala/system/basic/ConsoleTests.scala
new file mode 100644
index 0000000..3561e34
--- /dev/null
+++ b/tests/src/test/scala/system/basic/ConsoleTests.scala
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2015-2016 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 system.basic;
+
+import java.time.Clock
+import java.time.Instant
+
+import scala.concurrent.duration.Duration
+import scala.concurrent.duration.DurationInt
+import scala.concurrent.duration.MILLISECONDS
+import scala.language.postfixOps
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json.DefaultJsonProtocol.IntJsonFormat
+import spray.json.DefaultJsonProtocol.StringJsonFormat
+import spray.json.pimpAny
+
+/**
+ * Tests of the text console
+ */
+@RunWith(classOf[JUnitRunner])
+class ConsoleTests
+ extends TestHelpers
+ with WskTestHelpers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val guestNamespace = wskprops.namespace
+
+ behavior of "Wsk Activation Console"
+
+ it should "show an activation log message for hello world" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val packageName = "samples"
+ val actionName = "helloWorld"
+ val fullActionName = s"/$guestNamespace/$packageName/$actionName"
+ assetHelper.withCleaner(wsk.pkg, packageName) {
+ (pkg, _) => pkg.create(packageName, shared = Some(true))
+ }
+
+ assetHelper.withCleaner(wsk.action, fullActionName) {
+ (action, _) => action.create(fullActionName, Some(TestUtils.getTestActionFilename("hello.js")))
+ }
+
+ val duration = Some(30 seconds)
+ val payload = new String("from the console!".getBytes, "UTF-8")
+ val run = wsk.action.invoke(fullActionName, Map("payload" -> payload.toJson))
+ withActivation(wsk.activation, run, totalWait = duration.get) {
+ activation =>
+ val console = wsk.activation.console(10 seconds, since = duration)
+ println(console.stdout)
+ console.stdout should include(payload)
+ }
+ }
+
+ it should "show repeated activations" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "countdown"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("countdown.js")))
+ }
+
+ val start = Instant.now(Clock.systemUTC())
+ val run = wsk.action.invoke(name, Map("n" -> 3.toJson))
+ withActivation(wsk.activation, run) {
+ activation =>
+ val activations = wsk.activation.pollFor(N = 4, Some(name), since = Some(start), retries = 80).length
+ withClue(s"expected activations:") {
+ activations should be(4)
+ }
+ val duration = Duration(Instant.now(Clock.systemUTC()).toEpochMilli - start.toEpochMilli, MILLISECONDS)
+ val console = wsk.activation.console(10 seconds, since = Some(duration))
+ console.stdout should include("Happy New Year")
+ }
+ }
+
+}
diff --git a/tests/src/test/scala/system/basic/PackageTests.scala b/tests/src/test/scala/system/basic/PackageTests.scala
new file mode 100644
index 0000000..da4026f
--- /dev/null
+++ b/tests/src/test/scala/system/basic/PackageTests.scala
@@ -0,0 +1,146 @@
+/*
+ * Copyright 2015-2016 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 system.basic
+
+import java.util.Date
+import scala.language.postfixOps
+import scala.collection.mutable.HashMap
+import scala.concurrent.duration.DurationInt
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import spray.json._
+import spray.json.DefaultJsonProtocol.StringJsonFormat
+import common.TestHelpers
+import common.WskTestHelpers
+import common.TestHelpers
+import common.WskProps
+
+@RunWith(classOf[JUnitRunner])
+class PackageTests
+ extends TestHelpers
+ with WskTestHelpers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val LOG_DELAY = 80 seconds
+
+ behavior of "Wsk Package"
+
+ it should "confirm wsk exists" in {
+ Wsk.exists
+ }
+
+ it should "allow creation and deletion of a package" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "simplepackage"
+ assetHelper.withCleaner(wsk.pkg, name) {
+ (pkg, _) => pkg.create(name, Map())
+ }
+ }
+
+ val params1 = Map("p1" -> "v1".toJson, "p2" -> "".toJson)
+ val params2 = Map("p1" -> "v1".toJson, "p2" -> "v2".toJson, "p3" -> "v3".toJson)
+
+ it should "allow creation of a package with parameters" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "simplepackagewithparams"
+ assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>
+ pkg.create(name, params1)
+ }
+ }
+
+ it should "allow updating a package" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "simplepackagetoupdate"
+ assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>
+ pkg.create(name, params1)
+ pkg.create(name, params2, update = true)
+ }
+ }
+
+ it should "allow binding of a package" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "simplepackagetobind"
+ val bindName = "simplebind"
+ assetHelper.withCleaner(wsk.pkg, name) { (pkg, _) =>
+ pkg.create(name, params1)
+ }
+ assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>
+ pkg.bind(name, bindName, params2)
+ }
+ }
+
+ it should "perform package binds so parameters are inherited" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val packageName = "package1"
+ val bindName = "package2"
+ val actionName = "print"
+ val packageActionName = packageName + "/" + actionName
+ val bindActionName = bindName + "/" + actionName
+ val packageParams = Map("key1a" -> "value1a".toJson, "key1b" -> "value1b".toJson)
+ val bindParams = Map("key2a" -> "value2a".toJson, "key1b" -> "value2b".toJson)
+ val actionParams = Map("key0" -> "value0".toJson)
+ val file = TestUtils.getTestActionFilename("printParams.js")
+ assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>
+ pkg.create(packageName, packageParams)
+ }
+ assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>
+ action.create(packageActionName, Some(file), parameters = actionParams)
+ }
+ assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>
+ pkg.bind(packageName, bindName, bindParams)
+ }
+
+ // Check that the description of packages and actions includes all the inherited parameters.
+ val packageDescription = wsk.pkg.get(packageName).stdout
+ val bindDescription = wsk.pkg.get(bindName).stdout
+ val packageActionDescription = wsk.action.get(packageActionName).stdout
+ val bindActionDescription = wsk.action.get(bindActionName).stdout
+ checkForParameters(packageDescription, packageParams)
+ checkForParameters(bindDescription, packageParams, bindParams)
+ checkForParameters(packageActionDescription, packageParams, actionParams)
+ checkForParameters(bindActionDescription, packageParams, bindParams, actionParams)
+
+ // Check that inherited parameters are passed to the action.
+ val now = new Date().toString()
+ val run = wsk.action.invoke(bindActionName, Map("payload" -> now.toJson))
+ withActivation(wsk.activation, run, totalWait = LOG_DELAY) {
+ _.logs.get.mkString(" ") should include regex (
+ String.format(".*key0: value0.*key1a: value1a.*key1b: value2b.*key2a: value2a.*payload: %s", now))
+ }
+ }
+
+ /**
+ * Check that a description of an item includes the specified parameters.
+ * Parameters keys in later parameter maps override earlier ones.
+ */
+ def checkForParameters(itemDescription: String, paramSets: Map[String, JsValue]*) {
+ // Merge and the parameters handling overrides.
+ val merged = HashMap.empty[String, JsValue]
+ paramSets.foreach { merged ++= _ }
+ val flatDescription = itemDescription.replace("\n", "").replace("\r", "")
+ merged.foreach {
+ case (key: String, value: JsValue) =>
+ val toFind = s""""key": "${key}",.*"value": ${value.toString}"""
+ flatDescription should include regex toFind
+ }
+ }
+
+}
diff --git a/tests/src/test/scala/system/basic/Swift3WhiskObjectTests.scala b/tests/src/test/scala/system/basic/Swift3WhiskObjectTests.scala
new file mode 100644
index 0000000..c04c516
--- /dev/null
+++ b/tests/src/test/scala/system/basic/Swift3WhiskObjectTests.scala
@@ -0,0 +1,99 @@
+/*
+ * Copyright 2015-2016 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 system.basic
+
+import scala.concurrent.duration.DurationInt
+import scala.language.postfixOps
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json.DefaultJsonProtocol.StringJsonFormat
+import spray.json.pimpAny
+
+@RunWith(classOf[JUnitRunner])
+class Swift3WhiskObjectTests
+ extends TestHelpers
+ with WskTestHelpers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+
+ behavior of "Swift 3 Whisk backend API"
+
+ it should "allow Swift actions to invoke other actions" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ // use CLI to create action from dat/actions/invokeAction.swift
+ val file = TestUtils.getTestActionFilename("invoke.swift")
+ val actionName = "invokeAction"
+ assetHelper.withCleaner(wsk.action, actionName) {
+ (action, _) => action.create(name = actionName, artifact = Some(file), kind = Some("swift:3"))
+ }
+
+ // invoke the action
+ val run = wsk.action.invoke(actionName)
+ withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) {
+ activation =>
+ // should be successful
+ activation.response.success shouldBe true
+
+ // should have a field named "activationId" which is the date action's activationId
+ activation.response.result.get.fields("activationId").toString.length should be >= 32
+
+ // check for "date" field that comes from invoking the date action
+ //activation.response.result.get.fieldPathExists("response", "result", "date") should be(true)
+ }
+ }
+
+ it should "allow Swift actions to trigger events" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ // create a trigger
+ val triggerName = s"TestTrigger ${System.currentTimeMillis()}"
+ assetHelper.withCleaner(wsk.trigger, triggerName) {
+ (trigger, _) => trigger.create(triggerName)
+ }
+
+ // create an action that fires the trigger
+ val file = TestUtils.getTestActionFilename("trigger.swift")
+ val actionName = "ActionThatTriggers"
+ assetHelper.withCleaner(wsk.action, actionName) {
+ (action, _) => action.create(name = actionName, artifact = Some(file), kind = Some("swift:3"))
+ }
+
+ // invoke the action
+ val run = wsk.action.invoke(actionName, Map("triggerName" -> triggerName.toJson))
+ withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 60 seconds) {
+ activation =>
+ // should be successful
+ activation.response.success shouldBe true
+
+ // should have a field named "activationId" which is the date action's activationId
+ activation.response.result.get.fields("activationId").toString.length should be >= 32
+
+ // should result in an activation for triggerName
+ val triggerActivations = wsk.activation.pollFor(1, Some(triggerName), retries = 20)
+ withClue(s"trigger activations for $triggerName:") {
+ triggerActivations.length should be(1)
+ }
+ }
+ }
+}
diff --git a/tests/src/test/scala/system/basic/WskActionTests.scala b/tests/src/test/scala/system/basic/WskActionTests.scala
new file mode 100644
index 0000000..dcb6dcd
--- /dev/null
+++ b/tests/src/test/scala/system/basic/WskActionTests.scala
@@ -0,0 +1,293 @@
+/*
+ * Copyright 2015-2016 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 system.basic
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.JsHelpers
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import spray.json.JsObject
+import spray.json.pimpAny
+
+@RunWith(classOf[JUnitRunner])
+class WskActionTests
+ extends TestHelpers
+ with WskTestHelpers
+ with JsHelpers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+
+ val testString = "this is a test"
+ val testResult = JsObject("count" -> testString.split(" ").length.toJson)
+ val guestNamespace = wskprops.namespace
+
+ behavior of "Whisk actions"
+
+ it should "invoke an action returning a promise" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "hello promise"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloPromise.js")))
+ }
+
+ val run = wsk.action.invoke(name)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe "success"
+ activation.response.result shouldBe Some(JsObject("done" -> true.toJson))
+ activation.logs.get.mkString(" ") shouldBe empty
+ }
+ }
+
+ it should "invoke an action with a space in the name" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "hello Async"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloAsync.js")))
+ }
+
+ val run = wsk.action.invoke(name, Map("payload" -> testString.toJson))
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe "success"
+ activation.response.result shouldBe Some(testResult)
+ activation.logs.get.mkString(" ") should include(testString)
+ }
+ }
+
+ it should "pass parameters bound on creation-time to the action" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "printParams"
+ val params = Map(
+ "param1" -> "test1",
+ "param2" -> "test2")
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(
+ name,
+ Some(TestUtils.getTestActionFilename("printParams.js")),
+ parameters = params.mapValues(_.toJson))
+ }
+
+ val invokeParams = Map("payload" -> testString)
+ val run = wsk.action.invoke(name, invokeParams.mapValues(_.toJson))
+ withActivation(wsk.activation, run) {
+ activation =>
+ val logs = activation.logs.get.mkString(" ")
+
+ (params ++ invokeParams).foreach {
+ case (key, value) =>
+ logs should include(s"params.$key: $value")
+ }
+ }
+ }
+
+ it should "copy an action and invoke it successfully" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "copied"
+ val packageName = "samples"
+ val actionName = "wordcount"
+ val fullQualifiedName = s"/$guestNamespace/$packageName/$actionName"
+
+ assetHelper.withCleaner(wsk.pkg, packageName) {
+ (pkg, _) => pkg.create(packageName, shared = Some(true))
+ }
+
+ assetHelper.withCleaner(wsk.action, fullQualifiedName) {
+ val file = Some(TestUtils.getTestActionFilename("wc.js"))
+ (action, _) => action.create(fullQualifiedName, file)
+ }
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(fullQualifiedName), Some("copy"))
+ }
+
+ val run = wsk.action.invoke(name, Map("payload" -> testString.toJson))
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe "success"
+ activation.response.result shouldBe Some(testResult)
+ activation.logs.get.mkString(" ") should include(testString)
+ }
+ }
+
+ it should "copy an action and ensure exec, parameters, and annotations copied" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val origActionName = "orignAction"
+ val copiedActionName = "copiedAction"
+ val params = Map("a" -> "A".toJson)
+ val annots = Map("b" -> "B".toJson)
+
+ assetHelper.withCleaner(wsk.action, origActionName) {
+ val file = Some(TestUtils.getTestActionFilename("wc.js"))
+ (action, _) => action.create(origActionName, file, parameters = params, annotations = annots)
+ }
+
+ assetHelper.withCleaner(wsk.action, copiedActionName) {
+ (action, _) => action.create(copiedActionName, Some(origActionName), Some("copy"))
+ }
+
+ val copiedAction = getJSONFromCLIResponse(wsk.action.get(copiedActionName).stdout)
+ val origAction = getJSONFromCLIResponse(wsk.action.get(copiedActionName).stdout)
+
+ copiedAction.fields("annotations") shouldBe origAction.fields("annotations")
+ copiedAction.fields("parameters") shouldBe origAction.fields("parameters")
+ copiedAction.fields("exec") shouldBe origAction.fields("exec")
+ copiedAction.fields("version") shouldBe JsString("0.0.1")
+ }
+
+ it should "recreate and invoke a new action with different code" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "recreatedAction"
+ assetHelper.withCleaner(wsk.action, name, false) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("wc.js")))
+ }
+
+ val run1 = wsk.action.invoke(name, Map("payload" -> testString.toJson))
+ withActivation(wsk.activation, run1) {
+ activation =>
+ activation.response.status shouldBe "success"
+ activation.logs.get.mkString(" ") should include(s"The message '$testString' has")
+ }
+
+ wsk.action.delete(name)
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("hello.js")))
+ }
+
+ val run2 = wsk.action.invoke(name, Map("payload" -> testString.toJson))
+ withActivation(wsk.activation, run2) {
+ activation =>
+ activation.response.status shouldBe "success"
+ activation.logs.get.mkString(" ") should include(s"hello $testString")
+ }
+ }
+
+ it should "fail to invoke an action with an empty file" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "empty"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("empty.js")))
+ }
+ val run = wsk.action.invoke(name)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe "action developer error"
+ activation.response.result shouldBe Some(JsObject("error" -> "Missing main/no code to execute.".toJson))
+ }
+ }
+
+ it should "create an action with an empty file" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "empty"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("empty.js")))
+ }
+ val rr = wsk.action.get(name)
+ wsk.parseJsonString(rr.stdout).getFieldPath("exec", "code") shouldBe Some(JsString(""))
+ }
+
+ it should "blocking invoke of nested blocking actions" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "nestedBlockingAction"
+ val child = "wc"
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("wcbin.js")))
+ }
+ assetHelper.withCleaner(wsk.action, child) {
+ (action, _) => action.create(child, Some(TestUtils.getTestActionFilename("wc.js")))
+ }
+
+ val run = wsk.action.invoke(name, Map("payload" -> testString.toJson), blocking = true)
+ val activation = wsk.parseJsonString(run.stdout).convertTo[CliActivation]
+
+ withClue(s"check failed for activation: $activation") {
+ val wordCount = testString.split(" ").length
+ activation.response.result.get shouldBe JsObject("binaryCount" -> s"${wordCount.toBinaryString} (base 2)".toJson)
+ }
+ }
+
+ it should "blocking invoke an asynchronous action" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "helloAsync"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloAsync.js")))
+ }
+
+ val run = wsk.action.invoke(name, Map("payload" -> testString.toJson), blocking = true)
+ val activation = wsk.parseJsonString(run.stdout).convertTo[CliActivation]
+
+ withClue(s"check failed for activation: $activation") {
+ activation.response.status shouldBe "success"
+ activation.response.result shouldBe Some(testResult)
+ activation.logs shouldBe Some(List())
+ }
+ }
+
+ it should "reject an invoke with the wrong parameters set" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val fullQualifiedName = s"/$guestNamespace/samples/helloWorld"
+ val payload = "bob"
+ val rr = wsk.cli(Seq("action", "invoke", fullQualifiedName, payload) ++ wskprops.overrides,
+ expectedExitCode = TestUtils.ERROR_EXIT)
+ rr.stderr should include("Run 'wsk --help' for usage.")
+ rr.stderr should include(s"error: Invalid argument(s): $payload")
+ }
+
+ it should "not be able to use 'ping' in an action" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "ping"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("ping.js")))
+ }
+
+ val run = wsk.action.invoke(name, Map("payload" -> "google.com".toJson))
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.result shouldBe Some(JsObject(
+ "stderr" -> "ping: icmp open socket: Operation not permitted\n".toJson,
+ "stdout" -> "".toJson))
+ }
+ }
+
+ ignore should "support UTF-8 as input and output format" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "utf8Test"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("hello.js")))
+ }
+
+ val utf8 = "«ταБЬℓσö»: 1<2 & 4+1>³, now 20%€§$ off!"
+ val run = wsk.action.invoke(name, Map("payload" -> utf8.toJson))
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe "success"
+ activation.logs.get.mkString(" ") should include(s"hello $utf8")
+ }
+ }
+
+}
diff --git a/tests/src/test/scala/system/basic/WskBasicTests.scala b/tests/src/test/scala/system/basic/WskBasicTests.scala
new file mode 100644
index 0000000..f9e2fb9
--- /dev/null
+++ b/tests/src/test/scala/system/basic/WskBasicTests.scala
@@ -0,0 +1,739 @@
+/*
+ * Copyright 2015-2016 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 system.basic
+
+import java.io.File
+import java.time.Instant
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils._
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import spray.json.pimpAny
+import scala.concurrent.duration.DurationInt
+import scala.language.postfixOps
+
+@RunWith(classOf[JUnitRunner])
+class WskBasicTests
+ extends TestHelpers
+ with WskTestHelpers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val defaultAction = Some(TestUtils.getTestActionFilename("hello.js"))
+
+ behavior of "Wsk CLI"
+
+ it should "confirm wsk exists" in {
+ Wsk.exists
+ }
+
+ it should "show api build details" in {
+ val tmpProps = File.createTempFile("wskprops", ".tmp")
+ try {
+ val env = Map("WSK_CONFIG_FILE" -> tmpProps.getAbsolutePath())
+ wsk.cli(Seq("property", "set", "-i") ++ wskprops.overrides, env = env)
+ val rr = wsk.cli(Seq("property", "get", "--apibuild", "--apibuildno", "-i"), env = env)
+ rr.stderr should not include ("https:///api/v1: http: no Host in request URL")
+ rr.stdout should not include regex("Cannot determine API build")
+ rr.stdout should include regex ("""(?i)whisk API build\s+201.*""")
+ rr.stdout should include regex ("""(?i)whisk API build number\s+.*""")
+ } finally {
+ tmpProps.delete()
+ }
+ }
+
+ it should "reject creating duplicate entity" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "testDuplicateCreate"
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) => trigger.create(name)
+ }
+ assetHelper.withCleaner(wsk.action, name, confirmDelete = false) {
+ (action, _) => action.create(name, defaultAction, expectedExitCode = CONFLICT)
+ }
+ }
+
+ it should "reject deleting entity in wrong collection" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "testCrossDelete"
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) => trigger.create(name)
+ }
+ wsk.action.delete(name, expectedExitCode = CONFLICT)
+ }
+
+ it should "reject unauthenticated access" in {
+ implicit val wskprops = WskProps("xxx") // shadow properties
+ val errormsg = "The supplied authentication is invalid"
+ wsk.namespace.list(expectedExitCode = UNAUTHORIZED).
+ stderr should include(errormsg)
+ wsk.namespace.get(expectedExitCode = UNAUTHORIZED).
+ stderr should include(errormsg)
+ }
+
+ behavior of "Wsk Package CLI"
+
+ it should "create, update, get and list a package" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "testPackage"
+ val params = Map("a" -> "A".toJson)
+ assetHelper.withCleaner(wsk.pkg, name) {
+ (pkg, _) =>
+ pkg.create(name, parameters = params, shared = Some(true))
+ pkg.create(name, update = true)
+ }
+ val stdout = wsk.pkg.get(name).stdout
+ stdout should include regex (""""key": "a"""")
+ stdout should include regex (""""value": "A"""")
+ stdout should include regex (""""publish": true""")
+ stdout should include regex (""""version": "0.0.2"""")
+ wsk.pkg.list().stdout should include(name)
+ }
+
+ it should "create, and get a package summary" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val packageName = "packageName"
+ val actionName = "actionName"
+ val packageAnnots = Map(
+ "description" -> JsString("Package description"),
+ "parameters" -> JsArray(
+ JsObject(
+ "name" -> JsString("paramName1"),
+ "description" -> JsString("Parameter description 1")),
+ JsObject(
+ "name" -> JsString("paramName2"),
+ "description" -> JsString("Parameter description 2"))))
+ val actionAnnots = Map(
+ "description" -> JsString("Action description"),
+ "parameters" -> JsArray(
+ JsObject(
+ "name" -> JsString("paramName1"),
+ "description" -> JsString("Parameter description 1")),
+ JsObject(
+ "name" -> JsString("paramName2"),
+ "description" -> JsString("Parameter description 2"))))
+
+ assetHelper.withCleaner(wsk.pkg, packageName) {
+ (pkg, _) =>
+ pkg.create(packageName, annotations = packageAnnots)
+ }
+
+ wsk.action.create(packageName + "/" + actionName, defaultAction, annotations = actionAnnots)
+ val stdout = wsk.pkg.get(packageName, summary = true).stdout
+ val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+ wsk.action.delete(packageName + "/" + actionName)
+
+ stdout should include regex (s"(?i)package /${ns_regex_list}/${packageName}: Package description\\s*\\(parameters: paramName1, paramName2\\)")
+ stdout should include regex (s"(?i)action /${ns_regex_list}/${packageName}/${actionName}: Action description\\s*\\(parameters: paramName1, paramName2\\)")
+ }
+
+ it should "create a package with a name that contains spaces" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "package with spaces"
+
+ val res = assetHelper.withCleaner(wsk.pkg, name) {
+ (pkg, _) =>
+ pkg.create(name)
+ }
+
+ res.stdout should include(s"ok: created package $name")
+ }
+
+ it should "create a package, and get its individual fields" in withAssetCleaner(wskprops) {
+ val name = "packageFields"
+ val paramInput = Map("payload" -> "test".toJson)
+ val successMsg = s"ok: got package $name, displaying field"
+
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, name) {
+ (action, _) => action.create(name, parameters = paramInput)
+ }
+
+ val expectedParam = JsObject(
+ "payload" -> JsString("test"))
+
+ val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+ wsk.pkg.get(name, fieldFilter = Some("namespace")).stdout should include regex (s"""(?i)$successMsg namespace\n$ns_regex_list""")
+ wsk.pkg.get(name, fieldFilter = Some("name")).stdout should include(s"""$successMsg name\n"$name"""")
+ wsk.pkg.get(name, fieldFilter = Some("version")).stdout should include(s"""$successMsg version\n"0.0.1"""")
+ wsk.pkg.get(name, fieldFilter = Some("publish")).stdout should include(s"""$successMsg publish\nfalse""")
+ wsk.pkg.get(name, fieldFilter = Some("binding")).stdout should include regex (s"""\\{\\}""")
+ wsk.pkg.get(name, fieldFilter = Some("invalid"), expectedExitCode = ERROR_EXIT).stderr should include("error: Invalid field filter 'invalid'.")
+ }
+
+ behavior of "Wsk Action CLI"
+
+ it should "create the same action twice with different cases" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, "TWICE") { (action, name) => action.create(name, defaultAction) }
+ assetHelper.withCleaner(wsk.action, "twice") { (action, name) => action.create(name, defaultAction) }
+ }
+
+ it should "create an action, then update its kind" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "createAndUpdate"
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, file, kind = Some("nodejs"))
+ }
+
+ // create action as nodejs (v0.12)
+ wsk.action.get(name).stdout should include regex (""""kind": "nodejs"""")
+
+ // update to nodejs:6
+ wsk.action.create(name, file, kind = Some("nodejs:6"), update = true)
+ wsk.action.get(name).stdout should include regex (""""kind": "nodejs:6"""")
+ }
+
+ it should "create, update, get and list an action" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "createAndUpdate"
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ val params = Map("a" -> "A".toJson)
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, file, parameters = params)
+ action.create(name, None, parameters = Map("b" -> "B".toJson), update = true)
+ }
+ val stdout = wsk.action.get(name).stdout
+ stdout should not include regex(""""key": "a"""")
+ stdout should not include regex(""""value": "A"""")
+ stdout should include regex (""""key": "b""")
+ stdout should include regex (""""value": "B"""")
+ stdout should include regex (""""publish": false""")
+ stdout should include regex (""""version": "0.0.2"""")
+ wsk.action.list().stdout should include(name)
+ }
+
+ it should "reject delete of action that does not exist" in {
+ wsk.action.sanitize("deleteFantasy").
+ stderr should include regex ("""The requested resource does not exist. \(code \d+\)""")
+ }
+
+ it should "create, and invoke an action that utilizes a docker container" in withAssetCleaner(wskprops) {
+ val name = "dockerContainer"
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, name) {
+ // this docker image will be need to be pulled from dockerhub and hence has to be published there first
+ (action, _) => action.create(name, Some("openwhisk/example"), kind = Some("docker"))
+ }
+
+ val args = Map("payload" -> "test".toJson)
+ val run = wsk.action.invoke(name, args)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.result shouldBe Some(JsObject(
+ "args" -> args.toJson,
+ "msg" -> "Hello from arbitrary C program!".toJson))
+ }
+ }
+
+ it should "create, and invoke an action that utilizes dockerskeleton with native zip" in withAssetCleaner(wskprops) {
+ val name = "dockerContainerWithZip"
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, name) {
+ // this docker image will be need to be pulled from dockerhub and hence has to be published there first
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("blackbox.zip")), kind = Some("docker"))
+ }
+
+ val run = wsk.action.invoke(name, Map())
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.result shouldBe Some(JsObject(
+ "msg" -> "hello zip".toJson))
+ activation.logs shouldBe defined
+ val logs = activation.logs.get.toString
+ logs should include("This is an example zip used with the docker skeleton action.")
+ logs should not include ("XXX_THE_END_OF_A_WHISK_ACTIVATION_XXX")
+ }
+ }
+
+ it should "create, and invoke an action using a parameter file" in withAssetCleaner(wskprops) {
+ val name = "paramFileAction"
+ val file = Some(TestUtils.getTestActionFilename("argCheck.js"))
+ val argInput = Some(TestUtils.getTestActionFilename("validInput2.json"))
+
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, file)
+ }
+
+ val expectedOutput = JsObject(
+ "payload" -> JsString("test"))
+ val run = wsk.action.invoke(name, parameterFile = argInput)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.result shouldBe Some(expectedOutput)
+ }
+ }
+
+ it should "create an action, and get its individual fields" in withAssetCleaner(wskprops) {
+ val name = "actionFields"
+ val paramInput = Map("payload" -> "test".toJson)
+ val successMsg = s"ok: got action $name, displaying field"
+
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, defaultAction, parameters = paramInput)
+ }
+
+ val expectedParam = JsObject(
+ "payload" -> JsString("test"))
+
+ val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+ wsk.action.get(name, fieldFilter = Some("name")).stdout should include(s"""$successMsg name\n"$name"""")
+ wsk.action.get(name, fieldFilter = Some("version")).stdout should include(s"""$successMsg version\n"0.0.1"""")
+ wsk.action.get(name, fieldFilter = Some("exec")).stdout should include regex (s"""$successMsg exec\n\\{\\s+"kind":\\s+"nodejs:6",\\s+"code":\\s+"\\/\\*\\*[\\\\r]*\\\\n \\* Hello, world.[\\\\r]*\\\\n \\*\\/[\\\\r]*\\\\nfunction main\\(params\\) \\{[\\\\r]*\\\\n console.log\\('hello', params.payload\\+'!'\\);[\\\\r]*\\\\n\\}[\\\\r]*\\\\n"\n\\}""")
+ wsk.action.get(name, fieldFilter = Some("parameters")).stdout should include regex (s"""$successMsg parameters\n\\[\\s+\\{\\s+"key":\\s+"payload",\\s+"value":\\s+"test"\\s+\\}\\s+\\]""")
+ wsk.action.get(name, fieldFilter = Some("annotations")).stdout should include regex (s"""$successMsg annotations\n\\[\\s+\\{\\s+"key":\\s+"exec",\\s+"value":\\s+"nodejs:6"\\s+\\}\\s+\\]""")
+ wsk.action.get(name, fieldFilter = Some("limits")).stdout should include regex (s"""$successMsg limits\n\\{\\s+"timeout":\\s+60000,\\s+"memory":\\s+256,\\s+"logs":\\s+10\\s+\\}""")
+ wsk.action.get(name, fieldFilter = Some("namespace")).stdout should include regex (s"""(?i)$successMsg namespace\n$ns_regex_list""")
+ wsk.action.get(name, fieldFilter = Some("invalid"), expectedExitCode = ERROR_EXIT).stderr should include("error: Invalid field filter 'invalid'.")
+ wsk.action.get(name, fieldFilter = Some("publish")).stdout should include(s"""$successMsg publish\nfalse""")
+ }
+
+ /**
+ * Tests creating an action from a malformed js file. This should fail in
+ * some way - preferably when trying to create the action. If not, then
+ * surely when it runs there should be some indication in the logs. Don't
+ * think this is true currently.
+ */
+ it should "create and invoke action with malformed js resulting in activation error" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "MALFORMED"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("malformed.js")))
+ }
+
+ val run = wsk.action.invoke(name, Map("payload" -> "whatever".toJson))
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe "action developer error"
+ // representing nodejs giving an error when given malformed.js
+ activation.response.result.get.toString should include("ReferenceError")
+ }
+ }
+
+ it should "create and invoke a blocking action resulting in an application error response" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "applicationError"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("applicationError.js")))
+ }
+
+ wsk.action.invoke(name, blocking = true, expectedExitCode = 246)
+ .stderr should include regex (""""error": "This error thrown on purpose by the action."""")
+ }
+
+ it should "create and invoke a blocking action resulting in an failed promise" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "errorResponseObject"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("asyncError.js")))
+ }
+
+ val stderr = wsk.action.invoke(name, blocking = true, expectedExitCode = 246).stderr
+ CliActivation.serdes.read(stderr.parseJson).response.result shouldBe Some {
+ JsObject("error" -> JsObject("msg" -> "failed activation on purpose".toJson))
+ }
+ }
+
+ it should "invoke a blocking action and get only the result" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "basicInvoke"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("wc.js")))
+ }
+ wsk.action.invoke(name, Map("payload" -> "one two three".toJson), blocking = true, result = true)
+ .stdout should include regex (""""count": 3""")
+ }
+
+ it should "create, and get an action summary" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "actionName"
+ val annots = Map(
+ "description" -> JsString("Action description"),
+ "parameters" -> JsArray(
+ JsObject(
+ "name" -> JsString("paramName1"),
+ "description" -> JsString("Parameter description 1")),
+ JsObject(
+ "name" -> JsString("paramName2"),
+ "description" -> JsString("Parameter description 2"))))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, defaultAction, annotations = annots)
+ }
+
+ val stdout = wsk.action.get(name, summary = true).stdout
+ val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+ stdout should include regex (s"(?i)action /${ns_regex_list}/${name}: Action description\\s*\\(parameters: paramName1, paramName2\\)")
+ }
+
+ it should "create an action with a name that contains spaces" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "action with spaces"
+
+ val res = assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, defaultAction)
+ }
+
+ res.stdout should include(s"ok: created action $name")
+ }
+
+ it should "create an action, and invoke an action that returns an empty JSON object" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "emptyJSONAction"
+
+ val res = assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, Some(TestUtils.getTestActionFilename("emptyJSONResult.js")))
+ action.invoke(name, blocking = true, result = true)
+ }
+
+ res.stdout shouldBe ("{}\n")
+ }
+
+ it should "create, and invoke an action that times out to ensure the result is empty" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "sleepAction"
+ val params = Map("payload" -> "100000".toJson)
+ val allowedActionDuration = 120 seconds
+ val res = assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, Some(TestUtils.getTestActionFilename("timeout.js")),
+ timeout = Some(allowedActionDuration))
+ action.invoke(name, parameters = params, blocking = true, result = true)
+ }
+
+ res.stdout should include regex (s"""\\{\\s+"activationId":\\s+"[a-z0-9]{32}"\\s+\\}""")
+ }
+
+ it should "create, and get docker action get ensure exec code is omitted" in withAssetCleaner(wskprops) {
+ val name = "dockerContainer"
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some("fakeContainer"), kind = Some("docker"))
+ }
+
+ wsk.action.get(name).stdout should not include (""""code"""")
+ }
+
+ behavior of "Wsk Trigger CLI"
+
+ it should "create, update, get, fire and list trigger" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "listTriggers"
+ val params = Map("a" -> "A".toJson)
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name, parameters = params)
+ trigger.create(name, update = true)
+ }
+ val stdout = wsk.trigger.get(name).stdout
+ stdout should include regex (""""key": "a"""")
+ stdout should include regex (""""value": "A"""")
+ stdout should include regex (""""publish": false""")
+ stdout should include regex (""""version": "0.0.2"""")
+
+ val dynamicParams = Map("t" -> "T".toJson)
+ val run = wsk.trigger.fire(name, dynamicParams)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.result shouldBe Some(dynamicParams.toJson)
+ activation.duration shouldBe 0L // shouldn't exist but CLI generates it
+ activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it
+ }
+
+ val runWithNoParams = wsk.trigger.fire(name, Map())
+ withActivation(wsk.activation, runWithNoParams) {
+ activation =>
+ activation.response.result shouldBe Some(JsObject())
+ activation.duration shouldBe 0L // shouldn't exist but CLI generates it
+ activation.end shouldBe Instant.EPOCH // shouldn't exist but CLI generates it
+ }
+
+ wsk.trigger.list().stdout should include(name)
+ }
+
+ it should "create, and get a trigger summary" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "triggerName"
+ val annots = Map(
+ "description" -> JsString("Trigger description"),
+ "parameters" -> JsArray(
+ JsObject(
+ "name" -> JsString("paramName1"),
+ "description" -> JsString("Parameter description 1")),
+ JsObject(
+ "name" -> JsString("paramName2"),
+ "description" -> JsString("Parameter description 2"))))
+
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name, annotations = annots)
+ }
+
+ val stdout = wsk.trigger.get(name, summary = true).stdout
+ val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+ stdout should include regex (s"trigger /${ns_regex_list}/${name}: Trigger description\\s*\\(parameters: paramName1, paramName2\\)")
+ }
+
+ it should "create a trigger with a name that contains spaces" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "trigger with spaces"
+
+ val res = assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name)
+ }
+
+ res.stdout should include regex (s"ok: created trigger $name")
+ }
+
+ it should "create, and fire a trigger using a parameter file" in withAssetCleaner(wskprops) {
+ val name = "paramFileTrigger"
+ val file = Some(TestUtils.getTestActionFilename("argCheck.js"))
+ val argInput = Some(TestUtils.getTestActionFilename("validInput2.json"))
+
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name)
+ }
+
+ val expectedOutput = JsObject(
+ "payload" -> JsString("test"))
+ val run = wsk.trigger.fire(name, parameterFile = argInput)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.result shouldBe Some(expectedOutput)
+ }
+ }
+
+ it should "create a trigger, and get its individual fields" in withAssetCleaner(wskprops) {
+ val name = "triggerFields"
+ val paramInput = Map("payload" -> "test".toJson)
+ val successMsg = s"ok: got trigger $name, displaying field"
+
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name, parameters = paramInput)
+ }
+
+ val expectedParam = JsObject(
+ "payload" -> JsString("test"))
+
+ val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+ wsk.trigger.get(name, fieldFilter = Some("namespace")).stdout should include regex (s"""(?i)$successMsg namespace\n$ns_regex_list""")
+ wsk.trigger.get(name, fieldFilter = Some("name")).stdout should include(s"""$successMsg name\n"$name"""")
+ wsk.trigger.get(name, fieldFilter = Some("version")).stdout should include(s"""$successMsg version\n"0.0.1"""")
+ wsk.trigger.get(name, fieldFilter = Some("publish")).stdout should include(s"""$successMsg publish\nfalse""")
+ wsk.trigger.get(name, fieldFilter = Some("annotations")).stdout should include(s"""$successMsg annotations\n[]""")
+ wsk.trigger.get(name, fieldFilter = Some("parameters")).stdout should include regex (s"""$successMsg parameters\n\\[\\s+\\{\\s+"key":\\s+"payload",\\s+"value":\\s+"test"\\s+\\}\\s+\\]""")
+ wsk.trigger.get(name, fieldFilter = Some("limits")).stdout should include(s"""$successMsg limits\n{}""")
+ wsk.trigger.get(name, fieldFilter = Some("invalid"), expectedExitCode = ERROR_EXIT).stderr should include("error: Invalid field filter 'invalid'.")
+ }
+
+ it should "create, and fire a trigger to ensure result is empty" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "emptyResultTrigger"
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name)
+ }
+
+ val run = wsk.trigger.fire(name)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.result shouldBe Some(JsObject())
+ }
+ }
+
+ behavior of "Wsk Rule CLI"
+
+ it should "create rule, get rule, update rule and list rule" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val ruleName = "listRules"
+ val triggerName = "listRulesTrigger"
+ val actionName = "listRulesAction";
+ assetHelper.withCleaner(wsk.trigger, triggerName) {
+ (trigger, name) => trigger.create(name)
+ }
+ assetHelper.withCleaner(wsk.action, actionName) {
+ (action, name) => action.create(name, defaultAction)
+ }
+ assetHelper.withCleaner(wsk.rule, ruleName) {
+ (rule, name) =>
+ rule.create(name, trigger = triggerName, action = actionName)
+ }
+
+ // finally, we perform the update, and expect success this time
+ wsk.rule.create(ruleName, trigger = triggerName, action = actionName, update = true)
+
+ val stdout = wsk.rule.get(ruleName).stdout
+ stdout should include(ruleName)
+ stdout should include(triggerName)
+ stdout should include(actionName)
+ stdout should include regex (""""version": "0.0.2"""")
+ wsk.rule.list().stdout should include(ruleName)
+ }
+
+ it should "create rule, get rule, ensure rule is enabled by default" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val ruleName = "enabledRule"
+ val triggerName = "enabledRuleTrigger"
+ val actionName = "enabledRuleAction";
+ assetHelper.withCleaner(wsk.trigger, triggerName) {
+ (trigger, name) => trigger.create(name)
+ }
+ assetHelper.withCleaner(wsk.action, actionName) {
+ (action, name) => action.create(name, defaultAction)
+ }
+ assetHelper.withCleaner(wsk.rule, ruleName) {
+ (rule, name) =>
+ rule.create(name, trigger = triggerName, action = actionName)
+ }
+
+ val stdout = wsk.rule.get(ruleName).stdout
+ stdout should include regex (""""status":\s*"active"""")
+ }
+
+ it should "display a rule summary when --summary flag is used with 'wsk rule get'" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val ruleName = "mySummaryRule"
+ val triggerName = "summaryRuleTrigger"
+ val actionName = "summaryRuleAction";
+ assetHelper.withCleaner(wsk.trigger, triggerName) {
+ (trigger, name) => trigger.create(name)
+ }
+ assetHelper.withCleaner(wsk.action, actionName) {
+ (action, name) => action.create(name, defaultAction)
+ }
+ assetHelper.withCleaner(wsk.rule, ruleName, confirmDelete = false) {
+ (rule, name) => rule.create(name, trigger = triggerName, action = actionName)
+ }
+ // Summary namespace should match one of the allowable namespaces (typically 'guest')
+ val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+ val stdout = wsk.rule.get(ruleName, summary = true).stdout
+
+ stdout should include regex (s"(?i)rule /${ns_regex_list}/${ruleName}\\s*\\(status: active\\)")
+ }
+
+ it should "create a rule, and get its individual fields" in withAssetCleaner(wskprops) {
+ val ruleName = "ruleFields"
+ val triggerName = "ruleTriggerFields"
+ val actionName = "ruleActionFields"; val paramInput = Map("payload" -> "test".toJson)
+ val successMsg = s"ok: got rule $ruleName, displaying field"
+
+ (wp, assetHelper) =>
+
+ assetHelper.withCleaner(wsk.trigger, triggerName) {
+ (trigger, name) => trigger.create(name)
+ }
+ assetHelper.withCleaner(wsk.action, actionName) {
+ (action, name) => action.create(name, defaultAction)
+ }
+ assetHelper.withCleaner(wsk.rule, ruleName) {
+ (rule, name) =>
+ rule.create(name, trigger = triggerName, action = actionName)
+ }
+
+ val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+ wsk.rule.get(ruleName, fieldFilter = Some("namespace")).stdout should include regex (s"""(?i)$successMsg namespace\n$ns_regex_list""")
+ wsk.rule.get(ruleName, fieldFilter = Some("name")).stdout should include(s"""$successMsg name\n"$ruleName"""")
+ wsk.rule.get(ruleName, fieldFilter = Some("version")).stdout should include(s"""$successMsg version\n"0.0.1"\n""")
+ wsk.rule.get(ruleName, fieldFilter = Some("status")).stdout should include(s"""$successMsg status\n"active"""")
+ val trigger = wsk.rule.get(ruleName, fieldFilter = Some("trigger")).stdout
+ trigger should include regex (s"""$successMsg trigger\n""")
+ trigger should include(triggerName)
+ trigger should not include (actionName)
+ val action = wsk.rule.get(ruleName, fieldFilter = Some("action")).stdout
+ action should include regex (s"""$successMsg action\n""")
+ action should include(actionName)
+ action should not include (triggerName)
+ }
+
+ behavior of "Wsk Namespace CLI"
+
+ it should "return a list of exactly one namespace" in {
+ wsk.namespace.list().
+ stdout.lines should have size 2 // headline + namespace
+ }
+
+ it should "list entities in default namespace" in {
+ // use a fresh wsk props instance that is guaranteed to use
+ // the default namespace
+ wsk.namespace.get(expectedExitCode = SUCCESS_EXIT)(WskProps()).
+ stdout should include("default")
+ }
+
+ it should "not list entities with an invalid namespace" in {
+ val namespace = "fakeNamespace"
+ val stderr = wsk.namespace.get(Some(s"/${namespace}"), expectedExitCode = FORBIDDEN).stderr
+
+ stderr should include(s"Unable to obtain the list of entities for namespace '${namespace}'")
+ }
+
+ behavior of "Wsk Activation CLI"
+
+ it should "create a trigger, and fire a trigger to get its individual fields from an activation" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "activationFields"
+
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name)
+ }
+
+ val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+
+ val run = wsk.trigger.fire(name)
+ withActivation(wsk.activation, run) {
+ activation =>
+ val successMsg = s"ok: got activation ${activation.activationId}, displaying field"
+ wsk.activation.get(activation.activationId, fieldFilter = Some("namespace")).stdout should include regex (s"""(?i)$successMsg namespace\n$ns_regex_list""")
+ wsk.activation.get(activation.activationId, fieldFilter = Some("name")).stdout should include(s"""$successMsg name\n"$name"""")
+ wsk.activation.get(activation.activationId, fieldFilter = Some("version")).stdout should include(s"""$successMsg version\n"0.0.1"""")
+ wsk.activation.get(activation.activationId, fieldFilter = Some("publish")).stdout should include(s"""$successMsg publish\nfalse""")
+ wsk.activation.get(activation.activationId, fieldFilter = Some("subject")).stdout should include regex (s"""(?i)$successMsg subject\n""")
+ wsk.activation.get(activation.activationId, fieldFilter = Some("activationid")).stdout should include(s"""$successMsg activationid\n"${activation.activationId}""")
+ wsk.activation.get(activation.activationId, fieldFilter = Some("start")).stdout should include regex (s"""$successMsg start\n\\d""")
+ wsk.activation.get(activation.activationId, fieldFilter = Some("end")).stdout should include regex (s"""$successMsg end\n\\d""")
+ wsk.activation.get(activation.activationId, fieldFilter = Some("duration")).stdout should include regex (s"""$successMsg duration\n\\d""")
+ wsk.activation.get(activation.activationId, fieldFilter = Some("annotations")).stdout should include(s"""$successMsg annotations\n[]""")
+ }
+ }
+}
diff --git a/tests/src/test/scala/system/basic/WskRuleTests.scala b/tests/src/test/scala/system/basic/WskRuleTests.scala
new file mode 100644
index 0000000..aef370d
--- /dev/null
+++ b/tests/src/test/scala/system/basic/WskRuleTests.scala
@@ -0,0 +1,360 @@
+/*
+ * Copyright 2015-2016 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 system.basic
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import java.time.Instant
+
+@RunWith(classOf[JUnitRunner])
+class WskRuleTests
+ extends TestHelpers
+ with WskTestHelpers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val defaultAction = TestUtils.getTestActionFilename("wc.js")
+ val secondAction = TestUtils.getTestActionFilename("hello.js")
+ val testString = "this is a test"
+ val testResult = JsObject("count" -> testString.split(" ").length.toJson)
+
+ /**
+ * Sets up trigger -> rule -> action triplets. Deduplicates triggers and rules
+ * and links it all up.
+ *
+ * @param rules Tuple3s containing
+ * (rule, trigger, (action name for created action, action name for the rule binding, actionFile))
+ * where the action name for the created action is allowed to differ from that used by the rule binding
+ * for cases that reference actions in a package binding.
+ */
+ def ruleSetup(rules: Seq[(String, String, (String, String, String))], assetHelper: AssetCleaner) = {
+ val triggers = rules.map(_._2).distinct
+ val actions = rules.map(_._3).distinct
+
+ triggers.foreach { trigger =>
+ assetHelper.withCleaner(wsk.trigger, trigger) {
+ (trigger, name) => trigger.create(name)
+ }
+ }
+
+ actions.foreach {
+ case (actionName, _, file) =>
+ assetHelper.withCleaner(wsk.action, actionName) {
+ (action, name) => action.create(name, Some(file))
+ }
+ }
+
+ rules.foreach {
+ case (ruleName, triggerName, action) =>
+ assetHelper.withCleaner(wsk.rule, ruleName) {
+ (rule, name) => rule.create(name, triggerName, action._2)
+ }
+ }
+ }
+
+ behavior of "Whisk rules"
+
+ it should "invoke the action attached on trigger fire, creating an activation for each entity including the cause" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val ruleName = "r1to1"
+ val triggerName = "t1to1"
+ val actionName = "a1 to 1" // spaces in name intended for greater test coverage
+
+ ruleSetup(Seq(
+ (ruleName, triggerName, (actionName, actionName, defaultAction))),
+ assetHelper)
+
+ val run = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+ withActivation(wsk.activation, run) {
+ triggerActivation =>
+ triggerActivation.cause shouldBe None
+
+ withActivationsFromEntity(wsk.activation, ruleName, since = Some(triggerActivation.start)) {
+ _.head.cause shouldBe Some(triggerActivation.activationId)
+ }
+
+ withActivationsFromEntity(wsk.activation, actionName, since = Some(triggerActivation.start)) { activationList =>
+ activationList.head.response.result shouldBe Some(testResult)
+ activationList.head.cause shouldBe None
+ }
+ }
+ }
+
+ it should "invoke the action from a package attached on trigger fire, creating an activation for each entity including the cause" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val ruleName = "pr1to1"
+ val triggerName = "pt1to1"
+ val pkgName = "rule pkg" // spaces in name intended to test uri path encoding
+ val actionName = "a1 to 1"
+ val pkgActionName = s"$pkgName/$actionName"
+
+ assetHelper.withCleaner(wsk.pkg, pkgName) {
+ (pkg, name) => pkg.create(name)
+ }
+
+ ruleSetup(Seq(
+ (ruleName, triggerName, (pkgActionName, pkgActionName, defaultAction))),
+ assetHelper)
+
+ val now = Instant.now
+ val run = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+ withActivation(wsk.activation, run) {
+ triggerActivation =>
+ triggerActivation.cause shouldBe None
+
+ withActivationsFromEntity(wsk.activation, ruleName, since = Some(triggerActivation.start)) {
+ _.head.cause shouldBe Some(triggerActivation.activationId)
+ }
+
+ withActivationsFromEntity(wsk.activation, actionName, since = Some(triggerActivation.start)) {
+ _.head.response.result shouldBe Some(testResult)
+ }
+ }
+ }
+
+ it should "invoke the action from a package binding attached on trigger fire, creating an activation for each entity including the cause" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val ruleName = "pr1to1"
+ val triggerName = "pt1to1"
+ val pkgName = "rule pkg" // spaces in name intended to test uri path encoding
+ val pkgBindingName = "rule pkg binding"
+ val actionName = "a1 to 1"
+ val pkgActionName = s"$pkgName/$actionName"
+
+ assetHelper.withCleaner(wsk.pkg, pkgName) {
+ (pkg, name) => pkg.create(name)
+ }
+
+ assetHelper.withCleaner(wsk.pkg, pkgBindingName) {
+ (pkg, name) => pkg.bind(pkgName, pkgBindingName)
+ }
+
+ ruleSetup(Seq(
+ (ruleName, triggerName, (pkgActionName, s"$pkgBindingName/$actionName", defaultAction))),
+ assetHelper)
+
+ val run = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+ withActivation(wsk.activation, run) {
+ triggerActivation =>
+ triggerActivation.cause shouldBe None
+
+ withActivationsFromEntity(wsk.activation, ruleName, since = Some(triggerActivation.start)) {
+ _.head.cause shouldBe Some(triggerActivation.activationId)
+ }
+
+ withActivationsFromEntity(wsk.activation, actionName, since = Some(triggerActivation.start)) {
+ _.head.response.result shouldBe Some(testResult)
+ }
+ }
+ }
+
+ it should "not activate an action if the rule is deleted when the trigger is fired" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val ruleName = "ruleDelete"
+ val triggerName = "ruleDeleteTrigger"
+ val actionName = "ruleDeleteAction"
+
+ assetHelper.withCleaner(wsk.trigger, triggerName) {
+ (trigger, name) => trigger.create(name)
+ }
+ assetHelper.withCleaner(wsk.action, actionName) {
+ (action, name) => action.create(name, Some(defaultAction))
+ }
+ assetHelper.withCleaner(wsk.rule, ruleName, false) {
+ (rule, name) => rule.create(name, triggerName, actionName)
+ }
+
+ val first = wsk.trigger.fire(triggerName, Map("payload" -> "bogus".toJson))
+ wsk.rule.delete(ruleName)
+ wsk.trigger.fire(triggerName, Map("payload" -> "bogus2".toJson))
+
+ withActivation(wsk.activation, first) {
+ activation =>
+ // tries to find 2 activations for the action, should only find 1
+ val activations = wsk.activation.pollFor(2, Some(actionName), since = Some(activation.start), retries = 30)
+
+ activations.length shouldBe 1
+ }
+ }
+
+ it should "enable and disable a rule and check action is activated only when rule is enabled" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val ruleName = "ruleDisable"
+ val triggerName = "ruleDisableTrigger"
+ val actionName = "ruleDisableAction"
+
+ ruleSetup(Seq(
+ (ruleName, triggerName, (actionName, actionName, defaultAction))),
+ assetHelper)
+
+ val first = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+ wsk.rule.disableRule(ruleName)
+ wsk.trigger.fire(triggerName, Map("payload" -> s"$testString with added words".toJson))
+ wsk.rule.enableRule(ruleName)
+ wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+ withActivation(wsk.activation, first) {
+ triggerActivation =>
+ withActivationsFromEntity(wsk.activation, actionName, N = 2, since = Some(triggerActivation.start)) {
+ activations =>
+ val results = activations.map(_.response.result)
+ results should contain theSameElementsAs Seq(Some(testResult), Some(testResult))
+ }
+ }
+ }
+
+ it should "be able to recreate a rule with the same name and match it successfully" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val ruleName = "ruleRecreate"
+ val triggerName1 = "ruleRecreateTrigger1"
+ val triggerName2 = "ruleRecreateTrigger2"
+ val actionName = "ruleRecreateAction"
+
+ assetHelper.withCleaner(wsk.trigger, triggerName1) {
+ (trigger, name) => trigger.create(name)
+ }
+ assetHelper.withCleaner(wsk.action, actionName) {
+ (action, name) => action.create(name, Some(defaultAction))
+ }
+ assetHelper.withCleaner(wsk.rule, ruleName, false) {
+ (rule, name) => rule.create(name, triggerName1, actionName)
+ }
+
+ wsk.rule.delete(ruleName)
+
+ assetHelper.withCleaner(wsk.trigger, triggerName2) {
+ (trigger, name) => trigger.create(name)
+ }
+ assetHelper.withCleaner(wsk.rule, ruleName) {
+ (rule, name) => rule.create(name, triggerName2, actionName)
+ }
+
+ val first = wsk.trigger.fire(triggerName2, Map("payload" -> testString.toJson))
+ withActivation(wsk.activation, first) {
+ triggerActivation =>
+ withActivationsFromEntity(wsk.activation, actionName, since = Some(triggerActivation.start)) {
+ _.head.response.result shouldBe Some(testResult)
+ }
+ }
+ }
+
+ it should "connect two triggers via rules to one action and activate it accordingly" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val triggerName1 = "t2to1a"
+ val triggerName2 = "t2to1b"
+ val actionName = "a2to1"
+
+ ruleSetup(Seq(
+ ("r2to1a", triggerName1, (actionName, actionName, defaultAction)),
+ ("r2to1b", triggerName2, (actionName, actionName, defaultAction))),
+ assetHelper)
+
+ val testPayloads = Seq("got three words", "got four words, period")
+
+ val run = wsk.trigger.fire(triggerName1, Map("payload" -> testPayloads(0).toJson))
+ wsk.trigger.fire(triggerName2, Map("payload" -> testPayloads(1).toJson))
+
+ withActivation(wsk.activation, run) {
+ triggerActivation =>
+ withActivationsFromEntity(wsk.activation, actionName, N = 2, since = Some(triggerActivation.start)) {
+ activations =>
+ val results = activations.map(_.response.result)
+ val expectedResults = testPayloads.map { payload =>
+ Some(JsObject("count" -> payload.split(" ").length.toJson))
+ }
+
+ results should contain theSameElementsAs expectedResults
+ }
+ }
+ }
+
+ it should "connect one trigger to two different actions, invoking them both eventually" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val triggerName = "t1to2"
+ val actionName1 = "a1to2a"
+ val actionName2 = "a1to2b"
+
+ ruleSetup(Seq(
+ ("r1to2a", triggerName, (actionName1, actionName1, defaultAction)),
+ ("r1to2b", triggerName, (actionName2, actionName2, secondAction))),
+ assetHelper)
+
+ val run = wsk.trigger.fire(triggerName, Map("payload" -> testString.toJson))
+
+ withActivation(wsk.activation, run) {
+ triggerActivation =>
+ withActivationsFromEntity(wsk.activation, actionName1, since = Some(triggerActivation.start)) {
+ _.head.response.result shouldBe Some(testResult)
+ }
+ withActivationsFromEntity(wsk.activation, actionName2, since = Some(triggerActivation.start)) {
+ _.head.logs.get.mkString(" ") should include(s"hello $testString")
+ }
+ }
+ }
+
+ it should "connect two triggers to two different actions, invoking them both eventually" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val triggerName1 = "t1to1a"
+ val triggerName2 = "t1to1b"
+ val actionName1 = "a1to1a"
+ val actionName2 = "a1to1b"
+
+ ruleSetup(Seq(
+ ("r2to2a", triggerName1, (actionName1, actionName1, defaultAction)),
+ ("r2to2b", triggerName1, (actionName2, actionName2, secondAction)),
+ ("r2to2c", triggerName2, (actionName1, actionName1, defaultAction)),
+ ("r2to2d", triggerName2, (actionName2, actionName2, secondAction))),
+ assetHelper)
+
+ val testPayloads = Seq("got three words", "got four words, period")
+ val run = wsk.trigger.fire(triggerName1, Map("payload" -> testPayloads(0).toJson))
+ wsk.trigger.fire(triggerName2, Map("payload" -> testPayloads(1).toJson))
+
+ withActivation(wsk.activation, run) {
+ triggerActivation =>
+ withActivationsFromEntity(wsk.activation, actionName1, N = 2, since = Some(triggerActivation.start)) {
+ activations =>
+ val results = activations.map(_.response.result)
+ val expectedResults = testPayloads.map { payload =>
+ Some(JsObject("count" -> payload.split(" ").length.toJson))
+ }
+
+ results should contain theSameElementsAs expectedResults
+ }
+ withActivationsFromEntity(wsk.activation, actionName2, N = 2, since = Some(triggerActivation.start)) {
+ activations =>
+ // drops the leftmost 39 characters (timestamp + streamname)
+ val logs = activations.map(_.logs.get.map(_.drop(39))).flatten
+ val expectedLogs = testPayloads.map { payload => s"hello $payload!" }
+
+ logs should contain theSameElementsAs expectedLogs
+ }
+ }
+ }
+
+}
diff --git a/tests/src/test/scala/system/basic/WskSdkTests.scala b/tests/src/test/scala/system/basic/WskSdkTests.scala
new file mode 100644
index 0000000..0c79349
--- /dev/null
+++ b/tests/src/test/scala/system/basic/WskSdkTests.scala
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2015-2016 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 system.basic
+
+import java.io.File
+
+import scala.collection.JavaConversions.asScalaBuffer
+
+import org.apache.commons.io.FileUtils
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils.SUCCESS_EXIT
+import common.WhiskProperties
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+
+@RunWith(classOf[JUnitRunner])
+class WskSdkTests
+ extends TestHelpers
+ with WskTestHelpers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+
+ behavior of "Wsk SDK"
+
+ it should "download docker action sdk" in {
+ val dir = File.createTempFile("wskinstall", ".tmp")
+ dir.delete()
+ dir.mkdir() should be(true)
+ try {
+ wsk.cli(wskprops.overrides ++ Seq("sdk", "install", "docker"), workingDir = dir).
+ stdout should include("The docker skeleton is now installed at the current directory.")
+
+ val sdk = new File(dir, "dockerSkeleton")
+ sdk.exists() should be(true)
+ sdk.isDirectory() should be(true)
+
+ val dockerfile = new File(sdk, "Dockerfile")
+ dockerfile.exists() should be(true)
+ dockerfile.isFile() should be(true)
+ val lines = FileUtils.readLines(dockerfile)
+ // confirm that the image is correct
+ lines.get(1) shouldBe "FROM openwhisk/dockerskeleton"
+
+ val buildAndPushFile = new File(sdk, "buildAndPush.sh")
+ buildAndPushFile.canExecute() should be(true)
+
+ // confirm there is no other divergence from the base dockerfile
+ val originalDockerfile = WhiskProperties.getFileRelativeToWhiskHome("sdk/docker/Dockerfile")
+ val originalLines = FileUtils.readLines(originalDockerfile)
+ lines.get(0) shouldBe originalLines.get(0)
+ lines.drop(2).mkString("\n") shouldBe originalLines.drop(2).mkString("\n")
+ } finally {
+ FileUtils.deleteDirectory(dir)
+ }
+ }
+
+ it should "download iOS sdk" in {
+ val dir = File.createTempFile("wskinstall", ".tmp")
+ dir.delete()
+ dir.mkdir() should be(true)
+
+ wsk.cli(wskprops.overrides ++ Seq("sdk", "install", "iOS"), workingDir = dir).
+ stdout should include("Downloaded OpenWhisk iOS starter app. Unzip OpenWhiskIOSStarterApp.zip and open the project in Xcode.")
+
+ val sdk = new File(dir, "OpenWhiskIOSStarterApp.zip")
+ sdk.exists() should be(true)
+ sdk.isFile() should be(true)
+ FileUtils.sizeOf(sdk) should be > 30000L
+ FileUtils.deleteDirectory(dir)
+ }
+
+ it should "install the bash auto-completion bash script" in {
+ // Use a temp dir for testing to not disturb user's local folder
+ val dir = File.createTempFile("wskinstall", ".tmp")
+ dir.delete()
+ dir.mkdir() should be(true)
+
+ val scriptfilename = "wsk_cli_bash_completion.sh"
+ var scriptfile = new File(dir.getPath(), scriptfilename)
+ try {
+ val stdout = wsk.cli(Seq("sdk", "install", "bashauto"), workingDir = dir, expectedExitCode = SUCCESS_EXIT).stdout
+ stdout should include("is installed in the current directory")
+ val fileContent = FileUtils.readFileToString(scriptfile)
+ fileContent should include("bash completion for wsk")
+ } finally {
+ scriptfile.delete()
+ FileUtils.deleteDirectory(dir)
+ }
+ }
+
+}
diff --git a/tests/src/test/scala/system/basic/WskSequenceTests.scala b/tests/src/test/scala/system/basic/WskSequenceTests.scala
new file mode 100644
index 0000000..dd41108
--- /dev/null
+++ b/tests/src/test/scala/system/basic/WskSequenceTests.scala
@@ -0,0 +1,528 @@
+/*
+ * Copyright 2015-2016 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 system.basic
+
+import java.time.Instant
+import java.util.Date
+
+import scala.concurrent.duration.DurationInt
+import scala.language.postfixOps
+import scala.util.matching.Regex
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.StreamLogging
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils._
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import spray.testkit.ScalatestRouteTest
+import whisk.core.WhiskConfig
+import whisk.http.Messages.sequenceIsTooLong
+
+/**
+ * Tests sequence execution
+ */
+
+@RunWith(classOf[JUnitRunner])
+class WskSequenceTests
+ extends TestHelpers
+ with ScalatestRouteTest
+ with WskTestHelpers
+ with StreamLogging {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val allowedActionDuration = 120 seconds
+ val shortDuration = 10 seconds
+
+ val whiskConfig = new WhiskConfig(Map(WhiskConfig.actionSequenceDefaultLimit -> null))
+ assert(whiskConfig.isValid)
+
+ behavior of "Wsk Sequence"
+
+ it should "invoke a blocking sequence action and invoke the updated sequence with normal payload and payload with error field" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "sequence"
+ val actions = Seq("split", "sort", "head", "cat")
+ for (actionName <- actions) {
+ val file = TestUtils.getTestActionFilename(s"$actionName.js")
+ assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>
+ action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+ }
+ }
+
+ println(s"Sequence $actions")
+ assetHelper.withCleaner(wsk.action, name) {
+ val sequence = actions.mkString(",")
+ (action, _) => action.create(name, Some(sequence), kind = Some("sequence"), timeout = Some(allowedActionDuration))
+ }
+
+ val now = "it is now " + new Date()
+ val args = Array("what time is it?", now)
+ val run = wsk.action.invoke(name, Map("payload" -> args.mkString("\n").toJson))
+ withActivation(wsk.activation, run, totalWait = 4 * allowedActionDuration) {
+ activation =>
+ checkSequenceLogsAndAnnotations(activation, 4) // 4 activations in this sequence
+ activation.cause shouldBe None // topmost sequence
+ val result = activation.response.result.get
+ result.fields.get("payload") shouldBe defined
+ result.fields.get("length") should not be defined
+ result.fields.get("lines") shouldBe Some(JsArray(Vector(now.toJson)))
+ }
+
+ // update action sequence and run it with normal payload
+ val newSequence = Seq("split", "sort").mkString(",")
+ println(s"Update sequence to $newSequence")
+ wsk.action.create(name, Some(newSequence), kind = Some("sequence"), timeout = Some(allowedActionDuration), update = true)
+ val secondrun = wsk.action.invoke(name, Map("payload" -> args.mkString("\n").toJson))
+ withActivation(wsk.activation, secondrun, totalWait = 2 * allowedActionDuration) {
+ activation =>
+ checkSequenceLogsAndAnnotations(activation, 2) // 2 activations in this sequence
+ val result = activation.response.result.get
+ result.fields.get("length") shouldBe Some(2.toJson)
+ result.fields.get("lines") shouldBe Some(args.sortWith(_.compareTo(_) < 0).toArray.toJson)
+ }
+
+ println("Run sequence with error in payload")
+ // run sequence with error in the payload; nothing should run
+ val payload = Map("error" -> JsString("irrelevant error string"))
+ val thirdrun = wsk.action.invoke(name, payload)
+ withActivation(wsk.activation, thirdrun, totalWait = allowedActionDuration) {
+ activation =>
+ activation.logs shouldBe defined
+ // no activations should have run
+ activation.logs.get.size shouldBe (0)
+ activation.response.success shouldBe (false)
+ // the status should be error
+ activation.response.status shouldBe ("application error")
+ val result = activation.response.result.get
+ // the result of the activation should be the payload
+ result shouldBe (JsObject(payload))
+
+ }
+ }
+
+ /**
+ * s -> echo, x, echo
+ * x -> echo
+ *
+ * update x -> <limit-1> echo -- should work
+ * run s -> should stop after <limit> echo
+ */
+ it should "create a sequence, run it, update one of the atomic actions to a sequence and stop executing the outer sequence when limit reached" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val xName = "xSequence"
+ val sName = "sSequence"
+ val echo = "echo"
+
+ // create echo action
+ val file = TestUtils.getTestActionFilename(s"$echo.js")
+ assetHelper.withCleaner(wsk.action, echo) { (action, actionName) =>
+ action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+ }
+ // create x
+ assetHelper.withCleaner(wsk.action, xName) {
+ (action, seqName) => action.create(seqName, Some(echo), kind = Some("sequence"))
+ }
+ // create s
+ assetHelper.withCleaner(wsk.action, sName) {
+ (action, seqName) => action.create(seqName, Some(s"$echo,$xName,$echo"), kind = Some("sequence"))
+ }
+
+ // invoke s
+ val now = "it is now " + new Date()
+ val args = Array("what time is it?", now)
+ val argsJson = args.mkString("\n").toJson
+ val run = wsk.action.invoke(sName, Map("payload" -> argsJson))
+ println(s"RUN: ${run.stdout}")
+ withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) {
+ activation =>
+ checkSequenceLogsAndAnnotations(activation, 3) // 3 activations in this sequence
+ val result = activation.response.result.get
+ result.fields.get("payload") shouldBe Some(argsJson)
+ }
+ // update x with limit echo
+ val limit = whiskConfig.actionSequenceLimit.toInt
+ val manyEcho = for (i <- 1 to limit) yield echo
+
+ wsk.action.create(xName, Some(manyEcho.mkString(",")), kind = Some("sequence"), update = true)
+
+ val updateRun = wsk.action.invoke(sName, Map("payload" -> argsJson))
+ withActivation(wsk.activation, updateRun, totalWait = 2 * allowedActionDuration) {
+ activation =>
+ activation.response.status shouldBe ("application error")
+ checkSequenceLogsAndAnnotations(activation, 2)
+ val result = activation.response.result.get
+ result.fields.get("error") shouldBe Some(JsString(sequenceIsTooLong))
+ // check that inner sequence had only (limit - 1) activations
+ val innerSeq = activation.logs.get(1) // the id of the inner sequence activation
+ val getInnerSeq = wsk.activation.get(innerSeq)
+ withActivation(wsk.activation, getInnerSeq, totalWait = allowedActionDuration) {
+ innerSeqActivation =>
+ innerSeqActivation.logs.get.size shouldBe (limit - 1)
+ innerSeqActivation.cause shouldBe defined
+ innerSeqActivation.cause.get shouldBe (activation.activationId)
+ }
+ }
+ }
+
+ it should "invoke a blocking sequence action with an enclosing sequence action" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val inner_name = "inner_sequence"
+ val outer_name = "outer_sequence"
+ val inner_actions = Seq("sort", "head")
+ val actions = Seq("split") ++ inner_actions ++ Seq("cat")
+ // create atomic actions
+ for (actionName <- actions) {
+ val file = TestUtils.getTestActionFilename(s"$actionName.js")
+ assetHelper.withCleaner(wsk.action, actionName) { (action, _) =>
+ action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+ }
+ }
+
+ // create inner sequence
+ assetHelper.withCleaner(wsk.action, inner_name) {
+ val inner_sequence = inner_actions.mkString(",")
+ (action, _) => action.create(inner_name, Some(inner_sequence), kind = Some("sequence"))
+ }
+
+ // create outer sequence
+ assetHelper.withCleaner(wsk.action, outer_name) {
+ val outer_sequence = Seq("split", "inner_sequence", "cat").mkString(",")
+ (action, _) => action.create(outer_name, Some(outer_sequence), kind = Some("sequence"))
+ }
+
+ val now = "it is now " + new Date()
+ val args = Array("what time is it?", now)
+ val run = wsk.action.invoke(outer_name, Map("payload" -> args.mkString("\n").toJson))
+ withActivation(wsk.activation, run, totalWait = 4 * allowedActionDuration) {
+ activation =>
+ checkSequenceLogsAndAnnotations(activation, 3) // 3 activations in this sequence
+ activation.cause shouldBe None // topmost sequence
+ val result = activation.response.result.get
+ result.fields.get("payload") shouldBe defined
+ result.fields.get("length") should not be defined
+ result.fields.get("lines") shouldBe Some(JsArray(Vector(now.toJson)))
+ }
+ }
+
+ it should "create and run a sequence in a package with parameters" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val sName = "sSequence"
+
+ // create a package
+ val pkgName = "echopackage"
+ val pkgStr = "LonelyPackage"
+ assetHelper.withCleaner(wsk.pkg, pkgName) {
+ (pkg, name) => pkg.create(name, Map("payload" -> JsString(pkgStr)))
+ }
+ val helloName = "hello"
+ val helloWithPkg = s"$pkgName/$helloName"
+
+ // create hello action in package
+ val file = TestUtils.getTestActionFilename(s"$helloName.js")
+ val actionStr = "AtomicAction"
+ assetHelper.withCleaner(wsk.action, helloWithPkg) { (action, actionName) =>
+ action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration), parameters = Map("payload" -> JsString(actionStr)))
+ }
+ // create s
+ assetHelper.withCleaner(wsk.action, sName) {
+ (action, seqName) => action.create(seqName, Some(helloWithPkg), kind = Some("sequence"))
+ }
+ val run = wsk.action.invoke(sName)
+ // action params trump package params
+ checkLogsAtomicAction(0, run, new Regex(actionStr))
+ // run with some parameters
+ val sequenceStr = "AlmightySequence"
+ val sequenceParamRun = wsk.action.invoke(sName, parameters = Map("payload" -> JsString(sequenceStr)))
+ // sequence param should be passed to the first atomic action and trump the action params
+ checkLogsAtomicAction(0, sequenceParamRun, new Regex(sequenceStr))
+ // update action and remove the params by sending an unused param that overrides previous params
+ wsk.action.create(name = helloWithPkg, artifact = Some(file), timeout = Some(allowedActionDuration), parameters = Map("param" -> JsString("irrelevant")), update = true)
+ val sequenceParamSecondRun = wsk.action.invoke(sName, parameters = Map("payload" -> JsString(sequenceStr)))
+ // sequence param should be passed to the first atomic action and trump the package params
+ checkLogsAtomicAction(0, sequenceParamSecondRun, new Regex(sequenceStr))
+ val pkgParamRun = wsk.action.invoke(sName)
+ // no sequence params, no atomic action params used, the pkg params should show up
+ checkLogsAtomicAction(0, pkgParamRun, new Regex(pkgStr))
+ }
+
+ it should "run a sequence with an action in a package binding with parameters" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val packageName = "package1"
+ val bindName = "package2"
+ val actionName = "print"
+ val packageActionName = packageName + "/" + actionName
+ val bindActionName = bindName + "/" + actionName
+ val packageParams = Map("key1a" -> "value1a".toJson, "key1b" -> "value1b".toJson)
+ val bindParams = Map("key2a" -> "value2a".toJson, "key1b" -> "value2b".toJson)
+ val actionParams = Map("key0" -> "value0".toJson)
+ val file = TestUtils.getTestActionFilename("printParams.js")
+ assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) =>
+ pkg.create(packageName, packageParams)
+ }
+ assetHelper.withCleaner(wsk.action, packageActionName) { (action, _) =>
+ action.create(packageActionName, Some(file), parameters = actionParams)
+ }
+ assetHelper.withCleaner(wsk.pkg, bindName) { (pkg, _) =>
+ pkg.bind(packageName, bindName, bindParams)
+ }
+ // sequence
+ val sName = "sequenceWithBindingParams"
+ assetHelper.withCleaner(wsk.action, sName) {
+ (action, seqName) => action.create(seqName, Some(bindActionName), kind = Some("sequence"))
+ }
+ // Check that inherited parameters are passed to the action.
+ val now = new Date().toString()
+ val run = wsk.action.invoke(sName, Map("payload" -> now.toJson))
+ // action params trump package params
+ checkLogsAtomicAction(0, run, new Regex(String.format(".*key0: value0.*key1a: value1a.*key1b: value2b.*key2a: value2a.*payload: %s", now)))
+ }
+ /**
+ * s -> apperror, echo
+ * only apperror should run
+ */
+ it should "stop execution of a sequence (with no payload) on error" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val sName = "sSequence"
+ val apperror = "applicationError"
+ val echo = "echo"
+
+ // create actions
+ val actions = Seq(apperror, echo)
+ for (actionName <- actions) {
+ val file = TestUtils.getTestActionFilename(s"$actionName.js")
+ assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>
+ action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+ }
+ }
+ // create sequence s
+ assetHelper.withCleaner(wsk.action, sName) {
+ (action, seqName) => action.create(seqName, artifact = Some(actions.mkString(",")), kind = Some("sequence"))
+ }
+ // run sequence s with no payload
+ val run = wsk.action.invoke(sName)
+ withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) {
+ activation =>
+ checkSequenceLogsAndAnnotations(activation, 1) // only the first action should have run
+ activation.response.success shouldBe (false)
+ // the status should be error
+ activation.response.status shouldBe ("application error")
+ val result = activation.response.result.get
+ // the result of the activation should be the application error
+ result shouldBe (JsObject("error" -> JsString("This error thrown on purpose by the action.")))
+ }
+ }
+
+ /**
+ * s -> echo, initforever
+ * should run both, but error
+ */
+ it should "propagate execution error (timeout) from atomic action to sequence" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val sName = "sSequence"
+ val initforever = "initforever"
+ val echo = "echo"
+
+ // create actions
+ val actions = Seq(echo, initforever)
+ // timeouts for the action; make the one for initforever short
+ val timeout = Map(echo -> allowedActionDuration, initforever -> shortDuration)
+ for (actionName <- actions) {
+ val file = TestUtils.getTestActionFilename(s"$actionName.js")
+ assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>
+ action.create(name = actionName, artifact = Some(file), timeout = Some(timeout(actionName)))
+ }
+ }
+ // create sequence s
+ assetHelper.withCleaner(wsk.action, sName) {
+ (action, seqName) => action.create(seqName, artifact = Some(actions.mkString(",")), kind = Some("sequence"))
+ }
+ // run sequence s with no payload
+ val run = wsk.action.invoke(sName)
+ withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) {
+ activation =>
+ checkSequenceLogsAndAnnotations(activation, 2) // 2 actions
+ activation.response.success shouldBe (false)
+ // the status should be error
+ //activation.response.status shouldBe("application error")
+ val result = activation.response.result.get
+ // the result of the activation should be timeout
+ result shouldBe (JsObject("error" -> JsString("The action exceeded its time limits of 10000 milliseconds during initialization.")))
+ }
+ }
+
+ /**
+ * s -> echo, sleep
+ * sleep sleeps for 90s, timeout set at 120s
+ * should run both, the blocking call should be transformed into a non-blocking call, but finish executing
+ */
+ it should "execute a sequence in blocking fashion and finish execution even if longer than blocking response timeout" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val sName = "sSequence"
+ val sleep = "timeout"
+ val echo = "echo"
+
+ // create actions
+ val actions = Seq(echo, sleep)
+ for (actionName <- actions) {
+ val file = TestUtils.getTestActionFilename(s"$actionName.js")
+ assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>
+ action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+ }
+ }
+ // create sequence s
+ assetHelper.withCleaner(wsk.action, sName) {
+ (action, seqName) => action.create(seqName, artifact = Some(actions.mkString(",")), kind = Some("sequence"))
+ }
+ // run sequence s with sleep equal to payload
+ val payload = 65000
+ val run = wsk.action.invoke(sName, parameters = Map("payload" -> JsNumber(payload)), blocking = true)
+ withActivation(wsk.activation, run, initialWait = 5 seconds, totalWait = 3 * allowedActionDuration) {
+ activation =>
+ checkSequenceLogsAndAnnotations(activation, 2) // 2 actions
+ activation.response.success shouldBe (true)
+ // the status should be error
+ //activation.response.status shouldBe("application error")
+ val result = activation.response.result.get
+ // the result of the activation should be timeout
+ result shouldBe (JsObject("msg" -> JsString(s"[OK] message terminated successfully after $payload milliseconds.")))
+ }
+ }
+
+ /**
+ * sequence s -> echo
+ * t trigger with payload
+ * rule r: t -> s
+ */
+ it should "execute a sequence that is part of a rule and pass the trigger parameters to the sequence" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val seqName = "seqRule"
+ val actionName = "echo"
+ val triggerName = "trigSeq"
+ val ruleName = "ruleSeq"
+
+ val itIsNow = "it is now " + new Date()
+ // set up all entities
+ // trigger
+ val triggerPayload: Map[String, JsValue] = Map("payload" -> JsString(itIsNow))
+ assetHelper.withCleaner(wsk.trigger, triggerName) {
+ (trigger, name) => trigger.create(name, parameters = triggerPayload)
+ }
+ // action
+ val file = TestUtils.getTestActionFilename(s"$actionName.js")
+ assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) =>
+ action.create(name = actionName, artifact = Some(file), timeout = Some(allowedActionDuration))
+ }
+ // sequence
+ assetHelper.withCleaner(wsk.action, seqName) {
+ (action, seqName) => action.create(seqName, artifact = Some(actionName), kind = Some("sequence"))
+ }
+ // rule
+ assetHelper.withCleaner(wsk.rule, ruleName) {
+ (rule, name) => rule.create(name, triggerName, seqName)
+ }
+ // fire trigger
+ val run = wsk.trigger.fire(triggerName)
+ // check that the sequence was invoked and that the echo action produced the expected result
+ checkEchoSeqRuleResult(run, seqName, JsObject(triggerPayload))
+ // fire trigger with new payload
+ val now = "this is now: " + Instant.now
+ val newPayload = Map("payload" -> JsString(now))
+ val newRun = wsk.trigger.fire(triggerName, newPayload)
+ checkEchoSeqRuleResult(newRun, seqName, JsObject(newPayload))
+ }
+
+ /**
+ * checks the result of an echo sequence connected to a trigger through a rule
+ * @param triggerFireRun the run result of firing the trigger
+ * @param seqName the sequence name
+ * @param triggerPayload the payload used for the trigger (that should be reflected in the sequence result)
+ */
+ private def checkEchoSeqRuleResult(triggerFireRun: RunResult, seqName: String, triggerPayload: JsObject) = {
+ withActivation(wsk.activation, triggerFireRun) {
+ triggerActivation =>
+ withActivationsFromEntity(wsk.activation, seqName, since = Some(triggerActivation.start)) { activationList =>
+ activationList.head.response.result shouldBe Some(triggerPayload)
+ activationList.head.cause shouldBe None
+ }
+ }
+ }
+
+ /**
+ * checks logs for the activation of a sequence (length/size and ids)
+ * checks that the cause field for composing atomic actions is set properly
+ * checks duration
+ * checks memory
+ */
+ private def checkSequenceLogsAndAnnotations(activation: CliActivation, size: Int) = {
+ activation.logs shouldBe defined
+ // check that the logs are what they are supposed to be (activation ids)
+ // check that the cause field is properly set for these activations
+ activation.logs.get.size shouldBe (size) // the number of activations in this sequence
+ var totalTime: Long = 0
+ var maxMemory: Long = 0
+ for (id <- activation.logs.get) {
+ withActivation(wsk.activation, id, initialWait = 1 second, pollPeriod = 60 seconds, totalWait = allowedActionDuration) {
+ componentActivation =>
+ componentActivation.cause shouldBe defined
+ componentActivation.cause.get shouldBe (activation.activationId)
+ // check causedBy
+ val causedBy = componentActivation.getAnnotationValue("causedBy")
+ causedBy shouldBe defined
+ causedBy.get shouldBe (JsString("sequence"))
+ totalTime += componentActivation.duration
+ // extract memory
+ val mem = extractMemoryAnnotation(componentActivation)
+ maxMemory = maxMemory max mem
+ }
+ }
+ // extract duration
+ activation.duration shouldBe (totalTime)
+ // extract memory
+ activation.annotations shouldBe defined
+ val memory = extractMemoryAnnotation(activation)
+ memory shouldBe (maxMemory)
+ }
+
+ /** checks that the logs of the idx-th atomic action from a sequence contains logsStr */
+ private def checkLogsAtomicAction(atomicActionIdx: Int, run: RunResult, regex: Regex) {
+ withActivation(wsk.activation, run, totalWait = 2 * allowedActionDuration) { activation =>
+ checkSequenceLogsAndAnnotations(activation, 1)
+ val componentId = activation.logs.get(atomicActionIdx)
+ val getComponentActivation = wsk.activation.get(componentId)
+ withActivation(wsk.activation, getComponentActivation, totalWait = allowedActionDuration) { componentActivation =>
+ println(componentActivation)
+ componentActivation.logs shouldBe defined
+ val logs = componentActivation.logs.get.mkString(" ")
+ regex.findFirstIn(logs) shouldBe defined
+ }
+ }
+ }
+
+ private def extractMemoryAnnotation(activation: CliActivation): Long = {
+ val limits = activation.getAnnotationValue("limits")
+ limits shouldBe defined
+ limits.get.asJsObject.getFields("memory")(0).convertTo[Long]
+ }
+}
diff --git a/tests/src/test/scala/whisk/core/admin/WskAdminTests.scala b/tests/src/test/scala/whisk/core/admin/WskAdminTests.scala
new file mode 100644
index 0000000..4dfdc79
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/admin/WskAdminTests.scala
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015-2016 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 whisk.core.admin
+
+import scala.concurrent.duration.DurationInt
+
+import org.junit.runner.RunWith
+import org.scalatest.Matchers
+import org.scalatest.junit.JUnitRunner
+
+import common.RunWskAdminCmd
+import common.TestHelpers
+import common.WskAdmin
+import whisk.core.entity.AuthKey
+import whisk.core.entity.Subject
+import whisk.core.entity.WhiskAuth
+
+@RunWith(classOf[JUnitRunner])
+class WskAdminTests
+ extends TestHelpers
+ with Matchers {
+
+ behavior of "Wsk Admin CLI"
+
+ it should "confirm wskadmin exists" in {
+ WskAdmin.exists
+ }
+
+ it should "CRD a subject" in {
+ val wskadmin = new RunWskAdminCmd {}
+ val auth = WhiskAuth(Subject(), AuthKey())
+ val subject = auth.subject.asString
+ try {
+ println(s"CRD subject: $subject")
+ val create = wskadmin.cli(Seq("user", "create", subject))
+ val get = wskadmin.cli(Seq("user", "get", subject))
+ create.stdout should be(get.stdout)
+
+ val authkey = get.stdout.trim
+ authkey should include(":")
+ authkey.split(":")(0).length should be(36)
+ authkey.split(":")(1).length should be >= 64
+
+ wskadmin.cli(Seq("user", "whois", authkey)).stdout.trim should be(Seq(s"subject: $subject", s"namespace: $subject").mkString("\n"))
+
+ whisk.utils.retry({
+ // reverse lookup by namespace
+ wskadmin.cli(Seq("user", "list", "-k", subject)).stdout.trim should be(authkey)
+ }, 10, Some(1.second))
+
+ wskadmin.cli(Seq("user", "delete", subject)).stdout should include("Subject deleted")
+
+ // recreate with explicit
+ val newspace = s"${subject}.myspace"
+ wskadmin.cli(Seq("user", "create", subject, "-ns", newspace, "-u", auth.authkey.compact))
+
+ whisk.utils.retry({
+ // reverse lookup by namespace
+ wskadmin.cli(Seq("user", "list", "-k", newspace)).stdout.trim should be(auth.authkey.compact)
+ }, 10, Some(1.second))
+
+ wskadmin.cli(Seq("user", "get", subject, "-ns", newspace)).stdout.trim should be(auth.authkey.compact)
+
+ // delete namespace
+ wskadmin.cli(Seq("user", "delete", subject, "-ns", newspace)).stdout should include("Namespace deleted")
+ } finally {
+ wskadmin.cli(Seq("user", "delete", subject)).stdout should include("Subject deleted")
+ }
+ }
+
+}
diff --git a/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala b/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala
new file mode 100644
index 0000000..d289d7a
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala
@@ -0,0 +1,332 @@
+/*
+ * Copyright 2015-2016 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 whisk.core.apigw.actions.test
+
+import org.junit.runner.RunWith
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.junit.JUnitRunner
+
+import common.JsHelpers
+import common.StreamLogging
+import common.TestHelpers
+import common.TestUtils.ANY_ERROR_EXIT
+import common.TestUtils.DONTCARE_EXIT
+import common.TestUtils.RunResult
+import common.TestUtils.SUCCESS_EXIT
+import common.Wsk
+import common.WskActorSystem
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+
+case class ApiAction(
+ name: String,
+ namespace: String,
+ backendMethod: String = "POST",
+ backendUrl: String,
+ authkey: String) {
+ def toJson(): JsObject = {
+ return JsObject(
+ "name" -> name.toJson,
+ "namespace" -> namespace.toJson,
+ "backendMethod" -> backendMethod.toJson,
+ "backendUrl" -> backendUrl.toJson,
+ "authkey" -> authkey.toJson)
+ }
+}
+
+/**
+ * Tests for basic CLI usage. Some of these tests require a deployed backend.
+ */
+@RunWith(classOf[JUnitRunner])
+class ApiGwRoutemgmtActionTests
+ extends TestHelpers
+ with BeforeAndAfterAll
+ with WskActorSystem
+ with WskTestHelpers
+ with JsHelpers
+ with StreamLogging {
+
+ val systemId = "whisk.system"
+ implicit val wskprops = WskProps(authKey = WskAdmin.listKeys(systemId)(0)._1, namespace = systemId)
+ val wsk = new Wsk
+
+ def getApis(
+ bpOrName: Option[String],
+ relpath: Option[String] = None,
+ operation: Option[String] = None,
+ docid: Option[String] = None): Vector[JsValue] = {
+ val parms = Map[String, JsValue]() ++
+ Map("__ow_meta_namespace" -> wskprops.namespace.toJson) ++
+ { bpOrName map { b => Map("basepath" -> b.toJson) } getOrElse Map[String, JsValue]() } ++
+ { relpath map { r => Map("relpath" -> r.toJson) } getOrElse Map[String, JsValue]() } ++
+ { operation map { o => Map("operation" -> o.toJson) } getOrElse Map[String, JsValue]() } ++
+ { docid map { d => Map("docid" -> d.toJson) } getOrElse Map[String, JsValue]() }
+
+ val rr = wsk.action.invoke(
+ name = "routemgmt/getApi",
+ parameters = parms,
+ blocking = true,
+ result = true,
+ expectedExitCode = SUCCESS_EXIT)(wskprops)
+ var apiJsArray: JsArray =
+ try {
+ var apisobj = rr.stdout.parseJson.asJsObject.fields("apis")
+ apisobj.convertTo[JsArray]
+ } catch {
+ case e: Exception =>
+ JsArray.empty
+ }
+ return apiJsArray.elements
+ }
+
+ def createApi(
+ namespace: Option[String] = Some("_"),
+ basepath: Option[String] = Some("/"),
+ relpath: Option[String],
+ operation: Option[String],
+ apiname: Option[String],
+ action: Option[ApiAction],
+ swagger: Option[String] = None,
+ expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+ val parms = Map[String, JsValue]() ++
+ { namespace map { n => Map("namespace" -> n.toJson) } getOrElse Map[String, JsValue]() } ++
+ { basepath map { b => Map("gatewayBasePath" -> b.toJson) } getOrElse Map[String, JsValue]() } ++
+ { relpath map { r => Map("gatewayPath" -> r.toJson) } getOrElse Map[String, JsValue]() } ++
+ { operation map { o => Map("gatewayMethod" -> o.toJson) } getOrElse Map[String, JsValue]() } ++
+ { apiname map { an => Map("apiName" -> an.toJson) } getOrElse Map[String, JsValue]() } ++
+ { action map { a => Map("action" -> a.toJson) } getOrElse Map[String, JsValue]() } ++
+ { swagger map { s => Map("swagger" -> s.toJson) } getOrElse Map[String, JsValue]() }
+ val parm = Map[String, JsValue]("apidoc" -> JsObject(parms)) ++
+ { namespace map { n => Map("__ow_meta_namespace" -> n.toJson) } getOrElse Map[String, JsValue]() }
+
+ val rr = wsk.action.invoke(
+ name = "routemgmt/createApi",
+ parameters = parm,
+ blocking = true,
+ result = true,
+ expectedExitCode = expectedExitCode)(wskprops)
+ return rr
+ }
+
+ def deleteApi(
+ namespace: Option[String] = Some("_"),
+ basepath: Option[String] = Some("/"),
+ relpath: Option[String] = None,
+ operation: Option[String] = None,
+ apiname: Option[String] = None,
+ expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+ val parms = Map[String, JsValue]() ++
+ { namespace map { n => Map("__ow_meta_namespace" -> n.toJson) } getOrElse Map[String, JsValue]() } ++
+ { basepath map { b => Map("basepath" -> b.toJson) } getOrElse Map[String, JsValue]() } ++
+ { relpath map { r => Map("relpath" -> r.toJson) } getOrElse Map[String, JsValue]() } ++
+ { operation map { o => Map("operation" -> o.toJson) } getOrElse Map[String, JsValue]() } ++
+ { apiname map { an => Map("apiname" -> an.toJson) } getOrElse Map[String, JsValue]() }
+
+ val rr = wsk.action.invoke(
+ name = "routemgmt/deleteApi",
+ parameters = parms,
+ blocking = true,
+ result = true,
+ expectedExitCode = expectedExitCode)(wskprops)
+ return rr
+ }
+
+ def apiMatch(
+ apiarr: Vector[JsValue],
+ basepath: String = "/",
+ relpath: String = "",
+ operation: String = "",
+ apiname: String = "",
+ action: ApiAction = null): Boolean = {
+ var matches: Boolean = false
+ for (api <- apiarr) {
+ val basepathExists = JsObjectHelper(api.asJsObject).fieldPathExists("value", "apidoc", "basePath")
+ if (basepathExists) {
+ System.out.println("basePath exists")
+ val basepathMatches = (JsObjectHelper(api.asJsObject).getFieldPath("value", "apidoc", "basePath").get.convertTo[String] == basepath)
+ if (basepathMatches) {
+ System.out.println("basePath matches: " + basepath)
+ val apinameExists = JsObjectHelper(api.asJsObject).fieldPathExists("value", "apidoc", "info", "title")
+ if (apinameExists) {
+ System.out.println("api name exists")
+ val apinameMatches = (JsObjectHelper(api.asJsObject).getFieldPath("value", "apidoc", "info", "title").get.convertTo[String] == apiname)
+ if (apinameMatches) {
+ System.out.println("api name matches: " + apiname)
+ val endpointMatches = JsObjectHelper(api.asJsObject).fieldPathExists("value", "apidoc", "paths", relpath, operation)
+ if (endpointMatches) {
+ System.out.println("endpoint exists/matches : " + relpath + " " + operation)
+ val actionConfig = JsObjectHelper(api.asJsObject).getFieldPath("value", "apidoc", "paths", relpath, operation, "x-ibm-op-ext").get.asJsObject
+ val actionMatches = actionMatch(actionConfig, action)
+ if (actionMatches) {
+ System.out.println("endpoint action matches")
+ matches = true;
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ return matches
+ }
+
+ def actionMatch(
+ jsAction: JsObject,
+ action: ApiAction): Boolean = {
+ val matches = jsAction.fields("backendMethod").convertTo[String] == action.backendMethod &&
+ jsAction.fields("backendUrl").convertTo[String] == action.backendUrl &&
+ jsAction.fields("actionNamespace").convertTo[String] == action.namespace &&
+ jsAction.fields("actionName").convertTo[String] == action.name
+ return matches
+ }
+
+ behavior of "API Gateway routemgmt action parameter validation"
+
+ it should "verify successful creation of a new API" in {
+ val testName = "APIGWTEST1"
+ val testbasepath = "/" + testName + "_bp"
+ val testrelpath = "/path"
+ val testurlop = "get"
+ val testapiname = testName + " API Name"
+ val actionName = testName + "_action"
+ val actionNamespace = wskprops.namespace
+ val actionUrl = "http://some.whisk.host/api/v1/namespaces/" + actionNamespace + "/actions/" + actionName
+ val actionAuthKey = testName + "_authkey"
+ val testaction = ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)
+
+ try {
+ val createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
+ operation = Some(testurlop), apiname = Some(testapiname), action = Some(testaction))
+ JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists("apidoc") should be(true)
+ val apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+ apiVector.size should be > 0
+ apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)
+ } finally {
+ val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify successful API deletion using basepath" in {
+ val testName = "APIGWTEST2"
+ val testbasepath = "/" + testName + "_bp"
+ val testrelpath = "/path"
+ val testurlop = "get"
+ val testapiname = testName + " API Name"
+ val actionName = testName + "_action"
+ val actionNamespace = wskprops.namespace
+ val actionUrl = "http://some.whisk.host/api/v1/namespaces/" + actionNamespace + "/actions/" + actionName
+ val actionAuthKey = testName + "_authkey"
+ val testaction = ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)
+
+ try {
+ val createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
+ operation = Some(testurlop), apiname = Some(testapiname), action = Some(testaction))
+ JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists("apidoc") should be(true)
+ var apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+ apiVector.size should be > 0
+ apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)
+ val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath))
+ apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+ apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(false)
+ } finally {
+ val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify successful addition of new relative path to existing API" in {
+ val testName = "APIGWTEST3"
+ val testbasepath = "/" + testName + "_bp"
+ val testrelpath = "/path"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testnewurlop = "delete"
+ val testapiname = testName + " API Name"
+ val actionName = testName + "_action"
+ val actionNamespace = wskprops.namespace
+ val actionUrl = "http://some.whisk.host/api/v1/namespaces/" + actionNamespace + "/actions/" + actionName
+ val actionAuthKey = testName + "_authkey"
+ val testaction = ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)
+
+ try {
+ var createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
+ operation = Some(testurlop), apiname = Some(testapiname), action = Some(testaction))
+ createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testnewrelpath),
+ operation = Some(testnewurlop), apiname = Some(testapiname), action = Some(testaction))
+ JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists("apidoc") should be(true)
+ var apiVector = getApis(bpOrName = Some(testbasepath))
+ apiVector.size should be > 0
+ apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)
+ apiMatch(apiVector, testbasepath, testnewrelpath, testnewurlop, testapiname, testaction) should be(true)
+ } finally {
+ val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "reject routemgmt actions that are invoked with not enough parameters" in {
+ val invalidArgs = Seq(
+ //getApi
+ ("/whisk.system/routemgmt/getApi", ANY_ERROR_EXIT, "namespace is required", Seq()),
+
+ //deleteApi
+ ("/whisk.system/routemgmt/deleteApi", ANY_ERROR_EXIT, "namespace is required", Seq("-p", "basepath", "/ApiGwRoutemgmtActionTests_bp")),
+ ("/whisk.system/routemgmt/deleteApi", ANY_ERROR_EXIT, "basepath is required", Seq("-p", "__ow_meta_namespace", "_")),
+ ("/whisk.system/routemgmt/deleteApi", ANY_ERROR_EXIT, "When specifying an operation, the relpath is required",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "basepath", "/ApiGwRoutemgmtActionTests_bp", "-p", "operation", "get")),
+
+ //createApi
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is required", Seq("-p", "__ow_meta_namespace", "_")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is missing the namespace field",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", "{}")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is missing the gatewayBasePath field",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_"}""")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is missing the gatewayPath field",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp"}""")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is missing the gatewayMethod field",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp"}""")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc is missing the action field",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get"}""")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "action is missing the backendMethod field",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{}}""")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "action is missing the backendUrl field",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post"}}""")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "action is missing the namespace field",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL"}}""")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "action is missing the name field",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_"}}""")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "action is missing the authkey field",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_","name":"N"}}""")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "swagger and gatewayBasePath are mutually exclusive and cannot be specified together",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_","name":"N","authkey":"XXXX"},"swagger":{}}""")),
+ ("/whisk.system/routemgmt/createApi", ANY_ERROR_EXIT, "apidoc field cannot be parsed. Ensure it is valid JSON",
+ Seq("-p", "__ow_meta_namespace", "_", "-p", "apidoc", "{1:[}}}")))
+
+ invalidArgs foreach {
+ case (action: String, exitcode: Int, errmsg: String, params: Seq[String]) =>
+ val cmd: Seq[String] = Seq("action",
+ "invoke",
+ action,
+ "-i", "-b", "-r",
+ "--apihost", wskprops.apihost,
+ "--auth", wskprops.authKey) ++ params
+ val rr = wsk.cli(cmd, expectedExitCode = exitcode)
+ rr.stderr should include regex (errmsg)
+ }
+ }
+}
diff --git a/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala b/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala
new file mode 100644
index 0000000..eae638e
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala
@@ -0,0 +1,511 @@
+/*
+ * Copyright 2015-2016 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 whisk.core.cli.test
+
+import java.time.Instant
+import scala.concurrent.duration._
+import org.junit.runner.RunWith
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.junit.JUnitRunner
+import common.TestHelpers
+import common.TestUtils._
+import common.TestUtils
+import common.WhiskProperties
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+
+/**
+ * Tests for testing the CLI "api" subcommand. Most of these tests require a deployed backend.
+ */
+@RunWith(classOf[JUnitRunner])
+class ApiGwTests
+ extends TestHelpers
+ with WskTestHelpers
+ with BeforeAndAfterAll {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val (cliuser, clinamespace) = WskAdmin.getUser(wskprops.authKey)
+
+ // This test suite makes enough CLI invocations in 60 seconds to trigger the OpenWhisk
+ // throttling restriction. To avoid CLI failures due to being throttled, track the
+ // CLI invocation calls and when at the throttle limit, pause the next CLI invocation
+ // with exactly enough time to relax the throttling.
+ val throttleWindow = 1.minute
+ var cliCallCount = 5 // Set to >0 to allow for other action invocations in prior tests
+ var clearedThrottleTime = Instant.now
+ val maxActionsPerMin = WhiskProperties.getMaxActionInvokesPerMinute()
+
+ /**
+ * Expected to be called before or after each CLI invocation
+ * If number of CLI invocations in this suite have reached the throttle limit
+ * then pause the test for enough time so that the throttle restriction is gone
+ */
+ def checkThrottle() = {
+ // If the # CLI calls at the throttle limit, then wait enough time to avoid the CLI being blocked
+ cliCallCount += 1
+ if ( cliCallCount > maxActionsPerMin ) {
+ println(s"Action invokes ${cliCallCount} exceeds per minute thottle limit of ${maxActionsPerMin}")
+ val waitedAlready = Duration.fromNanos(java.time.Duration.between(clearedThrottleTime, Instant.now).toNanos)
+ settleThrottle(waitedAlready)
+ cliCallCount = 0
+ clearedThrottleTime = Instant.now
+ }
+ }
+
+ /**
+ * Settles throttles of 1 minute. Waits up to 1 minute depending on the time already waited.
+ *
+ * @param waitedAlready the time already gone after the last invoke or fire
+ */
+ def settleThrottle(waitedAlready: FiniteDuration) = {
+ val timeToWait = (throttleWindow - waitedAlready).max(Duration.Zero)
+ println(s"Waiting for ${timeToWait.toSeconds} seconds, already waited for ${waitedAlready.toSeconds} seconds")
+ Thread.sleep(timeToWait.toMillis)
+ }
+
+ /*
+ * Forcibly clear the throttle so that downstream tests are not affected by
+ * this test suite
+ */
+ override def afterAll() = {
+ // If this test suite is exiting with over 30 action invocations since the last throttle clearing, clear the throttle
+ if (cliCallCount > 30) {
+ val waitedAlready = Duration.fromNanos(java.time.Duration.between(clearedThrottleTime, Instant.now).toNanos)
+ settleThrottle(waitedAlready)
+ }
+ }
+
+ def apiCreate(
+ basepath: Option[String] = None,
+ relpath: Option[String] = None,
+ operation: Option[String] = None,
+ action: Option[String] = None,
+ apiname: Option[String] = None,
+ swagger: Option[String] = None,
+ expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+ checkThrottle()
+ wsk.api.create(basepath, relpath, operation, action, apiname, swagger, expectedExitCode)
+ }
+
+ def apiList(
+ basepathOrApiName: Option[String] = None,
+ relpath: Option[String] = None,
+ operation: Option[String] = None,
+ limit: Option[Int] = None,
+ since: Option[Instant] = None,
+ full: Option[Boolean] = None,
+ expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+ checkThrottle()
+ wsk.api.list(basepathOrApiName, relpath, operation, limit, since, full, expectedExitCode)
+ }
+
+ def apiGet(
+ basepathOrApiName: Option[String] = None,
+ full: Option[Boolean] = None,
+ expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+ checkThrottle()
+ wsk.api.get(basepathOrApiName, full, expectedExitCode)
+ }
+
+ def apiDelete(
+ basepathOrApiName: String,
+ relpath: Option[String] = None,
+ operation: Option[String] = None,
+ expectedExitCode: Int = SUCCESS_EXIT): RunResult = {
+ checkThrottle()
+ wsk.api.delete(basepathOrApiName, relpath, operation, expectedExitCode)
+ }
+
+ behavior of "Wsk api"
+
+ it should "reject an api commands with an invalid path parameter" in {
+ val badpath = "badpath"
+
+ var rr = apiCreate(basepath = Some("/basepath"), relpath = Some(badpath), operation = Some("GET"), action = Some("action"), expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include (s"'${badpath}' must begin with '/'")
+
+ rr = apiDelete(basepathOrApiName = "/basepath", relpath = Some(badpath), operation = Some("GET"), expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include (s"'${badpath}' must begin with '/'")
+
+ rr = apiList(basepathOrApiName = Some("/basepath"), relpath = Some(badpath), operation = Some("GET"), expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include (s"'${badpath}' must begin with '/'")
+ }
+
+ it should "verify full list output" in {
+ val testName = "CLI_APIGWTEST_RO1"
+ val testbasepath = "/" + testName + "_bp"
+ val testrelpath = "/path"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName + " API Name"
+ val actionName = testName + "_action"
+ try {
+ println("cli user: " + cliuser + "; cli namespace: " + clinamespace)
+
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ println("api create: " + rr.stdout)
+ rr.stdout should include("ok: created API")
+ rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), full = Some(true))
+ println("api list: " + rr.stdout)
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"Action:\\s+/${clinamespace}/${actionName}\n")
+ rr.stdout should include regex (s"Verb:\\s+${testurlop}\n")
+ rr.stdout should include regex (s"Base path:\\s+${testbasepath}\n")
+ rr.stdout should include regex (s"Path:\\s+${testrelpath}\n")
+ rr.stdout should include regex (s"API Name:\\s+${testapiname}\n")
+ rr.stdout should include regex (s"URL:\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+ }
+ finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath)
+ }
+ }
+
+ it should "verify successful creation and deletion of a new API" in {
+ val testName = "CLI_APIGWTEST1"
+ val testbasepath = "/"+testName+"_bp"
+ val testrelpath = "/path"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName+" API Name"
+ val actionName = testName+"_action"
+ try {
+ println("cli user: "+cliuser+"; cli namespace: "+clinamespace)
+
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath)
+ deleteresult.stdout should include("ok: deleted API")
+ }
+ finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify get API name " in {
+ val testName = "CLI_APIGWTEST3"
+ val testbasepath = "/"+testName+"_bp"
+ val testrelpath = "/path"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName+" API Name"
+ val actionName = testName+"_action"
+ try {
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiGet(basepathOrApiName = Some(testapiname))
+ rr.stdout should include(testbasepath)
+ rr.stdout should include(s"${actionName}")
+ }
+ finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify delete API name " in {
+ val testName = "CLI_APIGWTEST4"
+ val testbasepath = "/"+testName+"_bp"
+ val testrelpath = "/path"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName+" API Name"
+ val actionName = testName+"_action"
+ try {
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiDelete(basepathOrApiName = testapiname)
+ rr.stdout should include("ok: deleted API")
+ }
+ finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify delete API basepath " in {
+ val testName = "CLI_APIGWTEST5"
+ val testbasepath = "/"+testName+"_bp"
+ val testrelpath = "/path"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName+" API Name"
+ val actionName = testName+"_action"
+ try {
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiDelete(basepathOrApiName = testbasepath)
+ rr.stdout should include("ok: deleted API")
+ }
+ finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify adding endpoints to existing api" in {
+ val testName = "CLI_APIGWTEST6"
+ val testbasepath = "/"+testName+"_bp"
+ val testrelpath = "/path2"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName+" API Name"
+ val actionName = testName+"_action"
+ val newEndpoint = "/newEndpoint"
+ try {
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiCreate(basepath = Some(testbasepath), relpath = Some(newEndpoint), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiList(basepathOrApiName = Some(testbasepath))
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+ rr.stdout should include(testbasepath + newEndpoint)
+ }
+ finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify successful creation with swagger doc as input" in {
+ // NOTE: These values must match the swagger file contents
+ val testName = "CLI_APIGWTEST7"
+ val testbasepath = "/"+testName+"_bp"
+ val testrelpath = "/path"
+ val testurlop = "get"
+ val testapiname = testName+" API Name"
+ val actionName = testName+"_action"
+ val swaggerPath = TestUtils.getTestApiGwFilename("testswaggerdoc1")
+ try {
+ var rr = apiCreate(swagger = Some(swaggerPath))
+ rr.stdout should include("ok: created API")
+ rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+ println("list stdout: "+rr.stdout)
+ println("list stderr: "+rr.stderr)
+ rr.stdout should include("ok: APIs")
+ // Actual CLI namespace will vary from local dev to automated test environments, so don't check
+ rr.stdout should include regex (s"/[@\\w._\\-]+/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+ }
+ finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify adding endpoints to two existing apis" in {
+ val testName = "CLI_APIGWTEST8"
+ val testbasepath = "/"+testName+"_bp"
+ val testbasepath2 = "/"+testName+"_bp2"
+ val testrelpath = "/path2"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName+" API Name"
+ val actionName = testName+"_action"
+ val newEndpoint = "/newEndpoint"
+ try {
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiCreate(basepath = Some(testbasepath2), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+
+ // Update both APIs - each with a new endpoint
+ rr = apiCreate(basepath = Some(testbasepath), relpath = Some(newEndpoint), operation = Some(testurlop), action = Some(actionName))
+ rr.stdout should include("ok: created API")
+ rr = apiCreate(basepath = Some(testbasepath2), relpath = Some(newEndpoint), operation = Some(testurlop), action = Some(actionName))
+ rr.stdout should include("ok: created API")
+
+ rr = apiList(basepathOrApiName = Some(testbasepath))
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+ rr.stdout should include(testbasepath + newEndpoint)
+
+ rr = apiList(basepathOrApiName = Some(testbasepath2))
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath2 + testrelpath)
+ rr.stdout should include(testbasepath2 + newEndpoint)
+ }
+ finally {
+ var deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ deleteresult = apiDelete(basepathOrApiName = testbasepath2, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify successful creation of a new API using an action name using all allowed characters" in {
+ // Be aware: full action name is close to being truncated by the 'list' command
+ // e.g. /lime@us.ibm.com/CLI_APIGWTEST9a-c@t ion is currently at the 40 char 'list' display max
+ val testName = "CLI_APIGWTEST9"
+ val testbasepath = "/" + testName + "_bp"
+ val testrelpath = "/path"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName+" API Name"
+ val actionName = testName+"a-c@t ion"
+ try {
+ println("cli user: "+cliuser+"; cli namespace: "+clinamespace)
+
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath)
+ deleteresult.stdout should include("ok: deleted API")
+ }
+ finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify failed creation with invalid swagger doc as input" in {
+ val testName = "CLI_APIGWTEST10"
+ val testbasepath = "/" + testName + "_bp"
+ val testrelpath = "/path"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName + " API Name"
+ val actionName = testName + "_action"
+ val swaggerPath = TestUtils.getTestApiGwFilename(s"testswaggerdocinvalid")
+ try {
+ var rr = apiCreate(swagger = Some(swaggerPath), expectedExitCode = ANY_ERROR_EXIT)
+ println("api create stdout: " + rr.stdout)
+ println("api create stderr: " + rr.stderr)
+ rr.stderr should include(s"Swagger file is invalid")
+ } finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify delete basepath/path " in {
+ val testName = "CLI_APIGWTEST11"
+ val testbasepath = "/" + testName + "_bp"
+ val testrelpath = "/path"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName + " API Name"
+ val actionName = testName + "_action"
+ try {
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ var rr2 = apiCreate(basepath = Some(testbasepath), relpath = Some(testnewrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr2.stdout should include("ok: created API")
+ rr = apiDelete(basepathOrApiName = testbasepath, relpath = Some(testrelpath))
+ rr.stdout should include("ok: deleted " + testrelpath +" from "+ testbasepath)
+ rr2 = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testnewrelpath))
+ rr2.stdout should include("ok: APIs")
+ rr2.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr2.stdout should include(testbasepath + testnewrelpath)
+ } finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify delete single operation from existing API basepath/path/operation(s) " in {
+ val testName = "CLI_APIGWTEST12"
+ val testbasepath = "/" + testName + "_bp"
+ val testrelpath = "/path2"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testurlop2 = "post"
+ val testapiname = testName + " API Name"
+ val actionName = testName + "_action"
+ try {
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop2), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiList(basepathOrApiName = Some(testbasepath))
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+ rr = apiDelete(basepathOrApiName = testbasepath,relpath = Some(testrelpath), operation = Some(testurlop2))
+ rr.stdout should include("ok: deleted " + testrelpath + " " + "POST" +" from "+ testbasepath)
+ rr = apiList(basepathOrApiName = Some(testbasepath))
+ rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ } finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify successful creation with complex swagger doc as input" in {
+ val testName = "CLI_APIGWTEST13"
+ val testbasepath = "/test1/v1"
+ val testrelpath = "/whisk.system/utils/echo"
+ val testrelpath2 = "/whisk.system/utils/split"
+ val testurlop = "get"
+ val testapiname = "/test1/v1"
+ val actionName = "test1a"
+ val swaggerPath = TestUtils.getTestApiGwFilename(s"testswaggerdoc2")
+ try {
+ var rr = apiCreate(swagger = Some(swaggerPath))
+ println("api create stdout: " + rr.stdout)
+ println("api create stderror: " + rr.stderr)
+ rr.stdout should include("ok: created API")
+ rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+ rr.stdout should include("ok: APIs")
+ // Actual CLI namespace will vary from local dev to automated test environments, so don't check
+ rr.stdout should include regex (s"/[@\\w._\\-]+/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+ rr.stdout should include(testbasepath + testrelpath2)
+ } finally {
+ val deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+
+ it should "verify successful creation and deletion with multiple base paths" in {
+ val testName = "CLI_APIGWTEST14"
+ val testbasepath = "/" + testName + "_bp"
+ val testbasepath2 = "/" + testName + "_bp2"
+ val testrelpath = "/path"
+ val testnewrelpath = "/path_new"
+ val testurlop = "get"
+ val testapiname = testName + " API Name"
+ val actionName = testName + "_action"
+ try {
+ var rr = apiCreate(basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+ rr = apiCreate(basepath = Some(testbasepath2), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname))
+ rr.stdout should include("ok: created API")
+ rr = apiList(basepathOrApiName = Some(testbasepath2), relpath = Some(testrelpath), operation = Some(testurlop))
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath2 + testrelpath)
+ rr = apiDelete(basepathOrApiName = testbasepath2)
+ rr.stdout should include("ok: deleted API")
+ rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
+ rr.stdout should include("ok: APIs")
+ rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+")
+ rr.stdout should include(testbasepath + testrelpath)
+ rr = apiDelete(basepathOrApiName = testbasepath)
+ rr.stdout should include("ok: deleted API")
+ } finally {
+ var deleteresult = apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT)
+ deleteresult = apiDelete(basepathOrApiName = testbasepath2, expectedExitCode = DONTCARE_EXIT)
+ }
+ }
+}
diff --git a/tests/src/test/scala/whisk/core/cli/test/WskActionSequenceTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskActionSequenceTests.scala
new file mode 100644
index 0000000..99297f9
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/cli/test/WskActionSequenceTests.scala
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2015-2016 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 whisk.core.cli.test
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+
+/**
+ * Tests creation and retrieval of a sequence action
+ */
+@RunWith(classOf[JUnitRunner])
+class WskActionSequenceTests
+ extends TestHelpers
+ with WskTestHelpers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val defaultNamespace = wskprops.namespace
+ val (user, namespace) = WskAdmin.getUser(wskprops.authKey)
+
+ behavior of "Wsk Action Sequence"
+
+ it should "create, and get an action sequence" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "actionSeq"
+ val packageName = "samples"
+ val helloName = "hello"
+ val catName = "cat"
+ val fullHelloActionName = s"/$defaultNamespace/$packageName/$helloName"
+ val fullCatActionName = s"/$defaultNamespace/$packageName/$catName"
+
+ assetHelper.withCleaner(wsk.pkg, packageName) {
+ (pkg, _) => pkg.create(packageName, shared = Some(true))(wp)
+ }
+
+ assetHelper.withCleaner(wsk.action, fullHelloActionName) {
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ (action, _) => action.create(fullHelloActionName, file)(wp)
+ }
+
+ assetHelper.withCleaner(wsk.action, fullCatActionName) {
+ val file = Some(TestUtils.getTestActionFilename("cat.js"))
+ (action, _) => action.create(fullCatActionName, file)(wp)
+ }
+
+ val artifacts = s"$fullHelloActionName,$fullCatActionName"
+ val kindValue = JsString("sequence")
+ val compValue = JsArray(
+ JsString(resolveDefaultNamespace(fullHelloActionName)),
+ JsString(resolveDefaultNamespace(fullCatActionName)))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(artifacts), kind = Some("sequence"))
+ }
+
+ val stdout = wsk.action.get(name).stdout
+ assert(stdout.startsWith(s"ok: got action $name\n"))
+ wsk.parseJsonString(stdout).fields("exec").asJsObject.fields("components") shouldBe compValue
+ wsk.parseJsonString(stdout).fields("exec").asJsObject.fields("kind") shouldBe kindValue
+ }
+
+ private def resolveDefaultNamespace(actionName: String) = actionName.replace("/_/", s"/$namespace/")
+}
diff --git a/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala
new file mode 100644
index 0000000..e421df8
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/cli/test/WskBasicUsageTests.scala
@@ -0,0 +1,1245 @@
+/*
+ * Copyright 2015-2016 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 whisk.core.cli.test
+
+import java.io.File
+import java.io.BufferedWriter
+import java.io.FileWriter
+import java.time.Instant
+
+import scala.language.postfixOps
+import scala.concurrent.duration.Duration
+import scala.concurrent.duration.DurationInt
+import scala.util.Random
+
+import org.apache.commons.io.FileUtils
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils._
+import common.WhiskProperties
+import common.Wsk
+import common.WskProps
+import common.WskTestHelpers
+import spray.json.DefaultJsonProtocol._
+import spray.json._
+import whisk.core.entity._
+import whisk.core.entity.LogLimit._
+import whisk.core.entity.MemoryLimit._
+import whisk.core.entity.TimeLimit._
+import whisk.core.entity.size.SizeInt
+import whisk.utils.retry
+import JsonArgsForTests._
+import whisk.http.Messages
+import common.WskAdmin
+import java.time.Clock
+
+/**
+ * Tests for basic CLI usage. Some of these tests require a deployed backend.
+ */
+@RunWith(classOf[JUnitRunner])
+class WskBasicUsageTests
+ extends TestHelpers
+ with WskTestHelpers {
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val defaultAction = Some(TestUtils.getTestActionFilename("hello.js"))
+
+ behavior of "Wsk CLI usage"
+
+ it should "confirm wsk exists" in {
+ Wsk.exists
+ }
+
+ it should "show help and usage info" in {
+ val stdout = wsk.cli(Seq("-h")).stdout
+ stdout should include regex ("""(?i)Usage:""")
+ stdout should include regex ("""(?i)Flags""")
+ stdout should include regex ("""(?i)Available commands""")
+ stdout should include regex ("""(?i)--help""")
+ }
+
+ it should "show help and usage info using the default language" in {
+ val env = Map("LANG" -> "de_DE")
+ // Call will fail with exit code 2 if language not supported
+ wsk.cli(Seq("-h"), env = env)
+ }
+
+ it should "show cli build version" in {
+ val stdout = wsk.cli(Seq("property", "get", "--cliversion")).stdout
+ stdout should include regex ("""(?i)whisk CLI version\s+201.*""")
+ }
+
+ it should "show api version" in {
+ val stdout = wsk.cli(Seq("property", "get", "--apiversion")).stdout
+ stdout should include regex ("""(?i)whisk API version\s+v1""")
+ }
+
+ it should "set apihost, auth, and namespace" in {
+ val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+ try {
+ val namespace = wsk.namespace.list().stdout.trim.split("\n").last
+ val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+ val stdout = wsk.cli(Seq("property", "set", "-i", "--apihost", wskprops.apihost, "--auth", wskprops.authKey,
+ "--namespace", namespace), env = env).stdout
+ stdout should include(s"ok: whisk auth set to ${wskprops.authKey}")
+ stdout should include(s"ok: whisk API host set to ${wskprops.apihost}")
+ stdout should include(s"ok: whisk namespace set to ${namespace}")
+ } finally {
+ tmpwskprops.delete()
+ }
+ }
+
+ it should "ensure default namespace is used when a blank namespace is set" in {
+ val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+ try {
+ val writer = new BufferedWriter(new FileWriter(tmpwskprops))
+ writer.write(s"NAMESPACE=")
+ writer.close()
+ val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+ val stdout = wsk.cli(Seq("property", "get", "-i", "--namespace"), env = env).stdout
+ stdout should include regex ("whisk namespace\\s+_")
+ } finally {
+ tmpwskprops.delete()
+ }
+ }
+
+ it should "show api build version using property file" in {
+ val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+ try {
+ val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+ wsk.cli(Seq("property", "set", "-i") ++ wskprops.overrides, env = env)
+ val stdout = wsk.cli(Seq("property", "get", "--apibuild", "-i"), env = env).stdout
+ stdout should include regex ("""(?i)whisk API build\s+201.*""")
+ } finally {
+ tmpwskprops.delete()
+ }
+ }
+
+ it should "fail to show api build when setting apihost to bogus value" in {
+ val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+ try {
+ val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+ wsk.cli(Seq("property", "set", "-i", "--apihost", "xxxx.yyyy"), env = env)
+ val rr = wsk.cli(Seq("property", "get", "--apibuild", "-i"), env = env, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stdout should include regex ("""whisk API build\s*Unknown""")
+ rr.stderr should include regex ("Unable to obtain API build information")
+ } finally {
+ tmpwskprops.delete()
+ }
+ }
+
+ it should "show api build using http apihost" in {
+ val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+ try {
+ val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+ val apihost = s"http://${WhiskProperties.getControllerHost}:${WhiskProperties.getControllerPort}"
+ wsk.cli(Seq("property", "set", "--apihost", apihost), env = env)
+ val rr = wsk.cli(Seq("property", "get", "--apibuild", "-i"), env = env)
+ rr.stdout should not include regex("""whisk API build\s*Unknown""")
+ rr.stderr should not include regex("Unable to obtain API build information")
+ rr.stdout should include regex ("""(?i)whisk API build\s+201.*""")
+ } finally {
+ tmpwskprops.delete()
+ }
+ }
+
+ it should "validate default property values" in {
+ val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+ val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+ val stdout = wsk.cli(Seq("property", "unset", "--auth", "--apihost", "--apiversion", "--namespace"), env = env).stdout
+ try {
+ stdout should include regex ("ok: whisk auth unset")
+ stdout should include regex ("ok: whisk API host unset")
+ stdout should include regex ("ok: whisk API version unset")
+ stdout should include regex ("ok: whisk namespace unset")
+
+ wsk.cli(Seq("property", "get", "--auth"), env = env).
+ stdout should include regex ("""(?i)whisk auth\s*$""") // default = empty string
+ wsk.cli(Seq("property", "get", "--apihost"), env = env).
+ stdout should include regex ("""(?i)whisk API host\s*$""") // default = empty string
+ wsk.cli(Seq("property", "get", "--namespace"), env = env).
+ stdout should include regex ("""(?i)whisk namespace\s*_$""") // default = _
+ } finally {
+ tmpwskprops.delete()
+ }
+ }
+
+ it should "set auth in property file" in {
+ val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+ val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+ wsk.cli(Seq("property", "set", "--auth", "testKey"), env = env)
+ try {
+ val fileContent = FileUtils.readFileToString(tmpwskprops)
+ fileContent should include("AUTH=testKey")
+ } finally {
+ tmpwskprops.delete()
+ }
+ }
+
+ it should "set multiple property values with single command" in {
+ val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+ val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+ val stdout = wsk.cli(Seq("property", "set", "--auth", "testKey", "--apihost", "openwhisk.ng.bluemix.net", "--apiversion", "v1"), env = env).stdout
+ try {
+ stdout should include regex ("ok: whisk auth set")
+ stdout should include regex ("ok: whisk API host set")
+ stdout should include regex ("ok: whisk API version set")
+ val fileContent = FileUtils.readFileToString(tmpwskprops)
+ fileContent should include("AUTH=testKey")
+ fileContent should include("APIHOST=openwhisk.ng.bluemix.net")
+ fileContent should include("APIVERSION=v1")
+ } finally {
+ tmpwskprops.delete()
+ }
+ }
+
+ it should "reject bad command" in {
+ val result = wsk.cli(Seq("bogus"), expectedExitCode = ERROR_EXIT)
+ result.stderr should include regex ("""(?i)Run 'wsk --help' for usage""")
+ }
+
+ it should "reject authenticated command when no auth key is given" in {
+ // override wsk props file in case it exists
+ val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+ val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+ val stderr = wsk.cli(Seq("list") ++ wskprops.overrides, env = env, expectedExitCode = MISUSE_EXIT).stderr
+ try {
+ stderr should include regex (s"usage[:.]") // Python CLI: "usage:", Go CLI: "usage."
+ stderr should include("--auth is required")
+ } finally {
+ tmpwskprops.delete()
+ }
+ }
+
+ it should "reject a command when the API host is not set" in {
+ val tmpwskprops = File.createTempFile("wskprops", ".tmp")
+ try {
+ val env = Map("WSK_CONFIG_FILE" -> tmpwskprops.getAbsolutePath())
+ val stderr = wsk.cli(Seq("property", "get", "-i"), env = env, expectedExitCode = ERROR_EXIT).stderr
+ stderr should include("The API host is not valid: An API host must be provided.")
+ } finally {
+ tmpwskprops.delete()
+ }
+ }
+
+ behavior of "Wsk actions"
+
+ it should "reject creating entities with invalid names" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val names = Seq(
+ ("", NOT_ALLOWED),
+ (" ", BAD_REQUEST),
+ ("hi+there", BAD_REQUEST),
+ ("$hola", BAD_REQUEST),
+ ("dora?", BAD_REQUEST),
+ ("|dora|dora?", BAD_REQUEST))
+
+ names foreach {
+ case (name, ec) =>
+ assetHelper.withCleaner(wsk.action, name, confirmDelete = false) {
+ (action, _) => action.create(name, defaultAction, expectedExitCode = ec)
+ }
+ }
+ }
+
+ it should "reject create with missing file" in {
+ wsk.action.create("missingFile", Some("notfound"),
+ expectedExitCode = MISUSE_EXIT).
+ stderr should include("not a valid file")
+ }
+
+ it should "reject action update when specified file is missing" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ // Create dummy action to update
+ val name = "updateMissingFile"
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ assetHelper.withCleaner(wsk.action, name) { (action, name) => action.create(name, file) }
+ // Update it with a missing file
+ wsk.action.create("updateMissingFile", Some("notfound"), update = true, expectedExitCode = MISUSE_EXIT)
+ }
+
+ it should "create, and get an action to verify parameter and annotation parsing" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "actionAnnotations"
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, file, annotations = getValidJSONTestArgInput,
+ parameters = getValidJSONTestArgInput)
+ }
+
+ val stdout = wsk.action.get(name).stdout
+ assert(stdout.startsWith(s"ok: got action $name\n"))
+
+ val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+ val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+ val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements
+
+ for (expectedItem <- escapedJSONArr) {
+ receivedParams should contain(expectedItem)
+ receivedAnnots should contain(expectedItem)
+ }
+ }
+
+ it should "create, and get an action to verify file parameter and annotation parsing" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "actionAnnotAndParamParsing"
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ val argInput = Some(TestUtils.getTestActionFilename("validInput1.json"))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, file, annotationFile = argInput, parameterFile = argInput)
+ }
+
+ val stdout = wsk.action.get(name).stdout
+ assert(stdout.startsWith(s"ok: got action $name\n"))
+
+ val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+ val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+ val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements
+
+ for (expectedItem <- escapedJSONArr) {
+ receivedParams should contain(expectedItem)
+ receivedAnnots should contain(expectedItem)
+ }
+ }
+
+ it should "create an action with the proper parameter and annotation escapes" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "actionEscapes"
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, file, parameters = getEscapedJSONTestArgInput,
+ annotations = getEscapedJSONTestArgInput)
+ }
+
+ val stdout = wsk.action.get(name).stdout
+ assert(stdout.startsWith(s"ok: got action $name\n"))
+
+ val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+ val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+ val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements
+
+ for (expectedItem <- escapedJSONArr) {
+ receivedParams should contain(expectedItem)
+ receivedAnnots should contain(expectedItem)
+ }
+ }
+
+ it should "invoke an action that exits during initialization and get appropriate error" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "abort init"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("initexit.js")))
+ }
+
+ withActivation(wsk.activation, wsk.action.invoke(name)) {
+ activation =>
+ val response = activation.response
+ response.result.get.fields("error") shouldBe Messages.abnormalInitialization.toJson
+ response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ContainerError)
+ }
+ }
+
+ it should "invoke an action that hangs during initialization and get appropriate error" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "hang init"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(
+ name,
+ Some(TestUtils.getTestActionFilename("initforever.js")),
+ timeout = Some(3 seconds))
+ }
+
+ withActivation(wsk.activation, wsk.action.invoke(name)) {
+ activation =>
+ val response = activation.response
+ response.result.get.fields("error") shouldBe Messages.timedoutActivation(3 seconds, true).toJson
+ response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError)
+ }
+ }
+
+ it should "invoke an action that exits during run and get appropriate error" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "abort run"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("runexit.js")))
+ }
+
+ withActivation(wsk.activation, wsk.action.invoke(name)) {
+ activation =>
+ val response = activation.response
+ response.result.get.fields("error") shouldBe Messages.abnormalRun.toJson
+ response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ContainerError)
+ }
+ }
+
+ it should "ensure keys are not omitted from activation record" in withAssetCleaner(wskprops) {
+ val name = "activationRecordTest"
+
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("argCheck.js")))
+ }
+
+ val run = wsk.action.invoke(name)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.start should be > Instant.EPOCH
+ activation.end should be > Instant.EPOCH
+ activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.Success)
+ activation.response.success shouldBe true
+ activation.response.result shouldBe Some(JsObject())
+ activation.logs shouldBe Some(List())
+ activation.annotations shouldBe defined
+ }
+ }
+
+ it should "write the action-path and the limits to the annotations" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "annotations"
+ val memoryLimit = 512 MB
+ val logLimit = 1 MB
+ val timeLimit = 60 seconds
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloAsync.js")), memory = Some(memoryLimit), timeout = Some(timeLimit), logsize = Some(logLimit))
+ }
+
+ val run = wsk.action.invoke(name, Map("payload" -> "this is a test".toJson))
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe "success"
+ val annotations = activation.annotations.get
+
+ val limitsObj = JsObject(
+ "key" -> JsString("limits"),
+ "value" -> ActionLimits(TimeLimit(timeLimit), MemoryLimit(memoryLimit), LogLimit(logLimit)).toJson)
+
+ val path = annotations.find { _.fields("key").convertTo[String] == "path" }.get
+
+ path.fields("value").convertTo[String] should fullyMatch regex (s""".*/$name""")
+ annotations should contain(limitsObj)
+ }
+ }
+
+ it should "create, and invoke an action that utilizes an invalid docker container with appropriate error" in withAssetCleaner(wskprops) {
+ val name = "invalid dockerContainer"
+ val containerName = s"bogus${Random.alphanumeric.take(16).mkString.toLowerCase}"
+
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, name) {
+ // docker name is a randomly generate string
+ (action, _) => action.create(name, Some(containerName), kind = Some("docker"))
+ }
+
+ val run = wsk.action.invoke(name)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError)
+ activation.response.result.get.fields("error") shouldBe s"Failed to pull container image '$containerName'.".toJson
+ activation.annotations shouldBe defined
+ val limits = activation.annotations.get.filter(_.fields("key").convertTo[String] == "limits")
+ withClue(limits) {
+ limits.length should be > 0
+ limits(0).fields("value") should not be JsNull
+ }
+ }
+ }
+
+ it should "invoke an action using npm openwhisk" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "hello npm openwhisk"
+ assetHelper.withCleaner(wsk.action, name, confirmDelete = false) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloOpenwhiskPackage.js")))
+ }
+
+ val run = wsk.action.invoke(name, Map("ignore_certs" -> true.toJson, "name" -> name.toJson))
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe "success"
+ activation.response.result shouldBe Some(JsObject("delete" -> true.toJson))
+ activation.logs.get.mkString(" ") should include("action list has this many actions")
+ }
+
+ wsk.action.delete(name, expectedExitCode = TestUtils.NOT_FOUND)
+ }
+
+ it should "invoke an action receiving context properties" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val (user, namespace) = WskAdmin.getUser(wskprops.authKey)
+ val name = "context"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloContext.js")))
+ }
+
+ val start = Instant.now(Clock.systemUTC()).toEpochMilli
+ val run = wsk.action.invoke(name)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe "success"
+ val fields = activation.response.result.get.convertTo[Map[String, String]]
+ fields("api_host") shouldBe WhiskProperties.getApiHost
+ fields("api_key") shouldBe wskprops.authKey
+ fields("namespace") shouldBe namespace
+ fields("action_name") shouldBe s"/$namespace/$name"
+ fields("activation_id") shouldBe activation.activationId
+ fields("deadline").toLong should be >= start
+ }
+ }
+
+ it should "invoke an action that returns a result by the deadline" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "deadline"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloDeadline.js")), timeout = Some(3 seconds))
+ }
+
+ val run = wsk.action.invoke(name)
+ withActivation(wsk.activation, run) {
+ activation =>
+ activation.response.status shouldBe "success"
+ activation.response.result shouldBe Some(JsObject("timedout" -> true.toJson))
+ }
+ }
+
+ it should "invoke an action twice, where the first times out but the second does not and should succeed" in withAssetCleaner(wskprops) {
+ // this test issues two activations: the first is forced to time out and not return a result by its deadline (ie it does not resolve
+ // its promise). The invoker should reclaim its container so that a second activation of the same action (which must happen within a
+ // short period of time (seconds, not minutes) is allocated a fresh container and hence runs as expected (vs. hitting in the container
+ // cache and reusing a bad container).
+ (wp, assetHelper) =>
+ val name = "timeout"
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, Some(TestUtils.getTestActionFilename("helloDeadline.js")), timeout = Some(3 seconds))
+ }
+
+ val start = Instant.now(Clock.systemUTC()).toEpochMilli
+ val hungRun = wsk.action.invoke(name, Map("forceHang" -> true.toJson))
+ withActivation(wsk.activation, hungRun) {
+ activation =>
+ // the first action must fail with a timeout error
+ activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.ApplicationError)
+ activation.response.result shouldBe Some(JsObject("error" -> Messages.timedoutActivation(3 seconds, false).toJson))
+ }
+
+ // run the action again, this time without forcing it to timeout
+ // it should succeed because it ran in a fresh container
+ val goodRun = wsk.action.invoke(name, Map("forceHang" -> false.toJson))
+ withActivation(wsk.activation, goodRun) {
+ activation =>
+ // the first action must fail with a timeout error
+ activation.response.status shouldBe "success"
+ activation.response.result shouldBe Some(JsObject("timedout" -> true.toJson))
+ }
+ }
+
+ behavior of "Wsk packages"
+
+ it should "create, and delete a package" in {
+ val name = "createDeletePackage"
+ wsk.pkg.create(name).stdout should include(s"ok: created package $name")
+ wsk.pkg.delete(name).stdout should include(s"ok: deleted package $name")
+ }
+
+ it should "create, and get a package to verify parameter and annotation parsing" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "packageAnnotAndParamParsing"
+
+ assetHelper.withCleaner(wsk.pkg, name) {
+ (pkg, _) =>
+ pkg.create(name, annotations = getValidJSONTestArgInput, parameters = getValidJSONTestArgInput)
+ }
+
+ val stdout = wsk.pkg.get(name).stdout
+ assert(stdout.startsWith(s"ok: got package $name\n"))
+
+ val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+ val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+ val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements
+
+ for (expectedItem <- escapedJSONArr) {
+ receivedParams should contain(expectedItem)
+ receivedAnnots should contain(expectedItem)
+ }
+ }
+
+ it should "create, and get a package to verify file parameter and annotation parsing" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "packageAnnotAndParamFileParsing"
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ val argInput = Some(TestUtils.getTestActionFilename("validInput1.json"))
+
+ assetHelper.withCleaner(wsk.pkg, name) {
+ (pkg, _) =>
+ pkg.create(name, annotationFile = argInput, parameterFile = argInput)
+ }
+
+ val stdout = wsk.pkg.get(name).stdout
+ assert(stdout.startsWith(s"ok: got package $name\n"))
+
+ val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+ val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+ val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements
+
+ for (expectedItem <- escapedJSONArr) {
+ receivedParams should contain(expectedItem)
+ receivedAnnots should contain(expectedItem)
+ }
+ }
+
+ it should "create a package with the proper parameter and annotation escapes" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "packageEscapses"
+
+ assetHelper.withCleaner(wsk.pkg, name) {
+ (pkg, _) =>
+ pkg.create(name, parameters = getEscapedJSONTestArgInput,
+ annotations = getEscapedJSONTestArgInput)
+ }
+
+ val stdout = wsk.pkg.get(name).stdout
+ assert(stdout.startsWith(s"ok: got package $name\n"))
+
+ val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+ val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+ val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements
+
+ for (expectedItem <- escapedJSONArr) {
+ receivedParams should contain(expectedItem)
+ receivedAnnots should contain(expectedItem)
+ }
+ }
+
+ it should "report conformance error accessing action as package" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "aAsP"
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) => action.create(name, file)
+ }
+
+ wsk.pkg.get(name, expectedExitCode = CONFLICT).
+ stderr should include(Messages.conformanceMessage)
+
+ wsk.pkg.bind(name, "bogus", expectedExitCode = CONFLICT).
+ stderr should include(Messages.requestedBindingIsNotValid)
+
+ wsk.pkg.bind("bogus", "alsobogus", expectedExitCode = BAD_REQUEST).
+ stderr should include(Messages.bindingDoesNotExist)
+
+ }
+
+ behavior of "Wsk triggers"
+
+ it should "create, and get a trigger to verify parameter and annotation parsing" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "triggerAnnotAndParamParsing"
+
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name, annotations = getValidJSONTestArgInput, parameters = getValidJSONTestArgInput)
+ }
+
+ val stdout = wsk.trigger.get(name).stdout
+ assert(stdout.startsWith(s"ok: got trigger $name\n"))
+
+ val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+ val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+ val escapedJSONArr = getValidJSONTestArgOutput.convertTo[JsArray].elements
+
+ for (expectedItem <- escapedJSONArr) {
+ receivedParams should contain(expectedItem)
+ receivedAnnots should contain(expectedItem)
+ }
+ }
+
+ it should "create, and get a trigger to verify file parameter and annotation parsing" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "triggerAnnotAndParamFileParsing"
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ val argInput = Some(TestUtils.getTestActionFilename("validInput1.json"))
+
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name, annotationFile = argInput, parameterFile = argInput)
+ }
+
+ val stdout = wsk.trigger.get(name).stdout
+ assert(stdout.startsWith(s"ok: got trigger $name\n"))
+
+ val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+ val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+ val escapedJSONArr = getJSONFileOutput.convertTo[JsArray].elements
+
+ for (expectedItem <- escapedJSONArr) {
+ receivedParams should contain(expectedItem)
+ receivedAnnots should contain(expectedItem)
+ }
+ }
+
+ it should "display a trigger summary when --summary flag is used with 'wsk trigger get'" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val triggerName = "mySummaryTrigger"
+ assetHelper.withCleaner(wsk.trigger, triggerName, confirmDelete = false) {
+ (trigger, name) => trigger.create(name)
+ }
+
+ // Summary namespace should match one of the allowable namespaces (typically 'guest')
+ val ns_regex_list = wsk.namespace.list().stdout.trim.replace('\n', '|')
+ val stdout = wsk.trigger.get(triggerName, summary = true).stdout
+ stdout should include regex (s"(?i)trigger\\s+/${ns_regex_list}/${triggerName}")
+ }
+
+ it should "create a trigger with the proper parameter and annotation escapes" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "triggerEscapes"
+
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name, parameters = getEscapedJSONTestArgInput,
+ annotations = getEscapedJSONTestArgInput)
+ }
+
+ val stdout = wsk.trigger.get(name).stdout
+ assert(stdout.startsWith(s"ok: got trigger $name\n"))
+
+ val receivedParams = wsk.parseJsonString(stdout).fields("parameters").convertTo[JsArray].elements
+ val receivedAnnots = wsk.parseJsonString(stdout).fields("annotations").convertTo[JsArray].elements
+ val escapedJSONArr = getEscapedJSONTestArgOutput.convertTo[JsArray].elements
+
+ for (expectedItem <- escapedJSONArr) {
+ receivedParams should contain(expectedItem)
+ receivedAnnots should contain(expectedItem)
+ }
+ }
+
+ it should "not create a trigger when feed fails to initialize" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.trigger, "badfeed", confirmDelete = false) {
+ (trigger, name) =>
+ trigger.create(name, feed = Some(s"bogus"), expectedExitCode = ANY_ERROR_EXIT).
+ exitCode should equal(NOT_FOUND)
+ trigger.get(name, expectedExitCode = NOT_FOUND)
+
+ trigger.create(name, feed = Some(s"bogus/feed"), expectedExitCode = ANY_ERROR_EXIT).
+ exitCode should equal(NOT_FOUND)
+ trigger.get(name, expectedExitCode = NOT_FOUND)
+ }
+ }
+
+ behavior of "Wsk api"
+
+ it should "reject an api commands with an invalid path parameter" in {
+ val badpath = "badpath"
+
+ var rr = wsk.cli(Seq("api-experimental", "create", "/basepath", badpath, "GET", "action", "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include(s"'${badpath}' must begin with '/'")
+
+ rr = wsk.cli(Seq("api-experimental", "delete", "/basepath", badpath, "GET", "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include(s"'${badpath}' must begin with '/'")
+
+ rr = wsk.cli(Seq("api-experimental", "list", "/basepath", badpath, "GET", "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include(s"'${badpath}' must begin with '/'")
+ }
+
+ it should "reject an api commands with an invalid verb parameter" in {
+ val badverb = "badverb"
+
+ var rr = wsk.cli(Seq("api-experimental", "create", "/basepath", "/path", badverb, "action", "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include(s"'${badverb}' is not a valid API verb. Valid values are:")
+
+ rr = wsk.cli(Seq("api-experimental", "delete", "/basepath", "/path", badverb, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include(s"'${badverb}' is not a valid API verb. Valid values are:")
+
+ rr = wsk.cli(Seq("api-experimental", "list", "/basepath", "/path", badverb, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include(s"'${badverb}' is not a valid API verb. Valid values are:")
+ }
+
+ it should "reject an api create command with an API name argument and an API name option" in {
+ val apiName = "An API Name"
+ val rr = wsk.cli(Seq("api-experimental", "create", apiName, "/path", "GET", "action", "-n", apiName, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include(s"An API name can only be specified once.")
+ }
+
+ it should "reject an api create command that specifies a nonexistent configuration file" in {
+ val configfile = "/nonexistent/file"
+ val rr = wsk.cli(Seq("api-experimental", "create", "-c", configfile, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include(s"Error reading swagger file '${configfile}':")
+ }
+
+ it should "reject an api create command specifying a non-JSON configuration file" in {
+ val file = File.createTempFile("api.json", ".txt")
+ file.deleteOnExit()
+ val filename = file.getAbsolutePath()
+
+ val bw = new BufferedWriter(new FileWriter(file))
+ bw.write("a=A")
+ bw.close()
+
+ val rr = wsk.cli(Seq("api-experimental", "create", "-c", filename, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include(s"Error parsing swagger file '${filename}':")
+ }
+
+ it should "reject an api create command specifying a non-swagger JSON configuration file" in {
+ val file = File.createTempFile("api.json", ".txt")
+ file.deleteOnExit()
+ val filename = file.getAbsolutePath()
+
+ val bw = new BufferedWriter(new FileWriter(file))
+ bw.write("""|{
+ | "swagger": "2.0",
+ | "info": {
+ | "title": "My API",
+ | "version": "1.0.0"
+ | },
+ | "BADbasePath": "/bp",
+ | "paths": {
+ | "/rp": {
+ | "get":{}
+ | }
+ | }
+ |}""".stripMargin)
+ bw.close()
+
+ val rr = wsk.cli(Seq("api-experimental", "create", "-c", filename, "--auth", wskprops.authKey) ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT)
+ rr.stderr should include(s"Swagger file is invalid (missing basePath, info, paths, or swagger fields")
+ }
+
+ behavior of "Wsk entity list formatting"
+
+ it should "create, and list a package with a long name" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "x" * 70
+ assetHelper.withCleaner(wsk.pkg, name) {
+ (pkg, _) =>
+ pkg.create(name)
+ }
+ retry({
+ wsk.pkg.list().stdout should include(s"$name private")
+ }, 5, Some(1 second))
+ }
+
+ it should "create, and list an action with a long name" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "x" * 70
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, file)
+ }
+ retry({
+ wsk.action.list().stdout should include(s"$name private nodejs")
+ }, 5, Some(1 second))
+ }
+
+ it should "create, and list a trigger with a long name" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "x" * 70
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ trigger.create(name)
+ }
+ retry({
+ wsk.trigger.list().stdout should include(s"$name private")
+ }, 5, Some(1 second))
+ }
+
+ it should "create, and list a rule with a long name" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val ruleName = "x" * 70
+ val triggerName = "listRulesTrigger"
+ val actionName = "listRulesAction";
+ assetHelper.withCleaner(wsk.trigger, triggerName) {
+ (trigger, name) => trigger.create(name)
+ }
+ assetHelper.withCleaner(wsk.action, actionName) {
+ (action, name) => action.create(name, defaultAction)
+ }
+ assetHelper.withCleaner(wsk.rule, ruleName) {
+ (rule, name) =>
+ rule.create(name, trigger = triggerName, action = actionName)
+ }
+ retry({
+ wsk.rule.list().stdout should include(s"$ruleName private")
+ }, 5, Some(1 second))
+ }
+
+ behavior of "Wsk params and annotations"
+
+ it should "reject commands that are executed with invalid JSON for annotations and parameters" in {
+ val invalidJSONInputs = getInvalidJSONInput
+ val invalidJSONFiles = Seq(
+ TestUtils.getTestActionFilename("malformed.js"),
+ TestUtils.getTestActionFilename("invalidInput1.json"),
+ TestUtils.getTestActionFilename("invalidInput2.json"),
+ TestUtils.getTestActionFilename("invalidInput3.json"),
+ TestUtils.getTestActionFilename("invalidInput4.json"))
+ val paramCmds = Seq(
+ Seq("action", "create", "actionName", TestUtils.getTestActionFilename("hello.js")),
+ Seq("action", "update", "actionName", TestUtils.getTestActionFilename("hello.js")),
+ Seq("action", "invoke", "actionName"),
+ Seq("package", "create", "packageName"),
+ Seq("package", "update", "packageName"),
+ Seq("package", "bind", "packageName", "boundPackageName"),
+ Seq("trigger", "create", "triggerName"),
+ Seq("trigger", "update", "triggerName"),
+ Seq("trigger", "fire", "triggerName"))
+ val annotCmds = Seq(
+ Seq("action", "create", "actionName", TestUtils.getTestActionFilename("hello.js")),
+ Seq("action", "update", "actionName", TestUtils.getTestActionFilename("hello.js")),
+ Seq("package", "create", "packageName"),
+ Seq("package", "update", "packageName"),
+ Seq("package", "bind", "packageName", "boundPackageName"),
+ Seq("trigger", "create", "triggerName"),
+ Seq("trigger", "update", "triggerName"))
+
+ for (cmd <- paramCmds) {
+ for (invalid <- invalidJSONInputs) {
+ wsk.cli(cmd ++ Seq("-p", "key", invalid) ++ wskprops.overrides, expectedExitCode = ERROR_EXIT)
+ .stderr should include("Invalid parameter argument")
+ }
+
+ for (invalid <- invalidJSONFiles) {
+ wsk.cli(cmd ++ Seq("-P", invalid) ++ wskprops.overrides, expectedExitCode = ERROR_EXIT)
+ .stderr should include("Invalid parameter argument")
+
+ }
+ }
+
+ for (cmd <- annotCmds) {
+ for (invalid <- invalidJSONInputs) {
+ wsk.cli(cmd ++ Seq("-a", "key", invalid) ++ wskprops.overrides, expectedExitCode = ERROR_EXIT)
+ .stderr should include("Invalid annotation argument")
+ }
+
+ for (invalid <- invalidJSONFiles) {
+ wsk.cli(cmd ++ Seq("-A", invalid) ++ wskprops.overrides, expectedExitCode = ERROR_EXIT)
+ .stderr should include("Invalid annotation argument")
+ }
+ }
+ }
+
+ it should "reject commands that are executed with a missing or invalid parameter or annotation file" in {
+ val emptyFile = TestUtils.getTestActionFilename("emtpy.js")
+ val missingFile = "notafile"
+ val emptyFileMsg = s"File '$emptyFile' is not a valid file or it does not exist"
+ val missingFileMsg = s"File '$missingFile' is not a valid file or it does not exist"
+ val invalidArgs = Seq(
+ (Seq("action", "create", "actionName", TestUtils.getTestActionFilename("hello.js"), "-P", emptyFile),
+ emptyFileMsg),
+ (Seq("action", "update", "actionName", TestUtils.getTestActionFilename("hello.js"), "-P", emptyFile),
+ emptyFileMsg),
+ (Seq("action", "invoke", "actionName", "-P", emptyFile), emptyFileMsg),
+ (Seq("action", "create", "actionName", "-P", emptyFile), emptyFileMsg),
+ (Seq("action", "update", "actionName", "-P", emptyFile), emptyFileMsg),
+ (Seq("action", "invoke", "actionName", "-P", emptyFile), emptyFileMsg),
+ (Seq("package", "create", "packageName", "-P", emptyFile), emptyFileMsg),
+ (Seq("package", "update", "packageName", "-P", emptyFile), emptyFileMsg),
+ (Seq("package", "bind", "packageName", "boundPackageName", "-P", emptyFile), emptyFileMsg),
+ (Seq("package", "create", "packageName", "-P", emptyFile), emptyFileMsg),
+ (Seq("package", "update", "packageName", "-P", emptyFile), emptyFileMsg),
+ (Seq("package", "bind", "packageName", "boundPackageName", "-P", emptyFile), emptyFileMsg),
+ (Seq("trigger", "create", "triggerName", "-P", emptyFile), emptyFileMsg),
+ (Seq("trigger", "update", "triggerName", "-P", emptyFile), emptyFileMsg),
+ (Seq("trigger", "fire", "triggerName", "-P", emptyFile), emptyFileMsg),
+ (Seq("trigger", "create", "triggerName", "-P", emptyFile), emptyFileMsg),
+ (Seq("trigger", "update", "triggerName", "-P", emptyFile), emptyFileMsg),
+ (Seq("trigger", "fire", "triggerName", "-P", emptyFile), emptyFileMsg),
+ (Seq("action", "create", "actionName", TestUtils.getTestActionFilename("hello.js"), "-A", missingFile),
+ missingFileMsg),
+ (Seq("action", "update", "actionName", TestUtils.getTestActionFilename("hello.js"), "-A", missingFile),
+ missingFileMsg),
+ (Seq("action", "invoke", "actionName", "-A", missingFile), missingFileMsg),
+ (Seq("action", "create", "actionName", "-A", missingFile), missingFileMsg),
+ (Seq("action", "update", "actionName", "-A", missingFile), missingFileMsg),
+ (Seq("action", "invoke", "actionName", "-A", missingFile), missingFileMsg),
+ (Seq("package", "create", "packageName", "-A", missingFile), missingFileMsg),
+ (Seq("package", "update", "packageName", "-A", missingFile), missingFileMsg),
+ (Seq("package", "bind", "packageName", "boundPackageName", "-A", missingFile), missingFileMsg),
+ (Seq("package", "create", "packageName", "-A", missingFile), missingFileMsg),
+ (Seq("package", "update", "packageName", "-A", missingFile), missingFileMsg),
+ (Seq("package", "bind", "packageName", "boundPackageName", "-A", missingFile), missingFileMsg),
+ (Seq("trigger", "create", "triggerName", "-A", missingFile), missingFileMsg),
+ (Seq("trigger", "update", "triggerName", "-A", missingFile), missingFileMsg),
+ (Seq("trigger", "fire", "triggerName", "-A", missingFile), missingFileMsg),
+ (Seq("trigger", "create", "triggerName", "-A", missingFile), missingFileMsg),
+ (Seq("trigger", "update", "triggerName", "-A", missingFile), missingFileMsg),
+ (Seq("trigger", "fire", "triggerName", "-A", missingFile), missingFileMsg))
+
+ invalidArgs foreach {
+ case (cmd, err) =>
+ val stderr = wsk.cli(cmd, expectedExitCode = MISUSE_EXIT).stderr
+ stderr should include(err)
+ stderr should include("Run 'wsk --help' for usage.")
+ }
+ }
+
+ it should "reject commands that are executed with not enough param or annot arguments" in {
+ val invalidParamMsg = "Arguments for '-p' must be a key/value pair"
+ val invalidAnnotMsg = "Arguments for '-a' must be a key/value pair"
+ val invalidParamFileMsg = "An argument must be provided for '-P'"
+ val invalidAnnotFileMsg = "An argument must be provided for '-A'"
+ val invalidArgs = Seq(
+ (Seq("action", "create", "actionName", "-p"), invalidParamMsg),
+ (Seq("action", "create", "actionName", "-p", "key"), invalidParamMsg),
+ (Seq("action", "create", "actionName", "-P"), invalidParamFileMsg),
+ (Seq("action", "update", "actionName", "-p"), invalidParamMsg),
+ (Seq("action", "update", "actionName", "-p", "key"), invalidParamMsg),
+ (Seq("action", "update", "actionName", "-P"), invalidParamFileMsg),
+ (Seq("action", "invoke", "actionName", "-p"), invalidParamMsg),
+ (Seq("action", "invoke", "actionName", "-p", "key"), invalidParamMsg),
+ (Seq("action", "invoke", "actionName", "-P"), invalidParamFileMsg),
+ (Seq("action", "create", "actionName", "-a"), invalidAnnotMsg),
+ (Seq("action", "create", "actionName", "-a", "key"), invalidAnnotMsg),
+ (Seq("action", "create", "actionName", "-A"), invalidAnnotFileMsg),
+ (Seq("action", "update", "actionName", "-a"), invalidAnnotMsg),
+ (Seq("action", "update", "actionName", "-a", "key"), invalidAnnotMsg),
+ (Seq("action", "update", "actionName", "-A"), invalidAnnotFileMsg),
+ (Seq("action", "invoke", "actionName", "-a"), invalidAnnotMsg),
+ (Seq("action", "invoke", "actionName", "-a", "key"), invalidAnnotMsg),
+ (Seq("action", "invoke", "actionName", "-A"), invalidAnnotFileMsg),
+ (Seq("package", "create", "packageName", "-p"), invalidParamMsg),
+ (Seq("package", "create", "packageName", "-p", "key"), invalidParamMsg),
+ (Seq("package", "create", "packageName", "-P"), invalidParamFileMsg),
+ (Seq("package", "update", "packageName", "-p"), invalidParamMsg),
+ (Seq("package", "update", "packageName", "-p", "key"), invalidParamMsg),
+ (Seq("package", "update", "packageName", "-P"), invalidParamFileMsg),
+ (Seq("package", "bind", "packageName", "boundPackageName", "-p"), invalidParamMsg),
+ (Seq("package", "bind", "packageName", "boundPackageName", "-p", "key"), invalidParamMsg),
+ (Seq("package", "bind", "packageName", "boundPackageName", "-P"), invalidParamFileMsg),
+ (Seq("package", "create", "packageName", "-a"), invalidAnnotMsg),
+ (Seq("package", "create", "packageName", "-a", "key"), invalidAnnotMsg),
+ (Seq("package", "create", "packageName", "-A"), invalidAnnotFileMsg),
+ (Seq("package", "update", "packageName", "-a"), invalidAnnotMsg),
+ (Seq("package", "update", "packageName", "-a", "key"), invalidAnnotMsg),
+ (Seq("package", "update", "packageName", "-A"), invalidAnnotFileMsg),
+ (Seq("package", "bind", "packageName", "boundPackageName", "-a"), invalidAnnotMsg),
+ (Seq("package", "bind", "packageName", "boundPackageName", "-a", "key"), invalidAnnotMsg),
+ (Seq("package", "bind", "packageName", "boundPackageName", "-A"), invalidAnnotFileMsg),
+ (Seq("trigger", "create", "triggerName", "-p"), invalidParamMsg),
+ (Seq("trigger", "create", "triggerName", "-p", "key"), invalidParamMsg),
+ (Seq("trigger", "create", "triggerName", "-P"), invalidParamFileMsg),
+ (Seq("trigger", "update", "triggerName", "-p"), invalidParamMsg),
+ (Seq("trigger", "update", "triggerName", "-p", "key"), invalidParamMsg),
+ (Seq("trigger", "update", "triggerName", "-P"), invalidParamFileMsg),
+ (Seq("trigger", "fire", "triggerName", "-p"), invalidParamMsg),
+ (Seq("trigger", "fire", "triggerName", "-p", "key"), invalidParamMsg),
+ (Seq("trigger", "fire", "triggerName", "-P"), invalidParamFileMsg),
+ (Seq("trigger", "create", "triggerName", "-a"), invalidAnnotMsg),
+ (Seq("trigger", "create", "triggerName", "-a", "key"), invalidAnnotMsg),
+ (Seq("trigger", "create", "triggerName", "-A"), invalidAnnotFileMsg),
+ (Seq("trigger", "update", "triggerName", "-a"), invalidAnnotMsg),
+ (Seq("trigger", "update", "triggerName", "-a", "key"), invalidAnnotMsg),
+ (Seq("trigger", "update", "triggerName", "-A"), invalidAnnotFileMsg),
+ (Seq("trigger", "fire", "triggerName", "-a"), invalidAnnotMsg),
+ (Seq("trigger", "fire", "triggerName", "-a", "key"), invalidAnnotMsg),
+ (Seq("trigger", "fire", "triggerName", "-A"), invalidAnnotFileMsg))
+
+ invalidArgs foreach {
+ case (cmd, err) =>
+ val stderr = wsk.cli(cmd, expectedExitCode = ERROR_EXIT).stderr
+ stderr should include(err)
+ stderr should include("Run 'wsk --help' for usage.")
+ }
+ }
+
+ behavior of "Wsk invalid argument handling"
+
+ it should "reject commands that are executed with invalid arguments" in {
+ val invalidArgsMsg = "error: Invalid argument(s)"
+ val tooFewArgsMsg = invalidArgsMsg + "."
+ val tooManyArgsMsg = invalidArgsMsg + ": "
+ val actionNameActionReqMsg = "An action name and action are required."
+ val actionNameReqMsg = "An action name is required."
+ val actionOptMsg = "An action is optional."
+ val packageNameReqMsg = "A package name is required."
+ val packageNameBindingReqMsg = "A package name and binding name are required."
+ val ruleNameReqMsg = "A rule name is required."
+ val ruleTriggerActionReqMsg = "A rule, trigger and action name are required."
+ val activationIdReq = "An activation ID is required."
+ val triggerNameReqMsg = "A trigger name is required."
+ val optNamespaceMsg = "An optional namespace is the only valid argument."
+ val optPayloadMsg = "A payload is optional."
+ val noArgsReqMsg = "No arguments are required."
+ val invalidArg = "invalidArg"
+ val apiCreateReqMsg = "Specify a swagger file or specify an API base path with an API path, an API verb, and an action name."
+ val apiGetReqMsg = "An API base path or API name is required."
+ val apiDeleteReqMsg = "An API base path or API name is required. An optional API relative path and operation may also be provided."
+ val apiListReqMsg = "Optional parameters are: API base path (or API name), API relative path and operation."
+ val invalidShared = s"Cannot use value '$invalidArg' for shared"
+ val invalidArgs = Seq(
+ (Seq("api-experimental", "create"), s"${tooFewArgsMsg} ${apiCreateReqMsg}"),
+ (Seq("api-experimental", "create", "/basepath", "/path", "GET", "action", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${apiCreateReqMsg}"),
+ (Seq("api-experimental", "get"), s"${tooFewArgsMsg} ${apiGetReqMsg}"),
+ (Seq("api-experimental", "get", "/basepath", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${apiGetReqMsg}"),
+ (Seq("api-experimental", "delete"), s"${tooFewArgsMsg} ${apiDeleteReqMsg}"),
+ (Seq("api-experimental", "delete", "/basepath", "/path", "GET", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${apiDeleteReqMsg}"),
+ (Seq("api-experimental", "list", "/basepath", "/path", "GET", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${apiListReqMsg}"),
+ (Seq("action", "create"), s"${tooFewArgsMsg} ${actionNameActionReqMsg}"),
+ (Seq("action", "create", "someAction"), s"${tooFewArgsMsg} ${actionNameActionReqMsg}"),
+ (Seq("action", "create", "actionName", "artifactName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("action", "update"), s"${tooFewArgsMsg} ${actionNameReqMsg} ${actionOptMsg}"),
+ (Seq("action", "update", "actionName", "artifactName", invalidArg),
+ s"${tooManyArgsMsg}${invalidArg}. ${actionNameReqMsg} ${actionOptMsg}"),
+ (Seq("action", "delete"), s"${tooFewArgsMsg} ${actionNameReqMsg}"),
+ (Seq("action", "delete", "actionName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("action", "get"), s"${tooFewArgsMsg} ${actionNameReqMsg}"),
+ (Seq("action", "get", "actionName", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("action", "list", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+ (Seq("action", "invoke"), s"${tooFewArgsMsg} ${actionNameReqMsg}"),
+ (Seq("action", "invoke", "actionName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("activation", "list", "namespace", invalidArg),
+ s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+ (Seq("activation", "get"), s"${tooFewArgsMsg} ${activationIdReq}"),
+ (Seq("activation", "get", "activationID", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("activation", "logs"), s"${tooFewArgsMsg} ${activationIdReq}"),
+ (Seq("activation", "logs", "activationID", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("activation", "result"), s"${tooFewArgsMsg} ${activationIdReq}"),
+ (Seq("activation", "result", "activationID", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("activation", "poll", "activationID", invalidArg),
+ s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+ (Seq("namespace", "list", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${noArgsReqMsg}"),
+ (Seq("namespace", "get", "namespace", invalidArg),
+ s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+ (Seq("package", "create"), s"${tooFewArgsMsg} ${packageNameReqMsg}"),
+ (Seq("package", "create", "packageName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("package", "create", "packageName", "--shared", invalidArg), invalidShared),
+ (Seq("package", "update"), s"${tooFewArgsMsg} ${packageNameReqMsg}"),
+ (Seq("package", "update", "packageName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("package", "update", "packageName", "--shared", invalidArg), invalidShared),
+ (Seq("package", "get"), s"${tooFewArgsMsg} ${packageNameReqMsg}"),
+ (Seq("package", "get", "packageName", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("package", "bind"), s"${tooFewArgsMsg} ${packageNameBindingReqMsg}"),
+ (Seq("package", "bind", "packageName"), s"${tooFewArgsMsg} ${packageNameBindingReqMsg}"),
+ (Seq("package", "bind", "packageName", "bindingName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("package", "list", "namespace", invalidArg),
+ s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+ (Seq("package", "delete"), s"${tooFewArgsMsg} ${packageNameReqMsg}"),
+ (Seq("package", "delete", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("package", "refresh", "namespace", invalidArg),
+ s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+ (Seq("rule", "enable"), s"${tooFewArgsMsg} ${ruleNameReqMsg}"),
+ (Seq("rule", "enable", "ruleName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("rule", "disable"), s"${tooFewArgsMsg} ${ruleNameReqMsg}"),
+ (Seq("rule", "disable", "ruleName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("rule", "status"), s"${tooFewArgsMsg} ${ruleNameReqMsg}"),
+ (Seq("rule", "status", "ruleName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("rule", "create"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+ (Seq("rule", "create", "ruleName"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+ (Seq("rule", "create", "ruleName", "triggerName"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+ (Seq("rule", "create", "ruleName", "triggerName", "actionName", invalidArg),
+ s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("rule", "update"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+ (Seq("rule", "update", "ruleName"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+ (Seq("rule", "update", "ruleName", "triggerName"), s"${tooFewArgsMsg} ${ruleTriggerActionReqMsg}"),
+ (Seq("rule", "update", "ruleName", "triggerName", "actionName", invalidArg),
+ s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("rule", "get"), s"${tooFewArgsMsg} ${ruleNameReqMsg}"),
+ (Seq("rule", "get", "ruleName", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("rule", "delete"), s"${tooFewArgsMsg} ${ruleNameReqMsg}"),
+ (Seq("rule", "delete", "ruleName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("rule", "list", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"),
+ (Seq("trigger", "fire"), s"${tooFewArgsMsg} ${triggerNameReqMsg} ${optPayloadMsg}"),
+ (Seq("trigger", "fire", "triggerName", "triggerPayload", invalidArg),
+ s"${tooManyArgsMsg}${invalidArg}. ${triggerNameReqMsg} ${optPayloadMsg}"),
+ (Seq("trigger", "create"), s"${tooFewArgsMsg} ${triggerNameReqMsg}"),
+ (Seq("trigger", "create", "triggerName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("trigger", "update"), s"${tooFewArgsMsg} ${triggerNameReqMsg}"),
+ (Seq("trigger", "update", "triggerName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("trigger", "get"), s"${tooFewArgsMsg} ${triggerNameReqMsg}"),
+ (Seq("trigger", "get", "triggerName", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("trigger", "delete"), s"${tooFewArgsMsg} ${triggerNameReqMsg}"),
+ (Seq("trigger", "delete", "triggerName", invalidArg), s"${tooManyArgsMsg}${invalidArg}."),
+ (Seq("trigger", "list", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"))
+
+ invalidArgs foreach {
+ case (cmd, err) =>
+ val stderr = wsk.cli(cmd ++ wskprops.overrides, expectedExitCode = ERROR_EXIT).stderr
+ stderr should include(err)
+ stderr should include("Run 'wsk --help' for usage.")
+ }
+ }
+
+ behavior of "Wsk action parameters"
+
+ it should "create an action with different permutations of limits" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+
+ def testLimit(timeout: Option[Duration] = None, memory: Option[ByteSize] = None, logs: Option[ByteSize] = None, ec: Int = SUCCESS_EXIT) = {
+ // Limits to assert, standard values if CLI omits certain values
+ val limits = JsObject(
+ "timeout" -> timeout.getOrElse(STD_DURATION).toMillis.toJson,
+ "memory" -> memory.getOrElse(STD_MEMORY).toMB.toInt.toJson,
+ "logs" -> logs.getOrElse(STD_LOGSIZE).toMB.toInt.toJson)
+
+ val name = "ActionLimitTests" + Instant.now.toEpochMilli
+ val createResult = assetHelper.withCleaner(wsk.action, name, confirmDelete = (ec == SUCCESS_EXIT)) {
+ (action, _) =>
+ val result = action.create(name, file, logsize = logs, memory = memory, timeout = timeout, expectedExitCode = DONTCARE_EXIT)
+ withClue(s"create failed for parameters: timeout = $timeout, memory = $memory, logsize = $logs:") {
+ result.exitCode should be(ec)
+ }
+ result
+ }
+
+ if (ec == SUCCESS_EXIT) {
+ val JsObject(parsedAction) = wsk.action.get(name).stdout.split("\n").tail.mkString.parseJson.asJsObject
+ parsedAction("limits") shouldBe limits
+ } else {
+ createResult.stderr should include("allowed threshold")
+ }
+ }
+
+ // Assert for valid permutations that the values are set correctly
+ for {
+ time <- Seq(None, Some(MIN_DURATION), Some(MAX_DURATION))
+ mem <- Seq(None, Some(MIN_MEMORY), Some(MAX_MEMORY))
+ log <- Seq(None, Some(MIN_LOGSIZE), Some(MAX_LOGSIZE))
+ } testLimit(time, mem, log)
+
+ // Assert that invalid permutation are rejected
+ testLimit(Some(0.milliseconds), None, None, BAD_REQUEST)
+ testLimit(Some(100.minutes), None, None, BAD_REQUEST)
+ testLimit(None, Some(0.MB), None, BAD_REQUEST)
+ testLimit(None, Some(32768.MB), None, BAD_REQUEST)
+ testLimit(None, None, Some(32768.MB), BAD_REQUEST)
+ }
+
+ it should "create a trigger using property file" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "listTriggers"
+ val tmpProps = File.createTempFile("wskprops", ".tmp")
+ val env = Map("WSK_CONFIG_FILE" -> tmpProps.getAbsolutePath())
+ wsk.cli(Seq("property", "set", "--auth", wp.authKey) ++ wskprops.overrides, env = env)
+ assetHelper.withCleaner(wsk.trigger, name) {
+ (trigger, _) =>
+ wsk.cli(Seq("-i", "trigger", "create", name), env = env)
+ }
+ tmpProps.delete()
+ }
+}
diff --git a/tests/src/test/scala/whisk/core/cli/test/WskEntitlementTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskEntitlementTests.scala
new file mode 100644
index 0000000..42a34b0
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/cli/test/WskEntitlementTests.scala
@@ -0,0 +1,376 @@
+/*
+ * Copyright 2015-2016 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 whisk.core.cli.test
+
+import org.junit.runner.RunWith
+import org.scalatest.BeforeAndAfterAll
+import org.scalatest.junit.JUnitRunner
+
+import common.RunWskAdminCmd
+import common.TestHelpers
+import common.TestUtils
+import common.TestUtils.FORBIDDEN
+import common.TestUtils.NOT_FOUND
+import common.TestUtils.TIMEOUT
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import whisk.core.entity.Subject
+import whisk.core.entity.WhiskPackage
+
+@RunWith(classOf[JUnitRunner])
+class WskEntitlementTests
+ extends TestHelpers
+ with WskTestHelpers
+ with BeforeAndAfterAll {
+
+ val wsk = new Wsk
+ lazy val defaultWskProps = WskProps()
+ lazy val guestWskProps = getAdditionalTestSubject()
+
+ override def afterAll() = {
+ disposeAdditionalTestSubject(guestWskProps.namespace)
+ }
+
+ def getAdditionalTestSubject() = {
+ val wskadmin = new RunWskAdminCmd {}
+ val newSubject = Subject().toString
+ WskProps(
+ namespace = newSubject,
+ authKey = wskadmin.cli(Seq("user", "create", newSubject)).stdout.trim)
+ }
+
+ def disposeAdditionalTestSubject(subject: String) = {
+ val wskadmin = new RunWskAdminCmd {}
+ withClue(s"failed to delete temporary subject $subject") {
+ wskadmin.cli(Seq("user", "delete", subject)).stdout should include("Subject deleted")
+ }
+ }
+
+ val samplePackage = "samplePackage"
+ val sampleAction = "sampleAction"
+ val fullSampleActionName = s"$samplePackage/$sampleAction"
+ val guestNamespace = guestWskProps.namespace
+
+ behavior of "Wsk Package Entitlement"
+
+ it should "not allow unauthorized subject to operate on private action" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ val privateAction = "privateAction"
+
+ assetHelper.withCleaner(wsk.action, privateAction) {
+ (action, name) => action.create(name, Some(TestUtils.getTestActionFilename("hello.js")))(wp)
+ }
+
+ val fullyQualifiedActionName = s"/$guestNamespace/$privateAction"
+ wsk.action.get(fullyQualifiedActionName, expectedExitCode = FORBIDDEN)(defaultWskProps).
+ stderr should include("not authorized")
+
+ withAssetCleaner(defaultWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, fullyQualifiedActionName, confirmDelete = false) {
+ (action, name) =>
+ val rr = action.create(name, None, update = true, expectedExitCode = FORBIDDEN)(wp)
+ rr.stderr should include("not authorized")
+ rr
+ }
+ assetHelper.withCleaner(wsk.action, "unauthorized sequence", confirmDelete = false) {
+ (action, name) =>
+ val rr = action.create(name, Some(fullyQualifiedActionName), kind = Some("sequence"), update = true, expectedExitCode = FORBIDDEN)(wp)
+ rr.stderr should include("not authorized")
+ rr
+ }
+ }
+
+ wsk.action.delete(fullyQualifiedActionName, expectedExitCode = FORBIDDEN)(defaultWskProps).
+ stderr should include("not authorized")
+
+ wsk.action.invoke(fullyQualifiedActionName, expectedExitCode = FORBIDDEN)(defaultWskProps).
+ stderr should include("not authorized")
+ }
+
+ it should "reject deleting action in shared package not owned by authkey" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+ }
+
+ assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+ val file = Some(TestUtils.getTestActionFilename("empty.js"))
+ (action, _) => action.create(fullSampleActionName, file)(wp)
+ }
+
+ val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+ wsk.action.get(fullyQualifiedActionName)(defaultWskProps)
+ wsk.action.delete(fullyQualifiedActionName, expectedExitCode = FORBIDDEN)(defaultWskProps)
+ }
+
+ it should "reject create action in shared package not owned by authkey" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, name) => pkg.create(name, shared = Some(true))(wp)
+ }
+
+ val fullyQualifiedActionName = s"/$guestNamespace/notallowed"
+ val file = Some(TestUtils.getTestActionFilename("empty.js"))
+
+ withAssetCleaner(defaultWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, fullyQualifiedActionName, confirmDelete = false) {
+ (action, name) => action.create(name, file, expectedExitCode = FORBIDDEN)(wp)
+ }
+ }
+ }
+
+ it should "reject update action in shared package not owned by authkey" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+ }
+
+ assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+ val file = Some(TestUtils.getTestActionFilename("empty.js"))
+ (action, _) => action.create(fullSampleActionName, file)(wp)
+ }
+
+ val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+ wsk.action.create(fullyQualifiedActionName, None, update = true, expectedExitCode = FORBIDDEN)(defaultWskProps)
+ }
+
+ behavior of "Wsk Package Listing"
+
+ it should "list shared packages" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+ }
+
+ val fullyQualifiedPackageName = s"/$guestNamespace/$samplePackage"
+ val result = wsk.pkg.list(Some(s"/$guestNamespace"))(defaultWskProps).stdout
+ result should include regex (fullyQualifiedPackageName + """\s+shared""")
+ }
+
+ it should "not list private packages" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) => pkg.create(samplePackage)(wp)
+ }
+
+ val fullyQualifiedPackageName = s"/$guestNamespace/$samplePackage"
+ val result = wsk.pkg.list(Some(s"/$guestNamespace"))(defaultWskProps).stdout
+ result should not include regex(fullyQualifiedPackageName)
+ }
+
+ it should "list shared package actions" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+ }
+
+ assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+ val file = Some(TestUtils.getTestActionFilename("empty.js"))
+ (action, _) => action.create(fullSampleActionName, file, kind = Some("nodejs"))(wp)
+ }
+
+ val fullyQualifiedPackageName = s"/$guestNamespace/$samplePackage"
+ val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+ val result = wsk.action.list(Some(fullyQualifiedPackageName))(defaultWskProps).stdout
+ result should include regex (fullyQualifiedActionName)
+ }
+
+ behavior of "Wsk Package Binding"
+
+ it should "create a package binding" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+ }
+
+ val name = "bindPackage"
+ val annotations = Map("a" -> "A".toJson, WhiskPackage.bindingFieldName -> "xxx".toJson)
+ val provider = s"/$guestNamespace/$samplePackage"
+ withAssetCleaner(defaultWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, name) {
+ (pkg, _) => pkg.bind(provider, name, annotations = annotations)(wp)
+ }
+
+ val stdout = wsk.pkg.get(name)(defaultWskProps).stdout
+ val annotationString = wsk.parseJsonString(stdout).fields("annotations").toString
+ annotationString should include regex (""""key":"a"""")
+ annotationString should include regex (""""value":"A"""")
+ annotationString should include regex (s""""key":"${WhiskPackage.bindingFieldName}"""")
+ annotationString should not include regex(""""key":"xxx"""")
+ annotationString should include regex (s""""name":"${samplePackage}"""")
+ }
+ }
+
+ it should "not create a package binding for private package" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) => pkg.create(samplePackage, shared = Some(false))(wp)
+ }
+
+ val name = "bindPackage"
+ val provider = s"/$guestNamespace/$samplePackage"
+ withAssetCleaner(defaultWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, name, confirmDelete = false) {
+ (pkg, _) => pkg.bind(provider, name, expectedExitCode = FORBIDDEN)(wp)
+ }
+ }
+ }
+
+ behavior of "Wsk Package Action"
+
+ it should "get and invoke an action from package" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) => pkg.create(samplePackage, parameters = Map("a" -> "A".toJson), shared = Some(true))(wp)
+ }
+
+ assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ (action, _) => action.create(fullSampleActionName, file)(wp)
+ }
+
+ val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+ val stdout = wsk.action.get(fullyQualifiedActionName)(defaultWskProps).stdout
+ stdout should include("name")
+ stdout should include("parameters")
+ stdout should include("limits")
+ stdout should include regex (""""key": "a"""")
+ stdout should include regex (""""value": "A"""")
+
+ val run = wsk.action.invoke(fullyQualifiedActionName)(defaultWskProps)
+
+ withActivation(wsk.activation, run)({
+ _.response.success shouldBe true
+ })(defaultWskProps)
+ }
+
+ it should "invoke an action sequence from package" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) => pkg.create(samplePackage, parameters = Map("a" -> "A".toJson), shared = Some(true))(wp)
+ }
+
+ assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ (action, _) => action.create(fullSampleActionName, file)(wp)
+ }
+
+ withAssetCleaner(defaultWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, "sequence") {
+ (action, name) =>
+ val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+ action.create(name, Some(fullyQualifiedActionName), kind = Some("sequence"), update = true)(wp)
+ }
+
+ val run = wsk.action.invoke("sequence")(defaultWskProps)
+ withActivation(wsk.activation, run)({
+ _.response.success shouldBe true
+ })(defaultWskProps)
+ }
+ }
+
+ it should "not allow invoke an action sequence with more than one component from package after entitlement change" in withAssetCleaner(guestWskProps) {
+ (guestwp, assetHelper) =>
+ val privateSamplePackage = samplePackage + "prv"
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) =>
+ pkg.create(samplePackage, parameters = Map("a" -> "A".toJson), shared = Some(true))(guestwp)
+ pkg.create(privateSamplePackage, parameters = Map("a" -> "A".toJson), shared = Some(true))(guestwp)
+ }
+
+ assetHelper.withCleaner(wsk.action, fullSampleActionName) {
+ val file = Some(TestUtils.getTestActionFilename("hello.js"))
+ (action, _) =>
+ action.create(fullSampleActionName, file)(guestwp)
+ action.create(s"$privateSamplePackage/$sampleAction", file)(guestwp)
+ }
+
+ withAssetCleaner(defaultWskProps) {
+ (dwp, assetHelper) =>
+ assetHelper.withCleaner(wsk.action, "sequence") {
+ (action, name) =>
+ val fullyQualifiedActionName = s"/$guestNamespace/$fullSampleActionName"
+ val fullyQualifiedActionName2 = s"/$guestNamespace/$privateSamplePackage/$sampleAction"
+ action.create(name, Some(s"$fullyQualifiedActionName,$fullyQualifiedActionName2"),
+ kind = Some("sequence"))(dwp)
+ }
+
+ // change package visibility
+ wsk.pkg.create(privateSamplePackage, update = true, shared = Some(false))(guestwp)
+ wsk.action.invoke("sequence", expectedExitCode = FORBIDDEN)(defaultWskProps)
+ }
+ }
+
+ it should "invoke a packaged action not owned by the subject to get the subject's namespace" in withAssetCleaner(guestWskProps) {
+ (_, assetHelper) =>
+ val packageName = "namespacePackage"
+ val actionName = "namespaceAction"
+ val packagedActionName = s"$packageName/$actionName"
+
+ assetHelper.withCleaner(wsk.pkg, packageName) {
+ (pkg, _) => pkg.create(packageName, shared = Some(true))(guestWskProps)
+ }
+
+ assetHelper.withCleaner(wsk.action, packagedActionName) {
+ val file = Some(TestUtils.getTestActionFilename("helloContext.js"))
+ (action, _) => action.create(packagedActionName, file)(guestWskProps)
+ }
+
+ val fullyQualifiedActionName = s"/$guestNamespace/$packagedActionName"
+ val run = wsk.action.invoke(fullyQualifiedActionName)(defaultWskProps)
+
+ withActivation(wsk.activation, run)({ activation =>
+ val (_, namespace) = WskAdmin.getUser(defaultWskProps.authKey)
+ activation.response.success shouldBe true
+ activation.response.result.get.toString should include regex (s""""namespace":\\s*"$namespace"""")
+ })(defaultWskProps)
+ }
+
+ behavior of "Wsk Trigger Feed"
+
+ it should "not create a trigger with timeout error when feed fails to initialize" in withAssetCleaner(guestWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.pkg, samplePackage) {
+ (pkg, _) => pkg.create(samplePackage, shared = Some(true))(wp)
+ }
+
+ val sampleFeed = s"$samplePackage/sampleFeed"
+ assetHelper.withCleaner(wsk.action, sampleFeed) {
+ val file = Some(TestUtils.getTestActionFilename("empty.js"))
+ (action, _) => action.create(sampleFeed, file, kind = Some("nodejs"))(wp)
+ }
+
+ val fullyQualifiedFeedName = s"/$guestNamespace/$sampleFeed"
+ withAssetCleaner(defaultWskProps) {
+ (wp, assetHelper) =>
+ assetHelper.withCleaner(wsk.trigger, "badfeed", confirmDelete = false) {
+ (trigger, name) => trigger.create(name, feed = Some(fullyQualifiedFeedName), expectedExitCode = TIMEOUT)(wp)
+ }
+ wsk.trigger.get("badfeed", expectedExitCode = NOT_FOUND)(wp)
+ }
+ }
+
+}
diff --git a/tests/src/test/scala/whisk/core/cli/test/WskWebActionsTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskWebActionsTests.scala
new file mode 100644
index 0000000..bdb75f7
--- /dev/null
+++ b/tests/src/test/scala/whisk/core/cli/test/WskWebActionsTests.scala
@@ -0,0 +1,117 @@
+/*
+ * Copyright 2015-2016 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 whisk.core.cli.test
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import com.jayway.restassured.RestAssured
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import system.rest.RestUtil
+
+/**
+ * Tests web actions.
+ */
+@RunWith(classOf[JUnitRunner])
+class WskWebActionsTests
+ extends TestHelpers
+ with WskTestHelpers
+ with RestUtil {
+
+ val MAX_URL_LENGTH = 8192 // 8K matching nginx default
+
+ implicit val wskprops = WskProps()
+ val wsk = new Wsk
+ val namespace = WskAdmin.getUser(wskprops.authKey)._2
+
+ behavior of "Wsk Web Actions"
+
+ /**
+ * Tests web actions, plus max url limit.
+ */
+ it should "create a web action accessible via HTTPS" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "webaction"
+ val file = Some(TestUtils.getTestActionFilename("echo.js"))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, file, annotations = Map("web-export" -> true.toJson))
+ }
+
+ val host = getServiceURL()
+ val requestPath = host + s"/api/v1/experimental/web/$namespace/default/webaction.text/a?a="
+ val padAmount = MAX_URL_LENGTH - requestPath.length
+ Seq(("A", 200),
+ ("A" * padAmount, 200),
+ // ideally the bad case is just +1 but there's some differences
+ // in how characters are counted i.e., whether these count "https://:443"
+ // or not; it seems sufficient to test right around the boundary
+ ("A" * (padAmount + 100), 414))
+ .foreach {
+ case (pad, code) =>
+ val url = (requestPath + pad)
+ val response = RestAssured.given().config(sslconfig).get(url)
+ val responseCode = response.statusCode
+
+ withClue(s"response code: $responseCode, url length: ${url.length}, pad amount: ${pad.length}, url: $url") {
+ responseCode shouldBe code
+ if (code == 200) {
+ response.body().asString() shouldBe pad
+ } else {
+ response.body().asString() should include("414 Request-URI Too Large") // from nginx
+ }
+ }
+ }
+ }
+
+ /**
+ * Tests web action requiring authentication.
+ */
+ it should "create a web action requiring authentication accessible via HTTPS" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "webaction"
+ val file = Some(TestUtils.getTestActionFilename("echo.js"))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, file, annotations = Map("web-export" -> true.toJson, "require-whisk-auth" -> true.toJson))
+ }
+
+ val host = getServiceURL()
+ val url = host + s"/api/v1/experimental/web/$namespace/default/webaction.text/__ow_meta_namespace"
+
+ val unauthorizedResponse = RestAssured.given().config(sslconfig).get(url)
+ unauthorizedResponse.statusCode shouldBe 401
+
+ val authorizedResponse = RestAssured
+ .given()
+ .config(sslconfig)
+ .auth().preemptive().basic(wskprops.authKey.split(":")(0), wskprops.authKey.split(":")(1))
+ .get(url)
+ authorizedResponse.statusCode shouldBe 200
+ authorizedResponse.body().asString() shouldBe namespace
+ }
+}
--
To stop receiving notification emails like this one, please contact
"commits@openwhisk.apache.org" <co...@openwhisk.apache.org>.