blob: 58b2fd4809cf86107b3d00669d1bb852b8e7f202 [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.marshallers.sprayjson.SprayJsonSupport._
import akka.http.scaladsl.server.Route
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.model.headers.RawHeader
import spray.json._
import spray.json.DefaultJsonProtocol._
import org.apache.openwhisk.core.controller.WhiskTriggersApi
import org.apache.openwhisk.core.entitlement.Collection
import org.apache.openwhisk.core.entity._
import org.apache.openwhisk.core.entity.WhiskRule
import org.apache.openwhisk.core.entity.size._
import org.apache.openwhisk.core.entity.test.OldWhiskTrigger
import org.apache.openwhisk.http.ErrorResponse
import org.apache.openwhisk.http.Messages
import org.apache.openwhisk.core.database.UserContext
/**
* Tests Trigger 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 TriggersApiTests extends ControllerTestCommon with WhiskTriggersApi {
/** Triggers API tests */
behavior of "Triggers 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("triggers_tests")
def afullname(namespace: EntityPath, name: String) = FullyQualifiedEntityName(namespace, EntityName(name))
val parametersLimit = Parameters.sizeLimit
val dummyInstant = Instant.now()
//// GET /triggers
it should "list triggers by default/explicit namespace" in {
implicit val tid = transid()
val triggers = (1 to 2).map { i =>
WhiskTrigger(namespace, aname(), Parameters("x", "b"))
}.toList
triggers foreach { put(entityStore, _) }
waitOnView(entityStore, WhiskTrigger, namespace, 2)
Get(s"$collectionPath") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[JsObject]]
triggers.length should be(response.length)
response should contain theSameElementsAs triggers.map(_.summaryAsJson)
}
// it should "list triggers with explicit namespace owned by subject" in {
Get(s"/$namespace/${collection.path}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[JsObject]]
triggers.length should be(response.length)
response should contain theSameElementsAs triggers.map(_.summaryAsJson)
}
// it should "reject list triggers 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 "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.TRIGGERS, 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.TRIGGERS, 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.TRIGGERS, 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.TRIGGERS, notAnInteger)
}
}
}
// ?docs disabled
ignore should "list triggers by default namespace with full docs" in {
implicit val tid = transid()
val triggers = (1 to 2).map { i =>
WhiskTrigger(namespace, aname(), Parameters("x", "b"))
}.toList
triggers foreach { put(entityStore, _) }
waitOnView(entityStore, WhiskTrigger, namespace, 2)
Get(s"$collectionPath?docs=true") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[WhiskTrigger]]
triggers.length should be(response.length)
response should contain theSameElementsAs triggers.map(_.summaryAsJson)
}
}
//// GET /triggers/name
it should "get trigger by name in default/explicit namespace" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname(), Parameters("x", "b"))
put(entityStore, trigger)
Get(s"$collectionPath/${trigger.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskTrigger]
response should be(trigger)
}
// it should "get trigger by name in explicit namespace owned by subject" in
Get(s"/$namespace/${collection.path}/${trigger.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskTrigger]
response should be(trigger)
}
// it should "reject get trigger by name in explicit namespace not owned by subject" in
val auser = WhiskAuthHelpers.newIdentity()
Get(s"/$namespace/${collection.path}/${trigger.name}") ~> Route.seal(routes(auser)) ~> check {
status should be(Forbidden)
}
}
it should "get trigger with updated field" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname(), Parameters("x", "b"))
put(entityStore, trigger)
// `updated` field should be compared with a document in DB
val t = get(entityStore, trigger.docid, WhiskTrigger)
Get(s"$collectionPath/${trigger.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskTrigger]
response should be(trigger copy (updated = t.updated))
}
}
it should "report Conflict if the name was of a different type" in {
implicit val tid = transid()
val rule = WhiskRule(
namespace,
aname(),
FullyQualifiedEntityName(namespace, aname()),
FullyQualifiedEntityName(namespace, aname()))
put(entityStore, rule)
Get(s"/$namespace/${collection.path}/${rule.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(Conflict)
}
}
//// DEL /triggers/name
it should "delete trigger by name" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname(), Parameters("x", "b"))
put(entityStore, trigger)
Delete(s"$collectionPath/${trigger.name}") ~> Route.seal(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskTrigger]
response should be(trigger)
}
}
//// PUT /triggers/name
it should "put should accept request with missing optional properties" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname())
val content = WhiskTriggerPut()
Put(s"$collectionPath/${trigger.name}", content) ~> Route.seal(routes(creds)) ~> check {
deleteTrigger(trigger.docid)
status should be(OK)
val response = responseAs[WhiskTrigger]
checkWhiskEntityResponse(response, trigger.withoutRules)
}
}
it should "put should accept request with valid feed parameter" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname(), annotations = Parameters(Parameters.Feed, "xyz"))
val content = WhiskTriggerPut(annotations = Some(trigger.annotations))
Put(s"$collectionPath/${trigger.name}", content) ~> Route.seal(routes(creds)) ~> check {
deleteTrigger(trigger.docid)
status should be(OK)
val response = responseAs[WhiskTrigger]
checkWhiskEntityResponse(response, trigger.withoutRules)
}
}
it should "put should reject request with undefined feed parameter" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname(), annotations = Parameters(Parameters.Feed, ""))
val content = WhiskTriggerPut(annotations = Some(trigger.annotations))
Put(s"$collectionPath/${trigger.name}", content) ~> Route.seal(routes(creds)) ~> check {
status should be(BadRequest)
}
}
it should "put should reject request with bad feed parameters" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname(), annotations = Parameters(Parameters.Feed, "a,b"))
val content = WhiskTriggerPut(annotations = Some(trigger.annotations))
Put(s"$collectionPath/${trigger.name}", content) ~> Route.seal(routes(creds)) ~> check {
status should be(BadRequest)
}
}
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 "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"""{"parameters":$parameters}""".parseJson.asJsObject
Put(s"$collectionPath/${aname()}", content) ~> 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"""{"annotations":$annotations}""".parseJson.asJsObject
Put(s"$collectionPath/${aname()}", content) ~> 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 update with parameters which are too big" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname())
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"""{"parameters":$parameters}""".parseJson.asJsObject
put(entityStore, trigger)
Put(s"$collectionPath/${trigger.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
status should be(PayloadTooLarge)
responseAs[String] should include {
Messages.entityTooBig(SizeError(WhiskEntity.paramsFieldName, parameters.size, Parameters.sizeLimit))
}
}
}
it should "put should accept update request with missing optional properties" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname(), Parameters("x", "b"))
val content = WhiskTriggerPut()
put(entityStore, trigger)
Put(s"$collectionPath/${trigger.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
deleteTrigger(trigger.docid)
status should be(OK)
val response = responseAs[WhiskTrigger]
checkWhiskEntityResponse(
response,
WhiskTrigger(
trigger.namespace,
trigger.name,
trigger.parameters,
version = trigger.version.upPatch,
updated = dummyInstant).withoutRules)
}
}
it should "put should reject update request for trigger with existing feed" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname(), annotations = Parameters(Parameters.Feed, "xyz"))
val content = WhiskTriggerPut(annotations = Some(trigger.annotations))
put(entityStore, trigger)
Put(s"$collectionPath/${trigger.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
deleteTrigger(trigger.docid)
status should be(OK)
}
}
it should "put should reject update request for trigger with new feed" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname())
val content = WhiskTriggerPut(annotations = Some(Parameters(Parameters.Feed, "xyz")))
put(entityStore, trigger)
Put(s"$collectionPath/${trigger.name}?overwrite=true", content) ~> Route.seal(routes(creds)) ~> check {
deleteTrigger(trigger.docid)
status should be(OK)
}
}
//// POST /triggers/name
it should "fire a trigger" in {
implicit val tid = transid()
val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, "bogus action"))
val trigger = WhiskTrigger(namespace, rule.trigger.name, rules = Some {
Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))
})
val content = JsObject("xxx" -> "yyy".toJson)
put(entityStore, trigger)
put(entityStore, rule)
Post(s"$collectionPath/${trigger.name}", content) ~> Route.seal(routes(creds)) ~> check {
status should be(Accepted)
val response = responseAs[JsObject]
val JsString(id) = response.fields("activationId")
val activationId = ActivationId.parse(id).get
response.fields("activationId") should not be None
headers should contain(RawHeader(ActivationIdHeader, response.fields("activationId").convertTo[String]))
val activationDoc = DocId(WhiskEntity.qualifiedName(namespace, activationId))
org.apache.openwhisk.utils.retry({
println(s"trying to obtain async activation doc: '${activationDoc}'")
val activation = getActivation(ActivationId(activationDoc.asString), context)
deleteActivation(ActivationId(activationDoc.asString), context)
activation.end should be(Instant.EPOCH)
activation.response.result should be(Some(content))
}, 30, Some(1.second))
}
}
it should "fire a trigger without args" in {
implicit val tid = transid()
val rule = WhiskRule(namespace, aname(), afullname(namespace, aname().name), afullname(namespace, "bogus action"))
val trigger = WhiskTrigger(namespace, rule.trigger.name, Parameters("x", "b"), rules = Some {
Map(rule.fullyQualifiedName(false) -> ReducedRule(rule.action, Status.ACTIVE))
})
put(entityStore, trigger)
put(entityStore, rule)
Post(s"$collectionPath/${trigger.name}") ~> Route.seal(routes(creds)) ~> check {
val response = responseAs[JsObject]
val JsString(id) = response.fields("activationId")
val activationId = ActivationId.parse(id).get
val activationDoc = DocId(WhiskEntity.qualifiedName(namespace, activationId))
org.apache.openwhisk.utils.retry({
println(s"trying to delete async activation doc: '${activationDoc}'")
deleteActivation(ActivationId(activationDoc.asString), context)
response.fields("activationId") should not be None
headers should contain(RawHeader(ActivationIdHeader, response.fields("activationId").convertTo[String]))
}, 30, Some(1.second))
}
}
it should "not fire a trigger without a rule" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname())
put(entityStore, trigger)
Post(s"$collectionPath/${trigger.name}") ~> Route.seal(routes(creds)) ~> check {
status shouldBe NoContent
}
}
//// invalid resource
it should "reject invalid resource" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname())
put(entityStore, trigger)
Get(s"$collectionPath/${trigger.name}/bar") ~> Route.seal(routes(creds)) ~> check {
status should be(NotFound)
}
}
// migration path
it should "be able to handle a trigger as of the old schema" in {
implicit val tid = transid()
val trigger = OldWhiskTrigger(namespace, aname())
put(entityStore, trigger)
Get(s"$collectionPath/${trigger.name}") ~> Route.seal(routes(creds)) ~> check {
val response = responseAs[WhiskTrigger]
status should be(OK)
checkWhiskEntityResponse(response, trigger.toWhiskTrigger)
}
}
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 content = WhiskTriggerPut()
Put(s"$collectionPath/${entity.name}", content) ~> Route.seal(routes(creds)) ~> check {
status should be(InternalServerError)
responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity
}
}
}