blob: 9a1a96e7227e7c0bd933217d8fb7421531d2cf6c [file] [log] [blame]
/*
* 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.controller.test
import java.time.Instant
import scala.concurrent.duration.DurationInt
import scala.language.postfixOps
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonMarshaller
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport.sprayJsonUnmarshaller
import akka.http.scaladsl.server.Route
import spray.json._
import spray.json.DefaultJsonProtocol._
import org.apache.openwhisk.core.controller.WhiskActionsApi
import org.apache.openwhisk.core.entity._
import org.apache.openwhisk.core.entity.size._
import org.apache.openwhisk.core.entitlement.Collection
import org.apache.openwhisk.http.ErrorResponse
import org.apache.openwhisk.http.Messages
import org.apache.openwhisk.core.database.UserContext
import akka.http.scaladsl.model.headers.RawHeader
import org.apache.commons.lang3.StringUtils
import org.apache.openwhisk.core.connector.ActivationMessage
import org.apache.openwhisk.core.entity.Attachments.Inline
import org.apache.openwhisk.core.entity.test.ExecHelpers
import org.scalatest.{FlatSpec, Matchers}
/**
* Tests Actions API.
*
* Unit tests of the controller service as a standalone component.
* These tests exercise a fresh instance of the service object in memory -- these
* tests do NOT communication with a whisk deployment.
*
*
* @Idioglossia
* "using Specification DSL to write unit tests, as in should, must, not, be"
* "using Specs2RouteTest DSL to chain HTTP requests for unit testing, as in ~>"
*/
@RunWith(classOf[JUnitRunner])
class ActionsApiTests extends ControllerTestCommon with WhiskActionsApi {
/** Actions API tests */
behavior of "Actions API"
val creds = WhiskAuthHelpers.newIdentity()
val context = UserContext(creds)
val namespace = EntityPath(creds.subject.asString)
val collectionPath = s"/${EntityPath.DEFAULT}/${collection.path}"
def aname() = MakeName.next("action_tests")
val actionLimit = Exec.sizeLimit
val parametersLimit = Parameters.sizeLimit
//// GET /actions
it should "return empty list when no actions exist" in {
implicit val tid = transid()
Get(collectionPath) ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
responseAs[List[JsObject]] shouldBe 'empty
}
}
it should "list actions by default namespace" in {
implicit val tid = transid()
val actions = (1 to 2).map { i =>
WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
}.toList
actions foreach {
put(entityStore, _)
}
waitOnView(entityStore, WhiskAction, namespace, 2)
Get(collectionPath) ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[JsObject]]
actions.length should be(response.length)
response should contain theSameElementsAs actions.map(_.summaryAsJson)
}
}
it should "reject list when limit is greater than maximum allowed value" in {
implicit val tid = transid()
val exceededMaxLimit = Collection.MAX_LIST_LIMIT + 1
val response = Get(s"$collectionPath?limit=$exceededMaxLimit") ~> Route.seal(routes(creds)) ~> check {
status should be(BadRequest)
responseAs[String] should include {
Messages.listLimitOutOfRange(Collection.ACTIONS, exceededMaxLimit, Collection.MAX_LIST_LIMIT)
}
}
}
it should "reject list when limit is not an integer" in {
implicit val tid = transid()
val notAnInteger = "string"
val response = Get(s"$collectionPath?limit=$notAnInteger") ~> Route.seal(routes(creds)) ~> check {
status should be(BadRequest)
responseAs[String] should include {
Messages.argumentNotInteger(Collection.ACTIONS, notAnInteger)
}
}
}
it should "reject list when skip is negative" in {
implicit val tid = transid()
val negativeSkip = -1
val response = Get(s"$collectionPath?skip=$negativeSkip") ~> Route.seal(routes(creds)) ~> check {
status should be(BadRequest)
responseAs[String] should include {
Messages.listSkipOutOfRange(Collection.ACTIONS, negativeSkip)
}
}
}
it should "reject list when skip is not an integer" in {
implicit val tid = transid()
val notAnInteger = "string"
val response = Get(s"$collectionPath?skip=$notAnInteger") ~> Route.seal(routes(creds)) ~> check {
status should be(BadRequest)
responseAs[String] should include {
Messages.argumentNotInteger(Collection.ACTIONS, notAnInteger)
}
}
}
// ?docs disabled
ignore should "list action by default namespace with full docs" in {
implicit val tid = transid()
val actions = (1 to 2).map { i =>
WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
}.toList
actions foreach {
put(entityStore, _)
}
waitOnView(entityStore, WhiskAction, namespace, 2)
Get(s"$collectionPath?docs=true") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[WhiskAction]]
actions.length should be(response.length)
response should contain theSameElementsAs actions.map(_.summaryAsJson)
}
}
it should "list action with explicit namespace" in {
implicit val tid = transid()
val actions = (1 to 2).map { i =>
WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
}.toList
actions foreach {
put(entityStore, _)
}
waitOnView(entityStore, WhiskAction, namespace, 2)
Get(s"/$namespace/${collection.path}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[JsObject]]
actions.length should be(response.length)
response should contain theSameElementsAs actions.map(_.summaryAsJson)
}
// it should "reject list action with explicit namespace not owned by subject" in {
val auser = WhiskAuthHelpers.newIdentity()
Get(s"/$namespace/${collection.path}") ~> Route.seal(routes(auser)) ~> check {
status should be(Forbidden)
}
}
it should "list should reject request with post" in {
implicit val tid = transid()
Post(collectionPath) ~> Route.seal(routes(creds)) ~> check {
status should be(MethodNotAllowed)
}
}
//// GET /actions/name
it should "get action by name in default namespace" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
put(entityStore, action)
Get(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
response should be(action)
}
}
it should "get action with updated field" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
put(entityStore, action)
// `updated` field should be compared with a document in DB
val a = get(entityStore, action.docid, WhiskAction)
Get(s"/$namespace/${collection.path}/${action.name}?code=false") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val responseJson = responseAs[JsObject]
responseJson.fields("updated").convertTo[Long] should be(a.updated.toEpochMilli)
}
Get(s"/$namespace/${collection.path}/${action.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val responseJson = responseAs[JsObject]
responseJson.fields("updated").convertTo[Long] should be(a.updated.toEpochMilli)
}
}
it should "ignore updated field when updating action" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault(""))
val dummyUpdated = WhiskEntity.currentMillis().toEpochMilli
val content = JsObject(
"exec" -> JsObject("code" -> "".toJson, "kind" -> action.exec.kind.toJson),
"updated" -> dummyUpdated.toJson)
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
response.updated.toEpochMilli should be > dummyUpdated
}
}
def getExecPermutations() = {
implicit val tid = transid()
// BlackBox: binary: true, main: bbMain
val bbAction1 = WhiskAction(namespace, aname(), bb("bb", "RHViZWU=", Some("bbMain")))
val bbAction1Content = Map("exec" -> Map(
"kind" -> Exec.BLACKBOX,
"code" -> "RHViZWU=",
"image" -> "bb",
"main" -> "bbMain")).toJson.asJsObject
val bbAction1ExecMetaData = blackBoxMetaData("bb", Some("bbMain"), true)
// BlackBox: binary: false, main: bbMain
val bbAction2 = WhiskAction(namespace, aname(), bb("bb", "", Some("bbMain")))
val bbAction2Content =
Map("exec" -> Map("kind" -> Exec.BLACKBOX, "code" -> "", "image" -> "bb", "main" -> "bbMain")).toJson.asJsObject
val bbAction2ExecMetaData = blackBoxMetaData("bb", Some("bbMain"), false)
// BlackBox: binary: true, no main
val bbAction3 = WhiskAction(namespace, aname(), bb("bb", "RHViZWU="))
val bbAction3Content =
Map("exec" -> Map("kind" -> Exec.BLACKBOX, "code" -> "RHViZWU=", "image" -> "bb")).toJson.asJsObject
val bbAction3ExecMetaData = blackBoxMetaData("bb", None, true)
// BlackBox: binary: false, no main
val bbAction4 = WhiskAction(namespace, aname(), bb("bb", ""))
val bbAction4Content = Map("exec" -> Map("kind" -> Exec.BLACKBOX, "code" -> "", "image" -> "bb")).toJson.asJsObject
val bbAction4ExecMetaData = blackBoxMetaData("bb", None, false)
// Attachment: binary: true, main: javaMain
val javaAction1 = WhiskAction(namespace, aname(), javaDefault("RHViZWU=", Some("javaMain")))
val javaAction1Content =
Map("exec" -> Map("kind" -> JAVA_DEFAULT, "code" -> "RHViZWU=", "main" -> "javaMain")).toJson.asJsObject
val javaAction1ExecMetaData = javaMetaData(Some("javaMain"), true)
// String: binary: true, main: jsMain
val jsAction1 = WhiskAction(namespace, aname(), jsDefault("RHViZWU=", Some("jsMain")))
val jsAction1Content =
Map("exec" -> Map("kind" -> NODEJS10, "code" -> "RHViZWU=", "main" -> "jsMain")).toJson.asJsObject
val jsAction1ExecMetaData = js10MetaData(Some("jsMain"), true)
// String: binary: false, main: jsMain
val jsAction2 = WhiskAction(namespace, aname(), jsDefault("", Some("jsMain")))
val jsAction2Content = Map("exec" -> Map("kind" -> NODEJS10, "code" -> "", "main" -> "jsMain")).toJson.asJsObject
val jsAction2ExecMetaData = js10MetaData(Some("jsMain"), false)
// String: binary: true, no main
val jsAction3 = WhiskAction(namespace, aname(), jsDefault("RHViZWU="))
val jsAction3Content = Map("exec" -> Map("kind" -> NODEJS10, "code" -> "RHViZWU=")).toJson.asJsObject
val jsAction3ExecMetaData = js10MetaData(None, true)
// String: binary: false, no main
val jsAction4 = WhiskAction(namespace, aname(), jsDefault(""))
val jsAction4Content = Map("exec" -> Map("kind" -> NODEJS10, "code" -> "")).toJson.asJsObject
val jsAction4ExecMetaData = js10MetaData(None, false)
// Sequence
val component = WhiskAction(namespace, aname(), jsDefault("??"))
put(entityStore, component)
val components = Vector(s"/$namespace/${component.name}").map(stringToFullyQualifiedName(_))
val seqAction = WhiskAction(namespace, aname(), sequence(components), seqParameters(components))
val seqActionContent = JsObject(
"exec" -> JsObject("kind" -> "sequence".toJson, "components" -> JsArray(s"/$namespace/${component.name}".toJson)))
val seqActionExecMetaData = sequenceMetaData(components)
Seq(
(bbAction1, bbAction1Content, bbAction1ExecMetaData),
(bbAction2, bbAction2Content, bbAction2ExecMetaData),
(bbAction3, bbAction3Content, bbAction3ExecMetaData),
(bbAction4, bbAction4Content, bbAction4ExecMetaData),
(javaAction1, javaAction1Content, javaAction1ExecMetaData),
(jsAction1, jsAction1Content, jsAction1ExecMetaData),
(jsAction2, jsAction2Content, jsAction2ExecMetaData),
(jsAction3, jsAction3Content, jsAction3ExecMetaData),
(jsAction4, jsAction4Content, jsAction4ExecMetaData),
(seqAction, seqActionContent, seqActionExecMetaData))
}
it should "get action using code query parameter" in {
implicit val tid = transid()
getExecPermutations.foreach {
case (action, content, execMetaData) =>
val expectedWhiskAction = WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(action.exec.kind))
val expectedWhiskActionMetaData = WhiskActionMetaData(
action.namespace,
action.name,
execMetaData,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(action.exec.kind))
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(response, expectedWhiskAction)
}
Get(s"$collectionPath/${action.name}?code=false") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val responseJson = responseAs[JsObject]
responseJson.fields("exec").asJsObject.fields should not(contain key "code")
val response = responseAs[WhiskActionMetaData]
checkWhiskEntityResponse(response, expectedWhiskActionMetaData)
}
Seq(s"$collectionPath/${action.name}", s"$collectionPath/${action.name}?code=true").foreach { path =>
Get(path) ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(response, expectedWhiskAction)
}
}
Delete(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(response, expectedWhiskAction)
}
}
}
it should "report NotFound for get non existent action" in {
implicit val tid = transid()
Get(s"$collectionPath/xyz") ~> Route.seal(routes(creds)) ~> check {
status should be(NotFound)
}
}
it should "report Conflict if the name was of a different type" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname())
put(entityStore, trigger)
Get(s"/$namespace/${collection.path}/${trigger.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(Conflict)
}
}
it should "reject long entity names" in {
implicit val tid = transid()
val longName = "a" * (EntityName.ENTITY_NAME_MAX_LENGTH + 1)
Get(s"/$longName/${collection.path}/$longName") ~> Route.seal(routes(creds)) ~> check {
status should be(BadRequest)
responseAs[String] shouldBe {
Messages.entityNameTooLong(
SizeError(namespaceDescriptionForSizeError, longName.length.B, EntityName.ENTITY_NAME_MAX_LENGTH.B))
}
}
Seq(
s"/$namespace/${collection.path}/$longName",
s"/$namespace/${collection.path}/pkg/$longName",
s"/$namespace/${collection.path}/$longName/a",
s"/$namespace/${collection.path}/$longName/$longName").foreach { p =>
Get(p) ~> Route.seal(routes(creds)) ~> check {
status should be(BadRequest)
responseAs[String] shouldBe {
Messages.entityNameTooLong(
SizeError(segmentDescriptionForSizeError, longName.length.B, EntityName.ENTITY_NAME_MAX_LENGTH.B))
}
}
}
}
//// DEL /actions/name
it should "delete action by name" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
put(entityStore, action)
// it should "reject delete action by name not owned by subject" in
val auser = WhiskAuthHelpers.newIdentity()
Get(s"/$namespace/${collection.path}/${action.name}") ~> Route.seal(routes(auser)) ~> check {
status should be(Forbidden)
}
Delete(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
response should be(action)
}
}
it should "report NotFound for delete non existent action" in {
implicit val tid = transid()
Delete(s"$collectionPath/xyz") ~> Route.seal(routes(creds)) ~> check {
status should be(NotFound)
}
}
//// PUT /actions/name
it should "put should reject request missing json content" in {
implicit val tid = transid()
Put(s"$collectionPath/xxx", "") ~> Route.seal(routes(creds)) ~> check {
val response = responseAs[String]
status should be(UnsupportedMediaType)
}
}
it should "put should reject request missing property exec" in {
implicit val tid = transid()
val content = """|{"name":"name","publish":true}""".stripMargin.parseJson.asJsObject
Put(s"$collectionPath/xxx", content) ~> Route.seal(routes(creds)) ~> check {
val response = responseAs[String]
status should be(BadRequest)
}
}
it should "put should reject request with malformed property exec" in {
implicit val tid = transid()
val content =
"""|{"name":"name",
|"publish":true,
|"exec":""}""".stripMargin.parseJson.asJsObject
Put(s"$collectionPath/xxx", content) ~> Route.seal(routes(creds)) ~> check {
val response = responseAs[String]
status should be(BadRequest)
}
}
it should "reject create with exec which is too big" in {
implicit val tid = transid()
val code = "a" * (actionLimit.toBytes.toInt + 1)
val exec: Exec = jsDefault(code)
val content = JsObject("exec" -> exec.toJson)
Put(s"$collectionPath/${aname()}", content) ~> Route.seal(routes(creds)) ~> check {
status should be(PayloadTooLarge)
responseAs[String] should include {
Messages.entityTooBig(SizeError(WhiskAction.execFieldName, exec.size, Exec.sizeLimit))
}
}
}
it should "reject exec with unknown or missing kind" in {
implicit val tid = transid()
Seq("", "foobar").foreach { kind =>
val content = s"""{"exec":{"kind": "$kind", "code":"??"}}""".stripMargin.parseJson.asJsObject
Put(s"$collectionPath/${aname()}", content) ~> Route.seal(routes(creds)) ~> check {
status should be(BadRequest)
responseAs[String] should include {
Messages.invalidRuntimeError(kind, Set.empty).dropRight(3)
}
}
}
}
it should "reject update with exec which is too big" in {
implicit val tid = transid()
val oldCode = "function main()"
val code = "a" * (actionLimit.toBytes.toInt + 1)
val action = WhiskAction(namespace, aname(), jsDefault("??"))
val exec: Exec = jsDefault(code)
val content = JsObject("exec" -> exec.toJson)
put(entityStore, action)
Put(s"$collectionPath/${action.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
status should be(PayloadTooLarge)
responseAs[String] should include {
Messages.entityTooBig(SizeError(WhiskAction.execFieldName, exec.size, Exec.sizeLimit))
}
}
}
it should "reject create with parameters which are too big" in {
implicit val tid = transid()
val keys: List[Long] =
List.range(Math.pow(10, 9) toLong, (parametersLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)
val parameters = keys map { key =>
Parameters(key.toString, "a" * 10)
} reduce (_ ++ _)
val content = s"""{"exec":{"kind":"nodejs:default","code":"??"},"parameters":$parameters}""".stripMargin
Put(s"$collectionPath/${aname()}", content.parseJson.asJsObject) ~> Route.seal(routes(creds)) ~> check {
status should be(PayloadTooLarge)
responseAs[String] should include {
Messages.entityTooBig(SizeError(WhiskEntity.paramsFieldName, parameters.size, Parameters.sizeLimit))
}
}
}
it should "reject create with annotations which are too big" in {
implicit val tid = transid()
val keys: List[Long] =
List.range(Math.pow(10, 9) toLong, (parametersLimit.toBytes / 20 + Math.pow(10, 9) + 2) toLong)
val annotations = keys map { key =>
Parameters(key.toString, "a" * 10)
} reduce (_ ++ _)
val content = s"""{"exec":{"kind":"nodejs:default","code":"??"},"annotations":$annotations}""".stripMargin
Put(s"$collectionPath/${aname()}", content.parseJson.asJsObject) ~> Route.seal(routes(creds)) ~> check {
status should be(PayloadTooLarge)
responseAs[String] should include {
Messages.entityTooBig(SizeError(WhiskEntity.annotationsFieldName, annotations.size, Parameters.sizeLimit))
}
}
}
it should "reject activation with entity which is too big" in {
implicit val tid = transid()
val code = "a" * (allowedActivationEntitySize.toInt + 1)
val content = s"""{"a":"$code"}""".stripMargin
Post(s"$collectionPath/${aname()}", content.parseJson.asJsObject) ~> Route.seal(routes(creds)) ~> check {
status should be(PayloadTooLarge)
responseAs[String] should include {
Messages.entityTooBig(
SizeError(fieldDescriptionForSizeError, (content.length).B, allowedActivationEntitySize.B))
}
}
}
it should "put should accept request with missing optional properties" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault(""))
// only a kind must be defined (code otherwise could be empty)
val content = JsObject("exec" -> JsObject("code" -> "".toJson, "kind" -> action.exec.kind.toJson))
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(NODEJS10)))
}
}
it should "put should accept blackbox exec with empty code property" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), bb("bb"))
val content = Map("exec" -> Map("kind" -> "blackbox", "code" -> "", "image" -> "bb")).toJson.asJsObject
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(BLACKBOX)))
response.exec shouldBe an[BlackBoxExec]
response.exec.asInstanceOf[BlackBoxExec].code shouldBe empty
}
}
it should "put should accept blackbox exec with non-empty code property" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), bb("bb", "cc"))
val content = Map("exec" -> Map("kind" -> "blackbox", "code" -> "cc", "image" -> "bb")).toJson.asJsObject
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(BLACKBOX)))
response.exec shouldBe an[BlackBoxExec]
val bb = response.exec.asInstanceOf[BlackBoxExec]
bb.code shouldBe Some(Inline("cc"))
bb.binary shouldBe false
}
}
// this test is to ensure pre-existing actions can continue to to opt-out of new system annotations
it should "preserve annotations on pre-existing actions" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault(""))
put(entityStore, action, false) // install the action into the database directly
var content = JsObject("exec" -> JsObject("code" -> "".toJson, "kind" -> action.exec.kind.toJson))
Put(s"$collectionPath/${action.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version.upPatch,
action.publish,
action.annotations ++ Parameters(WhiskAction.execFieldName, action.exec.kind)))
}
content = """{"annotations":[{"key":"a","value":"B"}]}""".parseJson.asJsObject
Put(s"$collectionPath/${action.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version.upPatch.upPatch,
action.publish,
action.annotations ++ Parameters("a", "B") ++ Parameters(WhiskAction.execFieldName, action.exec.kind)))
}
}
private implicit val fqnSerdes = FullyQualifiedEntityName.serdes
private def seqParameters(seq: Vector[FullyQualifiedEntityName]) =
Parameters("_actions", seq.map("/" + _.asString).toJson)
// this test is sneaky; the installation of the sequence is done directly in the db
// and api checks are skipped
it should "reset parameters when changing sequence action to non sequence" in {
implicit val tid = transid()
val components = Vector("x/a", "x/b").map(stringToFullyQualifiedName(_))
val action = WhiskAction(namespace, aname(), sequence(components), seqParameters(components))
val content = WhiskActionPut(Some(jsDefault("")))
put(entityStore, action, false)
// create an action sequence
Put(s"$collectionPath/${action.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status should be(OK)
val response = responseAs[WhiskAction]
response.exec.kind should be(NODEJS10)
response.parameters shouldBe Parameters()
}
}
// this test is sneaky; the installation of the sequence is done directly in the db
// and api checks are skipped
it should "preserve new parameters when changing sequence action to non sequence" in {
implicit val tid = transid()
val components = Vector("x/a", "x/b").map(stringToFullyQualifiedName(_))
val action = WhiskAction(namespace, aname(), sequence(components), seqParameters(components))
val content = WhiskActionPut(Some(jsDefault("")), parameters = Some(Parameters("a", "A")))
put(entityStore, action, false)
// create an action sequence
Put(s"$collectionPath/${action.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status should be(OK)
val response = responseAs[WhiskAction]
response.exec.kind should be(NODEJS10)
response.parameters should be(Parameters("a", "A"))
}
}
it should "put should accept request with parameters property" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
val content = WhiskActionPut(Some(action.exec), Some(action.parameters))
// it should "reject put action in namespace not owned by subject" in
val auser = WhiskAuthHelpers.newIdentity()
Put(s"/$namespace/${collection.path}/${action.name}", content) ~> Route.seal(routes(auser)) ~> check {
status should be(Forbidden)
}
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(NODEJS10)))
}
}
it should "put should reject request with parameters property as jsobject" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
val content = WhiskActionPut(Some(action.exec), Some(action.parameters))
val params = """{ "parameters": { "a": "b" } }""".parseJson.asJsObject
val json = JsObject(WhiskActionPut.serdes.write(content).asJsObject.fields ++ params.fields)
Put(s"$collectionPath/${action.name}", json) ~> Route.seal(routes(creds)) ~> check {
status should be(BadRequest)
}
}
it should "put should accept request with limits property" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
val content = WhiskActionPut(
Some(action.exec),
Some(action.parameters),
Some(
ActionLimitsOption(
Some(action.limits.timeout),
Some(action.limits.memory),
Some(action.limits.logs),
Some(action.limits.concurrency))))
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(NODEJS10)))
}
}
it should "put and then get an action from cache" in {
implicit val tid = transid()
val javaAction =
WhiskAction(namespace, aname(), javaDefault("ZHViZWU=", Some("hello")), annotations = Parameters("exec", "java"))
val nodeAction = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
val actions = Seq((javaAction, JAVA_DEFAULT), (nodeAction, NODEJS10))
actions.foreach {
case (action, kind) =>
val content = WhiskActionPut(
Some(action.exec),
Some(action.parameters),
Some(
ActionLimitsOption(
Some(action.limits.timeout),
Some(action.limits.memory),
Some(action.limits.logs),
Some(action.limits.concurrency))))
// first request invalidates any previous entries and caches new result
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(kind)))
}
stream.toString should include(s"caching ${CacheKey(action)}")
stream.toString should not include (s"invalidating ${CacheKey(action)} on delete")
stream.reset()
// second request should fetch from cache
Get(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(kind)))
}
stream.toString should include(s"serving from cache: ${CacheKey(action)}")
stream.reset()
// update should invalidate cache
Put(s"$collectionPath/${action.name}?overwrite=true", content) ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version.upPatch,
action.publish,
action.annotations ++ systemAnnotations(kind)))
}
stream.toString should include(s"entity exists, will try to update '$action'")
stream.toString should include(s"invalidating ${CacheKey(action)}")
stream.toString should include(s"caching ${CacheKey(action)}")
stream.reset()
// delete should invalidate cache
Delete(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version.upPatch,
action.publish,
action.annotations ++ systemAnnotations(kind)))
}
stream.toString should include(s"invalidating ${CacheKey(action)}")
stream.reset()
}
}
it should "put and then get an action with attachment from cache" in {
val javaAction =
WhiskAction(
namespace,
aname(),
javaDefault(nonInlinedCode(entityStore), Some("hello")),
annotations = Parameters("exec", "java"))
val nodeAction = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters("x", "b"))
val swiftAction = WhiskAction(namespace, aname(), swift(nonInlinedCode(entityStore)), Parameters("x", "b"))
val bbAction = WhiskAction(namespace, aname(), bb("bb", nonInlinedCode(entityStore), Some("bbMain")))
val actions = Seq((javaAction, JAVA_DEFAULT), (nodeAction, NODEJS10), (swiftAction, SWIFT4), (bbAction, BLACKBOX))
actions.foreach {
case (action, kind) =>
val content = WhiskActionPut(
Some(action.exec),
Some(action.parameters),
Some(
ActionLimitsOption(
Some(action.limits.timeout),
Some(action.limits.memory),
Some(action.limits.logs),
Some(action.limits.concurrency))))
val cacheKey = s"${CacheKey(action)}".replace("(", "\\(").replace(")", "\\)")
val expectedPutLog =
Seq(
s"uploading attachment '[\\w-]+' of document 'id: ${action.namespace}/${action.name}",
s"caching $cacheKey")
.mkString("(?s).*")
val notExpectedGetLog = Seq(
s"finding document: 'id: ${action.namespace}/${action.name}",
s"finding attachment '[\\w-/:]+' of document 'id: ${action.namespace}/${action.name}").mkString("(?s).*")
// first request invalidates any previous entries and caches new result
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(kind)))
}
stream.toString should not include (s"invalidating ${CacheKey(action)} on delete")
stream.toString should include regex (expectedPutLog)
stream.reset()
// second request should fetch from cache
Get(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(kind)))
}
stream.toString should include(s"serving from cache: ${CacheKey(action)}")
stream.toString should not include regex(notExpectedGetLog)
stream.reset()
// delete should invalidate cache
Delete(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(kind)))
}
stream.toString should include(s"invalidating ${CacheKey(action)}")
stream.reset()
}
}
it should "put and then get an action with inlined attachment" in {
assumeAttachmentInliningEnabled(entityStore)
val action =
WhiskAction(
namespace,
aname(),
javaDefault(encodedRandomBytes(inlinedAttachmentSize(entityStore)), Some("hello")),
annotations = Parameters("exec", "java"))
val content = WhiskActionPut(
Some(action.exec),
Some(action.parameters),
Some(
ActionLimitsOption(
Some(action.limits.timeout),
Some(action.limits.memory),
Some(action.limits.logs),
Some(action.limits.concurrency))))
val name = action.name
val cacheKey = s"${CacheKey(action)}".replace("(", "\\(").replace(")", "\\)")
val notExpectedGetLog = Seq(
s"finding document: 'id: ${action.namespace}/${action.name}",
s"finding attachment '[\\w-/:]+' of document 'id: ${action.namespace}/${action.name}").mkString("(?s).*")
// first request invalidates any previous entries and caches new result
Put(s"$collectionPath/$name", content) ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(JAVA_DEFAULT)))
}
stream.toString should not include (s"invalidating ${CacheKey(action)} on delete")
stream.toString should not include ("uploading attachment")
stream.reset()
// second request should fetch from cache
Get(s"$collectionPath/$name") ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(JAVA_DEFAULT)))
}
stream.toString should include(s"serving from cache: ${CacheKey(action)}")
stream.toString should not include regex(notExpectedGetLog)
stream.reset()
// delete should invalidate cache
Delete(s"$collectionPath/$name") ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(JAVA_DEFAULT)))
}
stream.toString should include(s"invalidating ${CacheKey(action)}")
stream.reset()
}
it should "get an action with attachment that is not cached" in {
implicit val tid = transid()
val nodeAction = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters("x", "b"))
val swiftAction = WhiskAction(namespace, aname(), swift(nonInlinedCode(entityStore)), Parameters("x", "b"))
val bbAction = WhiskAction(namespace, aname(), bb("bb", nonInlinedCode(entityStore), Some("bbMain")))
val actions = Seq((nodeAction, NODEJS10), (swiftAction, SWIFT4), (bbAction, BLACKBOX))
actions.foreach {
case (action, kind) =>
val content = WhiskActionPut(
Some(action.exec),
Some(action.parameters),
Some(
ActionLimitsOption(
Some(action.limits.timeout),
Some(action.limits.memory),
Some(action.limits.logs),
Some(action.limits.concurrency))))
val name = action.name
val cacheKey = s"${CacheKey(action)}".replace("(", "\\(").replace(")", "\\)")
val expectedGetLog = Seq(
s"finding document: 'id: ${action.namespace}/${action.name}",
s"finding attachment '[\\w-/:]+' of document 'id: ${action.namespace}/${action.name}").mkString("(?s).*")
Put(s"$collectionPath/$name", content) ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
}
removeFromCache(action, WhiskAction)
// second request should not fetch from cache
Get(s"$collectionPath/$name") ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(kind)))
}
stream.toString should include regex (expectedGetLog)
stream.reset()
}
}
it should "concurrently get an action with attachment that is not cached" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters("x", "b"))
val kind = NODEJS10
val content = WhiskActionPut(
Some(action.exec),
Some(action.parameters),
Some(
ActionLimitsOption(
Some(action.limits.timeout),
Some(action.limits.memory),
Some(action.limits.logs),
Some(action.limits.concurrency))))
val name = action.name
val cacheKey = s"${CacheKey(action)}".replace("(", "\\(").replace(")", "\\)")
val expectedGetLog = Seq(
s"finding document: 'id: ${action.namespace}/${action.name}",
s"finding attachment '[\\w-/:]+' of document 'id: ${action.namespace}/${action.name}").mkString("(?s).*")
Put(s"$collectionPath/$name", content) ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
}
removeFromCache(action, WhiskAction)
stream.reset()
val expectedAction = WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version,
action.publish,
action.annotations ++ systemAnnotations(kind))
(0 until 5).map { i =>
Get(s"$collectionPath/$name") ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(response, expectedAction)
}
}
//Loading action with attachment concurrently should load only attachment once
val logs = stream.toString
withClue(s"db logs $logs") {
StringUtils.countMatches(logs, "finding document") shouldBe 1
StringUtils.countMatches(logs, "finding attachment") shouldBe 1
}
stream.reset()
}
it should "update an existing action with attachment that is not cached" in {
implicit val tid = transid()
val nodeAction = WhiskAction(namespace, aname(), jsDefault(nonInlinedCode(entityStore)), Parameters("x", "b"))
val swiftAction = WhiskAction(namespace, aname(), swift(nonInlinedCode(entityStore)), Parameters("x", "b"))
val bbAction = WhiskAction(namespace, aname(), bb("bb", nonInlinedCode(entityStore), Some("bbMain")))
val actions = Seq((nodeAction, NODEJS10), (swiftAction, SWIFT4), (bbAction, BLACKBOX))
actions.foreach {
case (action, kind) =>
val content = WhiskActionPut(
Some(action.exec),
Some(action.parameters),
Some(
ActionLimitsOption(
Some(action.limits.timeout),
Some(action.limits.memory),
Some(action.limits.logs),
Some(action.limits.concurrency))))
val name = action.name
val cacheKey = s"${CacheKey(action)}".replace("(", "\\(").replace(")", "\\)")
val expectedPutLog =
Seq(
s"uploading attachment '[\\w-/:]+' of document 'id: ${action.namespace}/${action.name}",
s"caching $cacheKey")
.mkString("(?s).*")
Put(s"$collectionPath/$name", content) ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
}
removeFromCache(action, WhiskAction)
Put(s"$collectionPath/$name?overwrite=true", content) ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version.upPatch,
action.publish,
action.annotations ++ systemAnnotations(kind)))
}
stream.toString should include regex (expectedPutLog)
stream.reset()
// delete should invalidate cache
Delete(s"$collectionPath/$name") ~> Route.seal(routes(creds)(transid())) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
action.parameters,
action.limits,
action.version.upPatch,
action.publish,
action.annotations ++ systemAnnotations(kind)))
}
stream.toString should include(s"invalidating ${CacheKey(action)}")
stream.reset()
}
}
it should "ensure old and new action schemas are supported" in {
implicit val tid = transid()
val code = nonInlinedCode(entityStore)
val actionOldSchema = WhiskAction(namespace, aname(), js10Old(code))
val actionNewSchema = WhiskAction(namespace, aname(), jsDefault(code))
val content = WhiskActionPut(
Some(actionOldSchema.exec),
Some(actionOldSchema.parameters),
Some(
ActionLimitsOption(
Some(actionOldSchema.limits.timeout),
Some(actionOldSchema.limits.memory),
Some(actionOldSchema.limits.logs),
Some(actionOldSchema.limits.concurrency))))
val expectedPutLog =
Seq(s"uploading attachment '[\\w-/:]+' of document 'id: ${actionOldSchema.namespace}/${actionOldSchema.name}")
.mkString("(?s).*")
put(entityStore, actionOldSchema)
stream.toString should not include regex(expectedPutLog)
stream.reset()
Post(s"$collectionPath/${actionOldSchema.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(Accepted)
val response = responseAs[JsObject]
response.fields("activationId") should not be None
}
Put(s"$collectionPath/${actionOldSchema.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
actionOldSchema.namespace,
actionOldSchema.name,
actionNewSchema.exec,
actionOldSchema.parameters,
actionOldSchema.limits,
actionOldSchema.version.upPatch,
actionOldSchema.publish,
actionOldSchema.annotations ++ systemAnnotations(NODEJS10, create = false)))
}
stream.toString should include regex (expectedPutLog)
stream.reset()
Post(s"$collectionPath/${actionOldSchema.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(Accepted)
val response = responseAs[JsObject]
response.fields("activationId") should not be None
}
Delete(s"$collectionPath/${actionOldSchema.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
actionOldSchema.namespace,
actionOldSchema.name,
actionNewSchema.exec,
actionOldSchema.parameters,
actionOldSchema.limits,
actionOldSchema.version.upPatch,
actionOldSchema.publish,
actionOldSchema.annotations ++ systemAnnotations(NODEJS10, create = false)))
}
}
it should "reject put with conflict for pre-existing action" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
val content = WhiskActionPut(Some(action.exec))
put(entityStore, action)
Put(s"$collectionPath/${action.name}", content) ~> Route.seal(routes(creds)) ~> check {
status should be(Conflict)
}
}
it should "update action with a put" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"), ActionLimits())
val content = WhiskActionPut(
Some(jsDefault("_")),
Some(Parameters("x", "X")),
Some(
ActionLimitsOption(
Some(TimeLimit(TimeLimit.MAX_DURATION)),
Some(MemoryLimit(MemoryLimit.MAX_MEMORY)),
Some(LogLimit(LogLimit.MAX_LOGSIZE)),
Some(ConcurrencyLimit(ConcurrencyLimit.MAX_CONCURRENT)))))
put(entityStore, action)
Put(s"$collectionPath/${action.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status should be(OK)
val response = responseAs[WhiskAction]
response.updated should not be action.updated
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
content.exec.get,
content.parameters.get,
ActionLimits(
content.limits.get.timeout.get,
content.limits.get.memory.get,
content.limits.get.logs.get,
content.limits.get.concurrency.get),
version = action.version.upPatch,
annotations = action.annotations ++ systemAnnotations(NODEJS10, create = false)))
}
}
it should "update action parameters with a put" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
val content = WhiskActionPut(parameters = Some(Parameters("x", "X")))
put(entityStore, action)
Put(s"$collectionPath/${action.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status should be(OK)
val response = responseAs[WhiskAction]
checkWhiskEntityResponse(
response,
WhiskAction(
action.namespace,
action.name,
action.exec,
content.parameters.get,
version = action.version.upPatch,
annotations = action.annotations ++ systemAnnotations(NODEJS10, false)))
}
}
//// POST /actions/name
it should "invoke an action with arguments, nonblocking" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"), Parameters("x", "b"))
val args = JsObject("xxx" -> "yyy".toJson)
put(entityStore, action)
// it should "reject post to action in namespace not owned by subject"
val auser = WhiskAuthHelpers.newIdentity()
Post(s"/$namespace/${collection.path}/${action.name}", args) ~> Route.seal(routes(auser)) ~> check {
status should be(Forbidden)
}
Post(s"$collectionPath/${action.name}", args) ~> Route.seal(routes(creds)) ~> check {
status should be(Accepted)
val response = responseAs[JsObject]
response.fields("activationId") should not be None
headers should contain(RawHeader(ActivationIdHeader, response.fields("activationId").convertTo[String]))
}
// it should "ignore &result when invoking nonblocking action"
Post(s"$collectionPath/${action.name}?result=true", args) ~> Route.seal(routes(creds)) ~> check {
status should be(Accepted)
val response = responseAs[JsObject]
response.fields("activationId") should not be None
headers should contain(RawHeader(ActivationIdHeader, response.fields("activationId").convertTo[String]))
}
}
it should "invoke an action with init arguments" in {
implicit val tid = transid()
val action =
WhiskAction(namespace, aname(), jsDefault("??"), Parameters("E", "e", init = true) ++ Parameters("a", "A"))
put(entityStore, action)
loadBalancer.activationMessageChecker = Some { msg: ActivationMessage =>
msg.initArgs shouldBe Set("E")
msg.content shouldBe Some {
JsObject("E" -> JsString("e"), "a" -> JsString("A"))
}
}
Post(s"$collectionPath/${action.name}", JsObject.empty) ~> Route.seal(routes(creds)) ~> check {
loadBalancer.activationMessageChecker = None
status should be(Accepted)
}
// overriding an init param is permitted
val args = JsObject("E" -> "E".toJson)
loadBalancer.activationMessageChecker = Some { msg: ActivationMessage =>
msg.initArgs shouldBe Set("E")
msg.content shouldBe Some {
JsObject("E" -> JsString("E"), "a" -> JsString("A"))
}
}
Post(s"$collectionPath/${action.name}", args) ~> Route.seal(routes(creds)) ~> check {
loadBalancer.activationMessageChecker = None
status should be(Accepted)
}
}
it should "invoke an action, nonblocking" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"))
put(entityStore, action)
Post(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(Accepted)
val response = responseAs[JsObject]
response.fields("activationId") should not be None
headers should contain(RawHeader(ActivationIdHeader, response.fields("activationId").convertTo[String]))
}
}
it should "not invoke an action when final parameters are redefined" in {
implicit val tid = transid()
val annotations = Parameters(Annotations.FinalParamsAnnotationName, JsTrue)
val parameters = Parameters("a", "A") ++ Parameters("empty", JsNull)
val action = WhiskAction(namespace, aname(), jsDefault("??"), parameters = parameters, annotations = annotations)
put(entityStore, action)
Seq((Parameters("a", "B"), BadRequest), (Parameters("empty", "C"), Accepted)).foreach {
case (p, code) =>
Post(s"$collectionPath/${action.name}", p.toJsObject) ~> Route.seal(routes(creds)) ~> check {
status should be(code)
if (code == BadRequest) {
responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
}
}
}
}
it should "invoke a blocking action and retrieve result via active ack" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"))
val activation = WhiskActivation(
action.namespace,
action.name,
creds.subject,
activationIdFactory.make(),
start = Instant.now,
end = Instant.now,
response = ActivationResponse.success(Some(JsObject("test" -> "yes".toJson))))
put(entityStore, action)
try {
// do not store the activation in the db, instead register it as the response to generate on active ack
loadBalancer.whiskActivationStub = Some((1.milliseconds, Right(activation)))
Post(s"$collectionPath/${action.name}?blocking=true") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[JsObject]
response should be(activation.withoutLogs.toExtendedJson())
}
// repeat invoke, get only result back
Post(s"$collectionPath/${action.name}?blocking=true&result=true") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[JsObject]
response should be(activation.resultAsJson)
}
} finally {
loadBalancer.whiskActivationStub = None
}
}
it should "invoke a blocking action, waiting up to specified timeout and retrieve result via active ack" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"))
val activation = WhiskActivation(
action.namespace,
action.name,
creds.subject,
activationIdFactory.make(),
start = Instant.now,
end = Instant.now,
response = ActivationResponse.success(Some(JsObject("test" -> "yes".toJson))))
put(entityStore, action)
try {
// do not store the activation in the db, instead register it as the response to generate on active ack
loadBalancer.whiskActivationStub = Some((300.milliseconds, Right(activation)))
Post(s"$collectionPath/${action.name}?blocking=true&timeout=0") ~> Route.seal(routes(creds)) ~> check {
status shouldBe BadRequest
responseAs[String] should include(
Messages.invalidTimeout(controllerActivationConfig.maxWaitForBlockingActivation))
}
Post(s"$collectionPath/${action.name}?blocking=true&timeout=65000") ~> Route.seal(routes(creds)) ~> check {
status shouldBe BadRequest
responseAs[String] should include(
Messages.invalidTimeout(controllerActivationConfig.maxWaitForBlockingActivation))
}
// will not wait long enough should get accepted status
Post(s"$collectionPath/${action.name}?blocking=true&timeout=100") ~> Route.seal(routes(creds)) ~> check {
val response = responseAs[JsObject]
status shouldBe Accepted
headers should contain(RawHeader(ActivationIdHeader, response.fields("activationId").convertTo[String]))
}
// repeat this time wait longer than active ack delay
Post(s"$collectionPath/${action.name}?blocking=true&timeout=500") ~> Route.seal(routes(creds)) ~> check {
status shouldBe OK
val response = responseAs[JsObject]
response shouldBe activation.withoutLogs.toExtendedJson()
headers should contain(RawHeader(ActivationIdHeader, response.fields("activationId").convertTo[String]))
}
} finally {
loadBalancer.whiskActivationStub = None
}
}
it should "ensure WhiskActionMetadata is used to invoke an action" in {
implicit val tid = transid()
val action = WhiskAction(namespace, aname(), jsDefault("??"))
put(entityStore, action)
Post(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(Accepted)
val response = responseAs[JsObject]
response.fields("activationId") should not be None
headers should contain(RawHeader(ActivationIdHeader, response.fields("activationId").convertTo[String]))
}
stream.toString should include(s"[WhiskActionMetaData] [GET] serving from datastore: ${CacheKey(action)}")
stream.reset()
}
it should "report proper error when record is corrupted on delete" in {
implicit val tid = transid()
val entity = BadEntity(namespace, aname())
put(entityStore, entity)
Delete(s"$collectionPath/${entity.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(InternalServerError)
responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity
}
}
it should "report proper error when record is corrupted on get" in {
implicit val tid = transid()
val entity = BadEntity(namespace, aname())
put(entityStore, entity)
Get(s"$collectionPath/${entity.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(InternalServerError)
responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity
}
}
it should "report proper error when record is corrupted on put" in {
implicit val tid = transid()
val entity = BadEntity(namespace, aname())
put(entityStore, entity)
val components = Vector(stringToFullyQualifiedName(s"$namespace/${entity.name}"))
val content = WhiskActionPut(Some(sequence(components)))
Put(s"$collectionPath/${aname()}", content) ~> Route.seal(routes(creds)) ~> check {
status should be(InternalServerError)
responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity
}
}
// get and delete allowed, create/update with deprecated exec not allowed, post/invoke not allowed
it should "report proper error when runtime is deprecated" in {
implicit val tid = transid()
try {
val okKind = "test:1"
val deprecatedKind = "test:2"
val customManifest = Some(s"""
|{ "runtimes": {
| "test": [
| {
| "kind": "$okKind",
| "deprecated": false,
| "default": true,
| "image": {
| "name": "xyz"
| }
| }, {
| "kind": "$deprecatedKind",
| "deprecated": true,
| "image": {
| "name": "xyz"
| }
| }
| ]
| }
|}
|""".stripMargin)
ExecManifest.initialize(whiskConfig, customManifest)
val deprecatedManifest = ExecManifest.runtimesManifest.resolveDefaultRuntime(deprecatedKind).get
val deprecatedExec =
CodeExecAsAttachment(deprecatedManifest, Attachments.serdes[String].read(JsString("??")), None)
val okManifest = ExecManifest.runtimesManifest.resolveDefaultRuntime(okKind).get
val okExec = CodeExecAsAttachment(okManifest, Attachments.serdes[String].read(JsString("??")), None)
val action = WhiskAction(namespace, aname(), deprecatedExec)
val okUpdate = WhiskActionPut(Some(okExec))
val badUpdate = WhiskActionPut(Some(deprecatedExec))
Put(s"$collectionPath/${action.name}", WhiskActionPut(Some(action.exec))) ~> Route.seal(routes(creds)) ~> check {
status shouldBe BadRequest
responseAs[ErrorResponse].error shouldBe Messages.runtimeDeprecated(action.exec)
}
Put(s"$collectionPath/${action.name}?overwrite=true", WhiskActionPut(Some(action.exec))) ~> Route.seal(
routes(creds)) ~> check {
status shouldBe BadRequest
responseAs[ErrorResponse].error shouldBe Messages.runtimeDeprecated(action.exec)
}
put(entityStore, action)
Put(s"$collectionPath/${action.name}?overwrite=true", JsObject.empty) ~> Route.seal(routes(creds)) ~> check {
status shouldBe BadRequest
responseAs[ErrorResponse].error shouldBe Messages.runtimeDeprecated(action.exec)
}
Put(s"$collectionPath/${action.name}?overwrite=true", badUpdate) ~> Route.seal(routes(creds)) ~> check {
status shouldBe BadRequest
responseAs[ErrorResponse].error shouldBe Messages.runtimeDeprecated(action.exec)
}
Post(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~> check {
status shouldBe BadRequest
responseAs[ErrorResponse].error shouldBe Messages.runtimeDeprecated(action.exec)
}
Get(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~> check {
status shouldBe OK
}
Delete(s"$collectionPath/${action.name}") ~> Route.seal(routes(creds)) ~> check {
status shouldBe OK
}
put(entityStore, action)
Put(s"$collectionPath/${action.name}?overwrite=true", okUpdate) ~> Route.seal(routes(creds)) ~> check {
deleteAction(action.docid)
status shouldBe OK
}
} finally {
// restore manifest
ExecManifest.initialize(whiskConfig)
}
}
}
@RunWith(classOf[JUnitRunner])
class WhiskActionsApiTests extends FlatSpec with Matchers with ExecHelpers {
import WhiskActionsApi.amendAnnotations
import Annotations.ProvideApiKeyAnnotationName
import WhiskAction.execFieldName
val baseParams = Parameters("a", JsString("A")) ++ Parameters("b", JsString("B"))
val keyTruthyAnnotation = Parameters(ProvideApiKeyAnnotationName, JsTrue)
val keyFalsyAnnotation = Parameters(ProvideApiKeyAnnotationName, JsString.empty) // falsy other than JsFalse
val execAnnotation = Parameters(execFieldName, JsString("foo"))
val exec: Exec = jsDefault("??")
it should "add key annotation if it is not present already" in {
Seq(Parameters(), baseParams).foreach { p =>
amendAnnotations(p, exec) shouldBe {
p ++ Parameters(ProvideApiKeyAnnotationName, JsFalse) ++
Parameters(WhiskAction.execFieldName, exec.kind)
}
}
}
it should "not add key annotation if already present regardless of value" in {
Seq(keyTruthyAnnotation, keyFalsyAnnotation).foreach { p =>
amendAnnotations(p, exec) shouldBe {
p ++ Parameters(WhiskAction.execFieldName, exec.kind)
}
}
}
it should "override system annotation as necessary" in {
amendAnnotations(baseParams ++ execAnnotation, exec) shouldBe {
baseParams ++ Parameters(ProvideApiKeyAnnotationName, JsFalse) ++ Parameters(WhiskAction.execFieldName, exec.kind)
}
}
}