| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package org.apache.openwhisk.core.cli.test |
| |
| import java.time.Instant |
| import java.net.URLEncoder |
| import java.nio.charset.StandardCharsets |
| import java.time.Clock |
| import java.io.File |
| |
| import scala.language.postfixOps |
| import scala.concurrent.duration.Duration |
| import scala.concurrent.duration.DurationInt |
| import scala.util.Random |
| 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 org.apache.openwhisk.core.entity._ |
| import org.apache.openwhisk.core.entity.ConcurrencyLimit._ |
| import org.apache.openwhisk.core.entity.LogLimit._ |
| import org.apache.openwhisk.core.entity.MemoryLimit._ |
| import org.apache.openwhisk.core.entity.TimeLimit._ |
| import org.apache.openwhisk.core.entity.size.SizeInt |
| import org.apache.openwhisk.utils.retry |
| import org.apache.openwhisk.core.cli.test.TestJsonArgs._ |
| import org.apache.openwhisk.http.Messages |
| |
| /** |
| * Tests for basic CLI usage. Some of these tests require a deployed backend. |
| */ |
| @RunWith(classOf[JUnitRunner]) |
| class WskCliBasicUsageTests extends TestHelpers with WskTestHelpers { |
| |
| implicit val wskprops = WskProps() |
| val wsk = new Wsk |
| val defaultAction = Some(TestUtils.getTestActionFilename("hello.js")) |
| val usrAgentHeaderRegEx = """\bUser-Agent\b": \[\s+"OpenWhisk\-CLI/1.\d+.*""" |
| val namespaceInvalidArgumentErrMsg = "error: Invalid argument(s): invalidArg. No arguments are required." |
| // certain environments may return router IP address instead of api_host string causing a failure |
| // Set apiHostCheck to false to avoid apihost check |
| val apiHostCheck = true |
| |
| // Some action invocation environments will not have an api key; so allow this check to be conditionally skipped |
| val apiKeyCheck = true |
| |
| val requireAPIKeyAnnotation = WhiskProperties.getBooleanProperty("whisk.feature.requireApiKeyAnnotation", true); |
| |
| behavior of "Wsk CLI usage" |
| |
| it should "show help and usage info" in { |
| val stdout = wsk.cli(Seq()).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+20.*""") |
| } |
| |
| 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 "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 "allow a 3 part Fully Qualified Name (FQN) without a leading '/'" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val guestNamespace = wsk.namespace.whois() |
| val packageName = withTimestamp("packageName3ptFQN") |
| val actionName = withTimestamp("actionName3ptFQN") |
| val triggerName = withTimestamp("triggerName3ptFQN") |
| val ruleName = withTimestamp("ruleName3ptFQN") |
| val fullQualifiedName = s"${guestNamespace}/${packageName}/${actionName}" |
| // Used for action and rule creation below |
| assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) => |
| pkg.create(packageName) |
| } |
| assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, _) => |
| trigger.create(triggerName) |
| } |
| // Test action and rule creation where action name is 3 part FQN w/out leading slash |
| assetHelper.withCleaner(wsk.action, fullQualifiedName) { (action, _) => |
| action.create(fullQualifiedName, defaultAction) |
| } |
| assetHelper.withCleaner(wsk.rule, ruleName) { (rule, _) => |
| rule.create(ruleName, trigger = triggerName, action = fullQualifiedName) |
| } |
| |
| wsk.action.invoke(fullQualifiedName).stdout should include(s"ok: invoked /$fullQualifiedName") |
| wsk.action.get(fullQualifiedName).stdout should include(s"ok: got action ${packageName}/${actionName}") |
| } |
| |
| it should "include CLI user agent headers with outbound requests" in { |
| val stdout = wsk |
| .cli(Seq("action", "list", "--auth", wskprops.authKey) ++ wskprops.overrides, verbose = true) |
| .stdout |
| stdout should include regex (usrAgentHeaderRegEx) |
| } |
| |
| behavior of "Wsk actions" |
| |
| it should "reject creating entities with invalid names" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| val names = Seq( |
| ("", ERROR_EXIT), |
| (" ", 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 { |
| val name = "notfound" |
| wsk.action |
| .create("missingFile", Some(name), expectedExitCode = MISUSE_EXIT) |
| .stderr should include(s"File '$name' is not a valid file or it does not exist") |
| } |
| |
| 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(name, Some("notfound"), update = true, expectedExitCode = MISUSE_EXIT) |
| } |
| |
| it should "reject action update for sequence with no components" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| val name = "updateMissingComponents" |
| val file = Some(TestUtils.getTestActionFilename("hello.js")) |
| assetHelper.withCleaner(wsk.action, name) { (action, name) => |
| action.create(name, file) |
| } |
| wsk.action.create(name, None, update = true, kind = Some("sequence"), 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.DeveloperError) |
| } |
| } |
| |
| 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.DeveloperError) |
| } |
| } |
| |
| 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.DeveloperError) |
| } |
| } |
| |
| it should "retrieve the last activation using --last flag" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| val auth: Seq[String] = Seq("--auth", wskprops.authKey) |
| val includeStr = "hello, undefined!" |
| |
| assetHelper.withCleaner(wsk.action, "lastName") { (action, _) => |
| wsk.action.create("lastName", defaultAction) |
| } |
| retry( |
| { |
| val lastInvoke = wsk.action.invoke("lastName") |
| withActivation(wsk.activation, lastInvoke) { |
| activation => |
| val lastFlag = Seq( |
| (Seq("activation", "get", "publish", "--last"), activation.activationId), |
| (Seq("activation", "get", "--last"), activation.activationId), |
| (Seq("activation", "logs", "--last"), includeStr), |
| (Seq("activation", "result", "--last"), includeStr)) |
| |
| retry({ |
| lastFlag foreach { |
| case (cmd, output) => |
| val stdout = wsk |
| .cli(cmd ++ wskprops.overrides ++ auth, expectedExitCode = SUCCESS_EXIT) |
| .stdout |
| stdout should include(output) |
| } |
| }, waitBeforeRetry = Some(500.milliseconds)) |
| |
| } |
| }, |
| waitBeforeRetry = Some(1.second), |
| N = 5) |
| } |
| |
| it should "ensure timestamp and stream are stripped from log lines" in withAssetCleaner(wskprops) { |
| val name = "activationLogStripTest" |
| val auth: Seq[String] = Seq("--auth", wskprops.authKey) |
| |
| (wp, assetHelper) => |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create(name, Some(TestUtils.getTestActionFilename("log.js"))) |
| } |
| |
| withActivation(wsk.activation, wsk.action.invoke(name)) { activation => |
| retry({ |
| val cmd = Seq("activation", "logs", "--strip", activation.activationId) |
| val run = wsk.cli(cmd ++ wskprops.overrides ++ auth, expectedExitCode = SUCCESS_EXIT) |
| run.stdout should { |
| be("this is stdout\nthis is stderr\n") or |
| be("this is stderr\nthis is stdout\n") |
| } |
| }, 120, Some(1.second)) |
| } |
| } |
| |
| 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 |
| } |
| } |
| |
| val concurrencyLimit = 500 |
| 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), |
| concurrency = Some(concurrencyLimit)) |
| } |
| |
| 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), |
| ConcurrencyLimit(concurrencyLimit)).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 "report error when creating an action with unknown kind" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val rr = assetHelper.withCleaner(wsk.action, "invalid kind", confirmDelete = false) { (action, name) => |
| action.create( |
| name, |
| Some(TestUtils.getTestActionFilename("echo.js")), |
| kind = Some("foobar"), |
| expectedExitCode = BAD_REQUEST) |
| } |
| rr.stderr should include regex "kind 'foobar' not in Set" |
| } |
| |
| it should "report error when creating an action with zip but without kind" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val name = "zipWithNoKind" |
| val zippedPythonAction = |
| Some(TestUtils.getTestActionFilename("python.zip")) |
| val createResult = |
| assetHelper.withCleaner(wsk.action, name, confirmDelete = false) { (action, _) => |
| action.create(name, zippedPythonAction, expectedExitCode = ANY_ERROR_EXIT) |
| } |
| |
| createResult.stderr should include regex "requires specifying the action kind" |
| } |
| |
| 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") |
| } |
| |
| it should "Ensure that zipped actions are encoded and uploaded as NodeJS actions" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val name = "zippedNpmAction" |
| val file = Some(TestUtils.getTestActionFilename("zippedaction.zip")) |
| |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create(name, file, kind = Some("nodejs:default")) |
| } |
| |
| withActivation(wsk.activation, wsk.action.invoke(name)) { activation => |
| val response = activation.response |
| response.result.get.fields.get("error") shouldBe empty |
| response.result.get.fields.get("author") shouldBe defined |
| } |
| } |
| |
| it should "create, and invoke an action that utilizes an invalid docker container with appropriate error" in withAssetCleaner( |
| wskprops) { |
| val name = "invalidDockerContainer" |
| 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, None, docker = Some(containerName)) |
| } |
| |
| val run = wsk.action.invoke(name) |
| withActivation(wsk.activation, run) { activation => |
| activation.response.status shouldBe ActivationResponse.messageForCode(ActivationResponse.DeveloperError) |
| 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 receiving context properties" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| val namespace = wsk.namespace.whois() |
| val name = "context" |
| |
| if (apiKeyCheck) { |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create( |
| name, |
| Some(TestUtils.getTestActionFilename("helloContext.js")), |
| annotations = Map(WhiskAction.provideApiKeyAnnotationName -> JsBoolean(true))) |
| } |
| } else { |
| 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]] |
| if (apiHostCheck) { |
| fields("api_host") shouldBe WhiskProperties.getApiHostForAction |
| } |
| if (apiKeyCheck) { |
| 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 successfully with options --blocking and --result" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val name = "invokeResult" |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create(name, Some(TestUtils.getTestActionFilename("echo.js"))) |
| } |
| val args = Map("hello" -> "Robert".toJson) |
| val run = wsk.action.invoke(name, args, blocking = true, result = true) |
| run.stdout.parseJson shouldBe args.toJson |
| } |
| |
| 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.DeveloperError) |
| 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)) |
| } |
| } |
| |
| it should "ensure --web flags set the proper annotations" 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) |
| } |
| |
| Seq("true", "faLse", "tRue", "nO", "yEs", "no", "raw", "NO", "Raw") |
| .foreach { flag => |
| val webEnabled = flag.toLowerCase == "true" || flag.toLowerCase == "yes" |
| val rawEnabled = flag.toLowerCase == "raw" |
| |
| wsk.action.create(name, file, web = Some(flag), update = true) |
| |
| val action = wsk.action.get(name) |
| |
| val baseAnnotations = Parameters("web-export", JsBoolean(webEnabled || rawEnabled)) ++ |
| Parameters("raw-http", JsBoolean(rawEnabled)) ++ |
| Parameters("final", JsBoolean(webEnabled || rawEnabled)) ++ |
| Parameters("exec", "nodejs:6") |
| val testAnnotations = if (requireAPIKeyAnnotation) { |
| baseAnnotations ++ Parameters(WhiskAction.provideApiKeyAnnotationName, JsFalse) |
| } else baseAnnotations |
| |
| removeCLIHeader(action.stdout).parseJson.asJsObject |
| .fields("annotations") |
| .convertTo[Set[JsObject]] shouldBe testAnnotations.toJsArray |
| .convertTo[Set[JsObject]] |
| } |
| } |
| |
| it should "ensure action update with --web flag only copies existing annotations when new annotations are not provided" in withAssetCleaner( |
| wskprops) { (wp, assetHelper) => |
| val name = "webaction" |
| val file = Some(TestUtils.getTestActionFilename("echo.js")) |
| val createKey = "createKey" |
| val createValue = JsString("createValue") |
| val updateKey = "updateKey" |
| val updateValue = JsString("updateValue") |
| val origKey = "origKey" |
| val origValue = JsString("origValue") |
| val overwrittenValue = JsString("overwrittenValue") |
| val createAnnots = Map(createKey -> createValue, origKey -> origValue) |
| val updateAnnots = |
| Map(updateKey -> updateValue, origKey -> overwrittenValue) |
| |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create(name, file, annotations = createAnnots) |
| } |
| |
| wsk.action.create(name, file, web = Some("true"), update = true) |
| |
| val existinAnnots = |
| wsk.action.get(name, fieldFilter = Some("annotations")).stdout |
| assert(existinAnnots.startsWith(s"ok: got action $name, displaying field annotations\n")) |
| removeCLIHeader(existinAnnots).parseJson shouldBe JsArray( |
| JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(true)), |
| JsObject("key" -> JsString(origKey), "value" -> origValue), |
| JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(false)), |
| JsObject("key" -> JsString("final"), "value" -> JsBoolean(true)), |
| JsObject("key" -> JsString(createKey), "value" -> createValue), |
| JsObject("key" -> JsString(WhiskAction.provideApiKeyAnnotationName), "value" -> JsBoolean(false)), |
| JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6"))) |
| |
| wsk.action.create(name, file, web = Some("true"), update = true, annotations = updateAnnots) |
| |
| val updatedAnnots = |
| wsk.action.get(name, fieldFilter = Some("annotations")).stdout |
| assert(updatedAnnots.startsWith(s"ok: got action $name, displaying field annotations\n")) |
| removeCLIHeader(updatedAnnots).parseJson shouldBe JsArray( |
| JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(true)), |
| JsObject("key" -> JsString(origKey), "value" -> overwrittenValue), |
| JsObject("key" -> JsString(updateKey), "value" -> updateValue), |
| JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(false)), |
| JsObject("key" -> JsString("final"), "value" -> JsBoolean(true)), |
| JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6"))) |
| } |
| |
| it should "ensure action update creates an action with --web flag" 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, web = Some("true"), update = true) |
| } |
| |
| val baseAnnotations = |
| Parameters("web-export", JsBoolean(true)) ++ |
| Parameters("raw-http", JsBoolean(false)) ++ |
| Parameters("final", JsBoolean(true)) |
| |
| val testAnnotations = if (requireAPIKeyAnnotation) { |
| baseAnnotations ++ |
| Parameters(WhiskAction.provideApiKeyAnnotationName, JsBoolean(false)) ++ |
| Parameters("exec", "nodejs:6") |
| } else { |
| baseAnnotations ++ |
| Parameters("exec", "nodejs:6") |
| } |
| |
| val action = wsk.action.get(name) |
| removeCLIHeader(action.stdout).parseJson.asJsObject |
| .fields("annotations") |
| .convertTo[Set[JsObject]] shouldBe testAnnotations.toJsArray |
| .convertTo[Set[JsObject]] |
| } |
| |
| it should "reject action create and update with invalid --web flag input" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val name = "webaction" |
| val file = Some(TestUtils.getTestActionFilename("echo.js")) |
| val invalidInput = "bogus" |
| val errorMsg = |
| s"Invalid argument '$invalidInput' for --web flag. Valid input consist of 'yes', 'true', 'raw', 'false', or 'no'." |
| wsk.action |
| .create(name, file, web = Some(invalidInput), expectedExitCode = MISUSE_EXIT) |
| .stderr should include(errorMsg) |
| wsk.action |
| .create(name, file, web = Some(invalidInput), update = true, expectedExitCode = MISUSE_EXIT) |
| .stderr should include(errorMsg) |
| } |
| |
| it should "reject action create and update when --web-secure used on a non-web action" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val name = withTimestamp("nonwebaction") |
| val file = Some(TestUtils.getTestActionFilename("echo.js")) |
| val errorMsg = |
| s"The --web-secure option is only valid when the --web option is enabled." |
| |
| // Create non-web action with --web-secure option -> fail |
| wsk.action |
| .create(name, file, websecure = Some("true"), expectedExitCode = MISUSE_EXIT) |
| .stderr should include(errorMsg) |
| |
| // Updating a existing non-web action should not allow the --web-secure option |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create(name, file) |
| } |
| wsk.action |
| .create(name, file, websecure = Some("true"), update = true, expectedExitCode = MISUSE_EXIT) |
| .stderr should include(errorMsg) |
| |
| // Updating an existing web action with --web false should not allow the --web-secure option |
| wsk.action.create(name, file, web = Some("true"), update = true) |
| wsk.action |
| .create( |
| name, |
| file, |
| web = Some("false"), |
| websecure = Some("true"), |
| update = true, |
| expectedExitCode = MISUSE_EXIT) |
| .stderr should include(errorMsg) |
| } |
| |
| it should "generate a require-whisk-annotation --web-secure used on a web action" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val name = withTimestamp("webaction") |
| val file = Some(TestUtils.getTestActionFilename("echo.js")) |
| val secretStr = "my-secret" |
| |
| // -web true --web-secure true -> annotation "require-whisk-auth" value is an int |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create(name, file, web = Some("true"), websecure = Some("true")) |
| } |
| var stdout = wsk.action.get(name, fieldFilter = Some("annotations")).stdout |
| var secretJsVar = removeCLIHeader(stdout).parseJson |
| .convertTo[JsArray] |
| .elements |
| .find({ |
| _.convertTo[JsObject].getFields("key").head == JsString("require-whisk-auth") |
| }) |
| var secretIsInt = |
| secretJsVar.get.convertTo[JsObject].getFields("value").head match { |
| case JsNumber(x) => true |
| case _ => false |
| } |
| secretIsInt shouldBe true |
| |
| // -web true --web-secure string -> annotation "require-whisk-auth" with a value of string |
| wsk.action.create(name, file, web = Some("true"), websecure = Some(s"$secretStr"), update = true) |
| stdout = wsk.action.get(name, fieldFilter = Some("annotations")).stdout |
| val actualAnnotations = |
| removeCLIHeader(stdout).parseJson.convertTo[JsArray].elements |
| actualAnnotations.contains(JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6"))) shouldBe true |
| actualAnnotations.contains(JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(true))) shouldBe true |
| actualAnnotations.contains(JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(false))) shouldBe true |
| actualAnnotations.contains(JsObject("key" -> JsString("final"), "value" -> JsBoolean(true))) shouldBe true |
| actualAnnotations.contains(JsObject("key" -> JsString("require-whisk-auth"), "value" -> JsString(s"$secretStr"))) shouldBe true |
| |
| // Updating web action multiple times with --web-secure true should not change the "require-whisk-auth" numeric value |
| wsk.action.create(name, file, web = Some("true"), websecure = Some("true"), update = true) |
| stdout = wsk.action.get(name, fieldFilter = Some("annotations")).stdout |
| val secretNumJsVar = removeCLIHeader(stdout).parseJson |
| .convertTo[JsArray] |
| .elements |
| .find({ |
| _.convertTo[JsObject].getFields("key").head == JsString("require-whisk-auth") |
| }) |
| wsk.action.create(name, file, web = Some("true"), websecure = Some("true"), update = true) |
| removeCLIHeader(stdout).parseJson |
| .convertTo[JsArray] |
| .elements |
| .find({ |
| _.convertTo[JsObject].getFields("key").head == JsString("require-whisk-auth") |
| }) shouldBe secretNumJsVar |
| } |
| |
| it should "remove existing require-whisk-annotation when --web-secure is false" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val name = withTimestamp("webaction") |
| val file = Some(TestUtils.getTestActionFilename("echo.js")) |
| val secretStr = "my-secret" |
| |
| // |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create(name, file, web = Some("true"), websecure = Some(s"$secretStr")) |
| } |
| var stdout = wsk.action.get(name, fieldFilter = Some("annotations")).stdout |
| var actualAnnotations = |
| removeCLIHeader(stdout).parseJson.convertTo[JsArray].elements |
| actualAnnotations.contains(JsObject("key" -> JsString("require-whisk-auth"), "value" -> JsString(s"$secretStr"))) shouldBe true |
| |
| wsk.action.create(name, file, web = Some("true"), websecure = Some("false"), update = true) |
| stdout = wsk.action.get(name, fieldFilter = Some("annotations")).stdout |
| actualAnnotations = removeCLIHeader(stdout).parseJson.convertTo[JsArray].elements |
| actualAnnotations.find({ |
| _.convertTo[JsObject].getFields("key").head == JsString("require-whisk-auth") |
| }) shouldBe None |
| } |
| |
| it should "ensure action update with --web-secure flag only copies existing annotations when new annotations are not provided" in withAssetCleaner( |
| wskprops) { (wp, assetHelper) => |
| val name = "webaction" |
| val file = Some(TestUtils.getTestActionFilename("echo.js")) |
| val createKey = "createKey" |
| val createValue = JsString("createValue") |
| val updateKey = "updateKey" |
| val updateValue = JsString("updateValue") |
| val origKey = "origKey" |
| val origValue = JsString("origValue") |
| val overwrittenValue = JsString("overwrittenValue") |
| val createAnnots = Map(createKey -> createValue, origKey -> origValue) |
| val updateAnnots = |
| Map(updateKey -> updateValue, origKey -> overwrittenValue) |
| val secretStr = "my-secret" |
| |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create(name, file, web = Some("true"), annotations = createAnnots) |
| } |
| |
| wsk.action.create(name, file, websecure = Some(secretStr), update = true) |
| var stdout = wsk.action.get(name, fieldFilter = Some("annotations")).stdout |
| var existingAnnotations = |
| removeCLIHeader(stdout).parseJson.convertTo[JsArray].elements |
| println("existingAnnotations: " + existingAnnotations) |
| existingAnnotations.contains(JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6"))) shouldBe true |
| existingAnnotations.contains(JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(true))) shouldBe true |
| existingAnnotations.contains(JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(false))) shouldBe true |
| existingAnnotations.contains(JsObject("key" -> JsString("final"), "value" -> JsBoolean(true))) shouldBe true |
| existingAnnotations.contains(JsObject("key" -> JsString("require-whisk-auth"), "value" -> JsString(secretStr))) shouldBe true |
| existingAnnotations.contains(JsObject("key" -> JsString(createKey), "value" -> createValue)) shouldBe true |
| existingAnnotations.contains(JsObject("key" -> JsString(origKey), "value" -> origValue)) shouldBe true |
| |
| wsk.action.create(name, file, websecure = Some(secretStr), update = true, annotations = updateAnnots) |
| stdout = wsk.action.get(name, fieldFilter = Some("annotations")).stdout |
| var updatedAnnotations = |
| removeCLIHeader(stdout).parseJson.convertTo[JsArray].elements |
| println("updatedAnnotations: " + updatedAnnotations) |
| updatedAnnotations.contains(JsObject("key" -> JsString("exec"), "value" -> JsString("nodejs:6"))) shouldBe true |
| updatedAnnotations.contains(JsObject("key" -> JsString("web-export"), "value" -> JsBoolean(true))) shouldBe true |
| updatedAnnotations.contains(JsObject("key" -> JsString("raw-http"), "value" -> JsBoolean(false))) shouldBe true |
| updatedAnnotations.contains(JsObject("key" -> JsString("final"), "value" -> JsBoolean(true))) shouldBe true |
| updatedAnnotations.contains(JsObject("key" -> JsString("require-whisk-auth"), "value" -> JsString(secretStr))) shouldBe true |
| updatedAnnotations.contains(JsObject("key" -> JsString(updateKey), "value" -> updateValue)) shouldBe true |
| updatedAnnotations.contains(JsObject("key" -> JsString(origKey), "value" -> overwrittenValue)) shouldBe true |
| } |
| |
| it should "invoke action while not encoding &, <, > characters" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| val name = "nonescape" |
| val file = Some(TestUtils.getTestActionFilename("hello.js")) |
| val nonescape = "&<>" |
| val input = Map("payload" -> nonescape.toJson) |
| val output = JsObject("payload" -> JsString(s"hello, $nonescape!")) |
| |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create(name, file) |
| } |
| |
| withActivation(wsk.activation, wsk.action.invoke(name, parameters = input)) { activation => |
| activation.response.success shouldBe true |
| activation.response.result shouldBe Some(output) |
| activation.logs.toList.flatten |
| .filter(_.contains(nonescape)) |
| .length shouldBe 1 |
| } |
| } |
| |
| it should "get an action URL" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| val actionName = withTimestamp("action name@_-.") |
| val packageName = withTimestamp("package name@_-.") |
| val defaultPackageName = "default" |
| val webActionName = withTimestamp("web action name@_-.") |
| val nonExistentActionName = withTimestamp("non-existence action") |
| val packagedAction = s"$packageName/$actionName" |
| val packagedWebAction = s"$packageName/$webActionName" |
| val namespace = wsk.namespace.whois() |
| val encodedActionName = URLEncoder |
| .encode(actionName, StandardCharsets.UTF_8.name) |
| .replace("+", "%20") |
| val encodedPackageName = URLEncoder |
| .encode(packageName, StandardCharsets.UTF_8.name) |
| .replace("+", "%20") |
| val encodedWebActionName = URLEncoder |
| .encode(webActionName, StandardCharsets.UTF_8.name) |
| .replace("+", "%20") |
| val encodedNamespace = URLEncoder |
| .encode(namespace, StandardCharsets.UTF_8.name) |
| .replace("+", "%20") |
| val scheme = "https" |
| val actionPath = "%s://%s/api/%s/namespaces/%s/actions/%s" |
| val packagedActionPath = s"$actionPath/%s" |
| val webActionPath = "%s://%s/api/%s/web/%s/%s/%s" |
| |
| assetHelper.withCleaner(wsk.action, actionName) { (action, _) => |
| action.create(actionName, defaultAction) |
| } |
| |
| assetHelper.withCleaner(wsk.action, webActionName) { (action, _) => |
| action.create(webActionName, defaultAction, web = Some("true")) |
| } |
| |
| assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, _) => |
| pkg.create(packageName) |
| } |
| |
| assetHelper.withCleaner(wsk.action, packagedAction) { (action, _) => |
| action.create(packagedAction, defaultAction) |
| } |
| |
| assetHelper.withCleaner(wsk.action, packagedWebAction) { (action, _) => |
| action.create(packagedWebAction, defaultAction, web = Some("true")) |
| } |
| |
| wsk.action.get(actionName, url = Some(true)).stdout should include( |
| actionPath.format(scheme, wskprops.apihost, wskprops.apiversion, encodedNamespace, encodedActionName)) |
| |
| // Ensure url flag works when a field filter and summary flag are specified |
| wsk.action |
| .get(actionName, url = Some(true), fieldFilter = Some("field"), summary = true) |
| .stdout should include( |
| actionPath.format(scheme, wskprops.apihost, wskprops.apiversion, encodedNamespace, encodedActionName)) |
| |
| wsk.action.get(webActionName, url = Some(true)).stdout should include( |
| webActionPath |
| .format( |
| scheme, |
| wskprops.apihost, |
| wskprops.apiversion, |
| encodedNamespace, |
| defaultPackageName, |
| encodedWebActionName)) |
| |
| wsk.action.get(packagedAction, url = Some(true)).stdout should include( |
| packagedActionPath |
| .format(scheme, wskprops.apihost, wskprops.apiversion, encodedNamespace, encodedPackageName, encodedActionName)) |
| |
| wsk.action.get(packagedWebAction, url = Some(true)).stdout should include( |
| webActionPath |
| .format( |
| scheme, |
| wskprops.apihost, |
| wskprops.apiversion, |
| encodedNamespace, |
| encodedPackageName, |
| encodedWebActionName)) |
| |
| wsk.action.get(nonExistentActionName, url = Some(true), expectedExitCode = NOT_FOUND) |
| |
| val httpsProps = WskProps(apihost = "https://" + wskprops.apihost, authKey = wskprops.authKey) |
| wsk.action |
| .get(actionName, url = Some(true))(httpsProps) |
| .stdout should include( |
| actionPath |
| .format("https", wskprops.apihost, wskprops.apiversion, encodedNamespace, encodedActionName)) |
| wsk.action |
| .get(webActionName, url = Some(true))(httpsProps) |
| .stdout should include( |
| webActionPath |
| .format( |
| "https", |
| wskprops.apihost, |
| wskprops.apiversion, |
| encodedNamespace, |
| defaultPackageName, |
| encodedWebActionName)) |
| |
| } |
| it should "limit length of HTTP request and response bodies for --verbose" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val name = "limitVerbose" |
| val msg = "will be truncated" |
| val params = Seq("-p", "bigValue", "a" * 1000) |
| |
| assetHelper.withCleaner(wsk.action, name) { (action, _) => |
| action.create(name, Some(TestUtils.getTestActionFilename("echo.js"))) |
| } |
| |
| val truncated = |
| wsk |
| .cli(Seq("action", "invoke", name, "-b", "-v", "--auth", wskprops.authKey) ++ params ++ wskprops.overrides) |
| .stdout |
| msg.r.findAllIn(truncated).length shouldBe 2 |
| |
| val notTruncated = |
| wsk |
| .cli(Seq("action", "invoke", name, "-b", "-d", "--auth", wskprops.authKey) ++ params ++ wskprops.overrides) |
| .stdout |
| msg.r.findAllIn(notTruncated).length shouldBe 0 |
| } |
| |
| it should "denote bound and finalized action parameters for action summaries" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val nameBoundParams = "actionBoundParams" |
| val nameFinalParams = "actionFinalParams" |
| val paramAnnot = "paramAnnot" |
| val paramOverlap = "paramOverlap" |
| val paramBound = "paramBound" |
| val annots = Map( |
| "parameters" -> JsArray( |
| JsObject("name" -> JsString(paramAnnot), "description" -> JsString("Annotated")), |
| JsObject("name" -> JsString(paramOverlap), "description" -> JsString("Annotated And Bound")))) |
| val annotsFinal = Map( |
| "final" -> JsBoolean(true), |
| "parameters" -> JsArray( |
| JsObject("name" -> JsString(paramAnnot), "description" -> JsString("Annotated Parameter description")), |
| JsObject("name" -> JsString(paramOverlap), "description" -> JsString("Annotated And Bound")))) |
| val paramsBound = Map(paramBound -> JsString("Bound"), paramOverlap -> JsString("Bound And Annotated")) |
| |
| assetHelper.withCleaner(wsk.action, nameBoundParams) { (action, _) => |
| action.create(nameBoundParams, defaultAction, annotations = annots, parameters = paramsBound) |
| } |
| assetHelper.withCleaner(wsk.action, nameFinalParams) { (action, _) => |
| action.create(nameFinalParams, defaultAction, annotations = annotsFinal, parameters = paramsBound) |
| } |
| |
| val stdoutBound = wsk.action.get(nameBoundParams, summary = true).stdout |
| val stdoutFinal = wsk.action.get(nameFinalParams, summary = true).stdout |
| |
| stdoutBound should include(s"(parameters: $paramAnnot, *$paramBound, *$paramOverlap)") |
| stdoutFinal should include(s"(parameters: $paramAnnot, **$paramBound, **$paramOverlap)") |
| } |
| |
| it should "create, and get an action summary without a description and/or defined parameters" in withAssetCleaner( |
| wskprops) { (wp, assetHelper) => |
| val actNameNoParams = "actionNoParams" |
| val actNameNoDesc = "actionNoDesc" |
| val actNameNoDescOrParams = "actionNoDescOrParams" |
| val desc = "Action description" |
| val descFromParamsResp = "Returns a result based on parameters" |
| val annotsNoParams = Map("description" -> JsString(desc)) |
| val annotsNoDesc = Map( |
| "parameters" -> JsArray( |
| JsObject("name" -> JsString("paramName1"), "description" -> JsString("Parameter description 1")), |
| JsObject("name" -> JsString("paramName2"), "description" -> JsString("Parameter description 2")))) |
| |
| assetHelper.withCleaner(wsk.action, actNameNoDesc) { (action, _) => |
| action.create(actNameNoDesc, defaultAction, annotations = annotsNoDesc) |
| } |
| assetHelper.withCleaner(wsk.action, actNameNoParams) { (action, _) => |
| action.create(actNameNoParams, defaultAction, annotations = annotsNoParams) |
| } |
| assetHelper.withCleaner(wsk.action, actNameNoDescOrParams) { (action, _) => |
| action.create(actNameNoDescOrParams, defaultAction) |
| } |
| |
| val stdoutNoDesc = wsk.action.get(actNameNoDesc, summary = true).stdout |
| val stdoutNoParams = wsk.action.get(actNameNoParams, summary = true).stdout |
| val stdoutNoDescOrParams = |
| wsk.action.get(actNameNoDescOrParams, summary = true).stdout |
| val namespace = wsk.namespace.whois() |
| |
| stdoutNoDesc should include regex (s"(?i)action /${namespace}/${actNameNoDesc}: ${descFromParamsResp} paramName1 and paramName2\\s*\\(parameters: paramName1, paramName2\\)") |
| stdoutNoParams should include regex (s"(?i)action /${namespace}/${actNameNoParams}: ${desc}\\s*\\(parameters: none defined\\)") |
| stdoutNoDescOrParams should include regex (s"(?i)action /${namespace}/${actNameNoDescOrParams}\\s*\\(parameters: none defined\\)") |
| } |
| |
| it should "save action code to file" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| val seqName = "seqName" |
| val dockerName = "dockerName" |
| val containerName = |
| s"bogus${Random.alphanumeric.take(16).mkString.toLowerCase}" |
| val badSaveName = s"bad-directory${File.separator}action" |
| |
| Seq( |
| ("saveAction", true, false, false), |
| ("saveAsAction", false, true, false), |
| ("dockerSaveAction", true, false, true), |
| ("dockerSaveAsAction", true, true, false)).foreach { |
| case (actionName, save, saveAs, blackbox) => |
| assetHelper.withCleaner(wsk.action, actionName) { (action, _) => |
| blackbox match { |
| case false => action.create(actionName, defaultAction, update = true) |
| case true => action.create(actionName, defaultAction, update = true, docker = Some("bogus")) |
| } |
| } |
| |
| val saveMsg: String = if (save) { |
| wsk.action.get(actionName, save = Some(true)).stdout |
| } else { |
| wsk.action.get(actionName, saveAs = Some(s"save-as-$actionName")).stdout |
| } |
| |
| saveMsg should include(s"saved action code to ") |
| |
| val savePath = saveMsg.split("ok: saved action code to ")(1).trim() |
| val saveFile = new File(savePath) |
| |
| try { |
| saveFile.exists shouldBe true |
| |
| // Test for failure saving file when it already exist |
| if (save) { |
| if (blackbox) { |
| wsk.action |
| .get(actionName, save = Some(true), expectedExitCode = MISUSE_EXIT) |
| .stderr should include(s"The file '$actionName' already exists") |
| } else { |
| wsk.action |
| .get(actionName, save = Some(true), expectedExitCode = MISUSE_EXIT) |
| .stderr should include(s"The file '$actionName.js' already exists") |
| } |
| } else { |
| wsk.action |
| .get(actionName, saveAs = Some(s"save-as-$actionName"), expectedExitCode = MISUSE_EXIT) |
| .stderr should include(s"The file 'save-as-$actionName' already exists") |
| } |
| } finally { |
| saveFile.delete() |
| } |
| |
| // Test for failure when using an invalid filename |
| wsk.action |
| .get(actionName, saveAs = Some(badSaveName), expectedExitCode = MISUSE_EXIT) |
| .stderr should include(s"Cannot create file '$badSaveName'") |
| } |
| |
| // Test for failure saving Docker images |
| assetHelper.withCleaner(wsk.action, dockerName) { (action, _) => |
| action.create(dockerName, None, docker = Some(containerName)) |
| } |
| |
| wsk.action |
| .get(dockerName, save = Some(true), expectedExitCode = MISUSE_EXIT) |
| .stderr should include("Cannot save Docker images") |
| |
| wsk.action |
| .get(dockerName, saveAs = Some(dockerName), expectedExitCode = MISUSE_EXIT) |
| .stderr should include("Cannot save Docker images") |
| |
| // Test for failure saving sequences |
| assetHelper.withCleaner(wsk.action, seqName) { (action, _) => |
| action.create(seqName, Some(dockerName), kind = Some("sequence")) |
| } |
| |
| wsk.action |
| .get(seqName, save = Some(true), expectedExitCode = MISUSE_EXIT) |
| .stderr should include("Cannot save action sequences") |
| |
| wsk.action |
| .get(seqName, saveAs = Some(seqName), expectedExitCode = MISUSE_EXIT) |
| .stderr should include("Cannot save action sequences") |
| } |
| |
| 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) |
| |
| } |
| |
| it should "create, and get a package summary without a description and/or parameters" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val pkgNoDesc = "pkgNoDesc" |
| val pkgNoParams = "pkgNoParams" |
| val pkgNoDescOrParams = "pkgNoDescOrParams" |
| val pkgDesc = "Package description" |
| val descFromParams = "Returns a result based on parameters" |
| val namespace = wsk.namespace.whois() |
| val qualpkgNoDesc = s"/${namespace}/${pkgNoDesc}" |
| val qualpkgNoParams = s"/${namespace}/${pkgNoParams}" |
| val qualpkgNoDescOrParams = s"/${namespace}/${pkgNoDescOrParams}" |
| |
| val pkgAnnotsNoParams = Map("description" -> JsString(pkgDesc)) |
| val pkgAnnotsNoDesc = Map( |
| "parameters" -> JsArray( |
| JsObject("name" -> JsString("paramName1"), "description" -> JsString("Parameter description 1")), |
| JsObject("name" -> JsString("paramName2"), "description" -> JsString("Parameter description 2")))) |
| |
| assetHelper.withCleaner(wsk.pkg, pkgNoDesc) { (pkg, _) => |
| pkg.create(pkgNoDesc, annotations = pkgAnnotsNoDesc) |
| } |
| assetHelper.withCleaner(wsk.pkg, pkgNoParams) { (pkg, _) => |
| pkg.create(pkgNoParams, annotations = pkgAnnotsNoParams) |
| } |
| assetHelper.withCleaner(wsk.pkg, pkgNoDescOrParams) { (pkg, _) => |
| pkg.create(pkgNoDescOrParams) |
| } |
| |
| val stdoutNoDescPkg = wsk.pkg.get(pkgNoDesc, summary = true).stdout |
| val stdoutNoParamsPkg = wsk.pkg.get(pkgNoParams, summary = true).stdout |
| val stdoutNoDescOrParams = |
| wsk.pkg.get(pkgNoDescOrParams, summary = true).stdout |
| |
| stdoutNoDescPkg should include regex (s"(?i)package ${qualpkgNoDesc}: ${descFromParams} paramName1 and paramName2\\s*\\(parameters: paramName1, paramName2\\)") |
| stdoutNoParamsPkg should include regex (s"(?i)package ${qualpkgNoParams}: ${pkgDesc}\\s*\\(parameters: none defined\\)") |
| stdoutNoDescOrParams should include regex (s"(?i)package ${qualpkgNoDescOrParams}\\s*\\(parameters: none defined\\)") |
| } |
| |
| it should "denote bound package parameters for package summaries" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| val pkgBoundParams = "pkgBoundParams" |
| val pkgParamAnnot = "pkgParamAnnot" |
| val pkgParamOverlap = "pkgParamOverlap" |
| val pkgParamBound = "pkgParamBound" |
| val pkgAnnots = Map( |
| "parameters" -> JsArray( |
| JsObject("name" -> JsString(pkgParamAnnot), "description" -> JsString("Annotated")), |
| JsObject("name" -> JsString(pkgParamOverlap), "description" -> JsString("Annotated And Bound")))) |
| val pkgParamsBound = Map(pkgParamBound -> JsString("Bound"), pkgParamOverlap -> JsString("Bound And Annotated")) |
| |
| assetHelper.withCleaner(wsk.pkg, pkgBoundParams) { (pkg, _) => |
| pkg.create(pkgBoundParams, annotations = pkgAnnots, parameters = pkgParamsBound) |
| } |
| |
| val pkgStdoutBound = wsk.pkg.get(pkgBoundParams, summary = true).stdout |
| |
| pkgStdoutBound should include(s"(parameters: $pkgParamAnnot, *$pkgParamBound, *$pkgParamOverlap)") |
| } |
| |
| 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 = wsk.namespace.whois() |
| val stdout = wsk.trigger.get(triggerName, summary = true).stdout |
| stdout should include regex (s"(?i)trigger\\s+/$ns/$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) |
| } |
| } |
| |
| it should "invoke a feed action with the correct lifecyle event when creating, retrieving and deleting a feed trigger" in withAssetCleaner( |
| wskprops) { (wp, assetHelper) => |
| val actionName = withTimestamp("echo") |
| val triggerName = "feedTest" |
| |
| assetHelper.withCleaner(wsk.action, actionName) { (action, _) => |
| action.create(actionName, Some(TestUtils.getTestActionFilename("echo.js"))) |
| } |
| |
| try { |
| wsk.trigger |
| .create(triggerName, feed = Some(actionName)) |
| .stdout should include(""""lifecycleEvent": "CREATE"""") |
| |
| wsk.trigger.get(triggerName).stdout should include(""""lifecycleEvent": "READ"""") |
| |
| wsk.trigger.create(triggerName, update = true).stdout should include(""""lifecycleEvent": "UPDATE""") |
| } finally { |
| wsk.trigger.delete(triggerName).stdout should include(""""lifecycleEvent": "DELETE"""") |
| } |
| } |
| |
| it should "denote bound trigger parameters for trigger summaries" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| val trgBoundParams = "trgBoundParams" |
| val trgParamAnnot = "trgParamAnnot" |
| val trgParamOverlap = "trgParamOverlap" |
| val trgParamBound = "trgParamBound" |
| val trgAnnots = Map( |
| "parameters" -> JsArray( |
| JsObject("name" -> JsString(trgParamAnnot), "description" -> JsString("Annotated")), |
| JsObject("name" -> JsString(trgParamOverlap), "description" -> JsString("Annotated And Bound")))) |
| val trgParamsBound = Map(trgParamBound -> JsString("Bound"), trgParamOverlap -> JsString("Bound And Annotated")) |
| |
| assetHelper.withCleaner(wsk.trigger, trgBoundParams) { (trigger, _) => |
| trigger.create(trgBoundParams, annotations = trgAnnots, parameters = trgParamsBound) |
| } |
| |
| val trgStdoutBound = wsk.trigger.get(trgBoundParams, summary = true).stdout |
| |
| trgStdoutBound should include(s"(parameters: $trgParamAnnot, *$trgParamBound, *$trgParamOverlap)") |
| } |
| |
| it should "create, and get a trigger summary without a description and/or parameters" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| val trgNoDesc = "trgNoDesc" |
| val trgNoParams = "trgNoParams" |
| val trgNoDescOrParams = "trgNoDescOrParams" |
| val trgDesc = "Package description" |
| val descFromParams = "Returns a result based on parameters" |
| val namespace = wsk.namespace.whois() |
| val qualtrgNoDesc = s"/${namespace}/${trgNoDesc}" |
| val qualtrgNoParams = s"/${namespace}/${trgNoParams}" |
| val qualtrgNoDescOrParams = s"/${namespace}/${trgNoDescOrParams}" |
| |
| val trgAnnotsNoParams = Map("description" -> JsString(trgDesc)) |
| val trgAnnotsNoDesc = Map( |
| "parameters" -> JsArray( |
| JsObject("name" -> JsString("paramName1"), "description" -> JsString("Parameter description 1")), |
| JsObject("name" -> JsString("paramName2"), "description" -> JsString("Parameter description 2")))) |
| |
| assetHelper.withCleaner(wsk.trigger, trgNoDesc) { (trigger, _) => |
| trigger.create(trgNoDesc, annotations = trgAnnotsNoDesc) |
| } |
| assetHelper.withCleaner(wsk.trigger, trgNoParams) { (trigger, _) => |
| trigger.create(trgNoParams, annotations = trgAnnotsNoParams) |
| } |
| assetHelper.withCleaner(wsk.trigger, trgNoDescOrParams) { (trigger, _) => |
| trigger.create(trgNoDescOrParams) |
| } |
| |
| val stdoutNoDescPkg = wsk.trigger.get(trgNoDesc, summary = true).stdout |
| val stdoutNoParamsPkg = wsk.trigger.get(trgNoParams, summary = true).stdout |
| val stdoutNoDescOrParams = |
| wsk.trigger.get(trgNoDescOrParams, summary = true).stdout |
| |
| stdoutNoDescPkg should include regex (s"(?i)trigger ${qualtrgNoDesc}: ${descFromParams} paramName1 and paramName2\\s*\\(parameters: paramName1, paramName2\\)") |
| stdoutNoParamsPkg should include regex (s"(?i)trigger ${qualtrgNoParams}: ${trgDesc}\\s*\\(parameters: none defined\\)") |
| stdoutNoDescOrParams should include regex (s"(?i)trigger ${qualtrgNoDescOrParams}\\s*\\(parameters: none defined\\)") |
| } |
| |
| 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 = withTimestamp("listRulesTrigger") |
| val actionName = withTimestamp("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)) |
| } |
| |
| it should "return a list of alphabetized actions" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| // Declare 4 actions, create them out of alphabetical order |
| val actionName = withTimestamp("actionAlphaTest") |
| for (i <- 1 to 3) { |
| val name = s"$actionName$i" |
| assetHelper.withCleaner(wsk.action, name) { (action, name) => |
| action.create(name, defaultAction) |
| } |
| } |
| retry({ |
| val original = wsk.action.list(nameSort = Some(true)).stdout |
| // Create list with action names in correct order |
| val scalaSorted = |
| List(s"${actionName}1", s"${actionName}2", s"${actionName}3") |
| // Filter out everything not previously created |
| val regex = s"${actionName}[1-3]".r |
| // Retrieve action names into list as found in original |
| val list = (regex.findAllMatchIn(original)).toList |
| scalaSorted.toString shouldEqual list.toString |
| }, 5, Some(1 second)) |
| } |
| |
| it should "return an alphabetized list with default package actions on top" in withAssetCleaner(wskprops) { |
| (wp, assetHelper) => |
| // Declare 4 actions, create them out of alphabetical order |
| val actionName = withTimestamp("actionPackageAlphaTest") |
| val packageName = withTimestamp("packageAlphaTest") |
| assetHelper.withCleaner(wsk.action, actionName) { (action, actionName) => |
| action.create(actionName, defaultAction) |
| } |
| assetHelper.withCleaner(wsk.pkg, packageName) { (pkg, packageName) => |
| pkg.create(packageName) |
| } |
| for (i <- 1 to 3) { |
| val name = s"${packageName}/${actionName}$i" |
| assetHelper.withCleaner(wsk.action, name) { (action, name) => |
| action.create(name, defaultAction) |
| } |
| } |
| retry( |
| { |
| val original = wsk.action.list(nameSort = Some(true)).stdout |
| // Create list with action names in correct order |
| val scalaSorted = List( |
| s"$actionName", |
| s"${packageName}/${actionName}1", |
| s"${packageName}/${actionName}2", |
| s"${packageName}/${actionName}3") |
| // Filter out everything not previously created |
| val regexNoPackage = s"$actionName".r |
| val regexWithPackage = s"${packageName}/${actionName}[1-3]".r |
| // Retrieve action names into list as found in original |
| val list = regexNoPackage |
| .findFirstIn(original) |
| .get :: (regexWithPackage.findAllMatchIn(original)).toList |
| scalaSorted.toString shouldEqual list.toString |
| }, |
| 5, |
| Some(1 second)) |
| } |
| |
| it should "return a list of alphabetized packages" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| // Declare 3 packages, create them out of alphabetical order |
| val packageName = "pkgAlphaTest" |
| for (i <- 1 to 3) { |
| val name = s"$packageName$i" |
| assetHelper.withCleaner(wsk.pkg, name) { (pkg, name) => |
| pkg.create(name) |
| } |
| } |
| retry({ |
| val original = wsk.pkg.list(nameSort = Some(true)).stdout |
| // Create list with package names in correct order |
| val scalaSorted = |
| List(s"${packageName}1", s"${packageName}2", s"${packageName}3") |
| // Filter out everything not previously created |
| val regex = s"${packageName}[1-3]".r |
| // Retrieve package names into list as found in original |
| val list = (regex.findAllMatchIn(original)).toList |
| scalaSorted.toString shouldEqual list.toString |
| }, 5, Some(1 second)) |
| } |
| |
| it should "return a list of alphabetized triggers" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| // Declare 4 triggers, create them out of alphabetical order |
| val triggerName = "triggerAlphaTest" |
| for (i <- 1 to 3) { |
| val name = s"$triggerName$i" |
| assetHelper.withCleaner(wsk.trigger, name) { (trigger, name) => |
| trigger.create(name) |
| } |
| } |
| retry({ |
| val original = wsk.trigger.list(nameSort = Some(true)).stdout |
| // Create list with trigger names in correct order |
| val scalaSorted = |
| List(s"${triggerName}1", s"${triggerName}2", s"${triggerName}3") |
| // Filter out everything not previously created |
| val regex = s"${triggerName}[1-3]".r |
| // Retrieve trigger names into list as found in original |
| val list = (regex.findAllMatchIn(original)).toList |
| scalaSorted.toString shouldEqual list.toString |
| }, 5, Some(1 second)) |
| } |
| |
| it should "return a list of alphabetized rules" in withAssetCleaner(wskprops) { (wp, assetHelper) => |
| // Declare a trigger and an action for the purposes of creating rules |
| val triggerName = withTimestamp("listRulesTrigger") |
| val actionName = withTimestamp("listRulesAction") |
| |
| assetHelper.withCleaner(wsk.trigger, triggerName) { (trigger, name) => |
| trigger.create(name) |
| } |
| assetHelper.withCleaner(wsk.action, actionName) { (action, name) => |
| action.create(name, defaultAction) |
| } |
| // Declare 3 rules, create them out of alphabetical order |
| val ruleName = "ruleAlphaTest" |
| for (i <- 1 to 3) { |
| val name = s"$ruleName$i" |
| assetHelper.withCleaner(wsk.rule, name) { (rule, name) => |
| rule.create(name, trigger = triggerName, action = actionName) |
| } |
| } |
| retry({ |
| val original = wsk.rule.list(nameSort = Some(true)).stdout |
| // Create list with rule names in correct order |
| val scalaSorted = |
| List(s"${ruleName}1", s"${ruleName}2", s"${ruleName}3") |
| // Filter out everything not previously created |
| val regex = s"${ruleName}[1-3]".r |
| // Retrieve rule names into list as found in original |
| val list = (regex.findAllMatchIn(original)).toList |
| scalaSorted.toString shouldEqual list.toString |
| }) |
| } |
| |
| 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 code artifact are required." |
| val actionNameReqMsg = "An action name is required." |
| val actionOptMsg = "A code artifact 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 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 entityNameMsg = |
| s"An entity name, '$invalidArg', was provided instead of a namespace. Valid namespaces are of the following format: /NAMESPACE." |
| val invalidArgs = Seq( |
| (Seq("api", "create"), s"${tooFewArgsMsg} ${apiCreateReqMsg}"), |
| ( |
| Seq("api", "create", "/basepath", "/path", "GET", "action", invalidArg), |
| s"${tooManyArgsMsg}${invalidArg}. ${apiCreateReqMsg}"), |
| (Seq("api", "get"), s"${tooFewArgsMsg} ${apiGetReqMsg}"), |
| (Seq("api", "get", "/basepath", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${apiGetReqMsg}"), |
| (Seq("api", "delete"), s"${tooFewArgsMsg} ${apiDeleteReqMsg}"), |
| ( |
| Seq("api", "delete", "/basepath", "/path", "GET", invalidArg), |
| s"${tooManyArgsMsg}${invalidArg}. ${apiDeleteReqMsg}"), |
| ( |
| Seq("api", "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", invalidArg), s"${namespaceInvalidArgumentErrMsg}"), |
| (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", "list", invalidArg), entityNameMsg), |
| (Seq("package", "delete"), s"${tooFewArgsMsg} ${packageNameReqMsg}"), |
| (Seq("package", "delete", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}."), |
| (Seq("package", "refresh", "namespace", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${optNamespaceMsg}"), |
| (Seq("package", "refresh", invalidArg), entityNameMsg), |
| (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("rule", "list", invalidArg), entityNameMsg), |
| (Seq("trigger", "fire"), s"${tooFewArgsMsg} ${triggerNameReqMsg}"), |
| (Seq("trigger", "fire", "triggerName", invalidArg), s"${tooManyArgsMsg}${invalidArg}. ${triggerNameReqMsg}"), |
| (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}"), |
| (Seq("trigger", "list", invalidArg), entityNameMsg)) |
| |
| invalidArgs foreach { |
| case (cmd, err) => |
| withClue(cmd) { |
| val rr = wsk.cli(cmd ++ wskprops.overrides, expectedExitCode = ANY_ERROR_EXIT) |
| rr.exitCode should (be(ERROR_EXIT) or be(MISUSE_EXIT)) |
| rr.stderr should include(err) |
| rr.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, |
| concurrency: Option[Int] = 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(stdMemory).toMB.toInt.toJson, |
| "logs" -> logs.getOrElse(stdLogSize).toMB.toInt.toJson, |
| "concurrency" -> concurrency.getOrElse(stdConcurrent).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, |
| concurrency = concurrency, |
| expectedExitCode = DONTCARE_EXIT) |
| withClue( |
| s"create failed for parameters: timeout = $timeout, memory = $memory, logsize = $logs, concurrency = $concurrency:") { |
| 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(minMemory), Some(maxMemory)) |
| log <- Seq(None, Some(minLogSize), Some(maxLogSize)) |
| concurrency <- Seq(None, Some(minConcurrent), Some(maxConcurrent)) |
| } testLimit(time, mem, log, concurrency) |
| |
| // Assert that invalid permutation are rejected |
| testLimit(Some(0.milliseconds), None, None, None, BAD_REQUEST) |
| testLimit(Some(100.minutes), None, None, None, BAD_REQUEST) |
| testLimit(None, Some(0.MB), None, None, BAD_REQUEST) |
| testLimit(None, Some(32768.MB), None, None, BAD_REQUEST) |
| testLimit(None, None, Some(32768.MB), None, BAD_REQUEST) |
| testLimit(None, None, None, Some(5000), BAD_REQUEST) |
| } |
| } |