blob: 2603e17a869f817fc36d2c1e71c920f1b1ca0f6e [file] [log] [blame]
/*
* Copyright 2015-2016 IBM Corporation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package whisk.core.controller.test
import java.time.Instant
import scala.language.postfixOps
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import spray.http.StatusCodes.BadRequest
import spray.http.StatusCodes.Conflict
import spray.http.StatusCodes.NotFound
import spray.http.StatusCodes.OK
import spray.http.StatusCodes.RequestEntityTooLarge
import spray.httpx.SprayJsonSupport.sprayJsonMarshaller
import spray.httpx.SprayJsonSupport.sprayJsonUnmarshaller
import spray.json.DefaultJsonProtocol.listFormat
import spray.json.DefaultJsonProtocol.RootJsObjectFormat
import spray.json.DefaultJsonProtocol.StringJsonFormat
import spray.json.JsObject
import spray.json.JsString
import spray.json.pimpAny
import spray.json.pimpString
import whisk.core.controller.WhiskTriggersApi
import whisk.core.entity.ActivationId
import whisk.core.entity.AuthKey
import whisk.core.entity.DocId
import whisk.core.entity.EntityPath
import whisk.core.entity.Parameters
import whisk.core.entity.Subject
import whisk.core.entity.WhiskActivation
import whisk.core.entity.WhiskAuth
import whisk.core.entity.WhiskEntity
import whisk.core.entity.WhiskTrigger
import whisk.core.entity.WhiskTriggerPut
import whisk.core.entity.test.OldWhiskTrigger
import whisk.core.entity.WhiskRule
/**
* 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 = WhiskAuth(Subject(), AuthKey()).toIdentity
val namespace = EntityPath(creds.subject())
val collectionPath = s"/${EntityPath.DEFAULT}/${collection.path}"
def aname = MakeName.next("triggers_tests")
val entityTooBigRejectionMessage = "request entity too large"
val parametersLimit = Parameters.sizeLimit
//// GET /triggers
it should "list triggers by default 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") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[JsObject]]
triggers.length should be(response.length)
triggers forall { a => response contains a.summaryAsJson } should be(true)
}
}
// ?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") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[WhiskTrigger]]
triggers.length should be(response.length)
triggers forall { a => response contains a } should be(true)
}
}
//// GET /triggers/name
it should "get trigger by name in default namespace" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname, Parameters("x", "b"))
put(entityStore, trigger)
Get(s"$collectionPath/${trigger.name}") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskTrigger]
response should be(trigger.withoutRules)
}
}
it should "report Conflict if the name was of a different type" in {
implicit val tid = transid()
val rule = WhiskRule(namespace, aname, aname, aname)
put(entityStore, rule)
Get(s"/$namespace/${collection.path}/${rule.name}") ~> sealRoute(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}") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[WhiskTrigger]
response should be(trigger.withoutRules)
}
}
//// 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) ~> sealRoute(routes(creds)) ~> check {
deleteTrigger(trigger.docid)
status should be(OK)
val response = responseAs[WhiskTrigger]
response should be(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) ~> sealRoute(routes(creds)) ~> check {
deleteTrigger(trigger.docid)
status should be(OK)
val response = responseAs[WhiskTrigger]
response should be(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) ~> sealRoute(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) ~> sealRoute(routes(creds)) ~> check {
status should be(BadRequest)
}
}
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 / 2 / 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) ~> sealRoute(routes(creds)) ~> check {
status should be(RequestEntityTooLarge)
response.entity.toString should include(entityTooBigRejectionMessage)
}
}
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 / 2 / 20 + Math.pow(10, 9) + 2) toLong)
val parameters = keys map { key =>
Parameters(key.toString, "a" * 10)
} reduce (_ ++ _)
val content = s"""{"annotations":$parameters}""".parseJson.asJsObject
Put(s"$collectionPath/${aname}", content) ~> sealRoute(routes(creds)) ~> check {
status should be(RequestEntityTooLarge)
response.entity.toString should include(entityTooBigRejectionMessage)
}
}
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 / 2 / 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) ~> sealRoute(routes(creds)) ~> check {
status should be(RequestEntityTooLarge)
response.entity.toString should include(entityTooBigRejectionMessage)
}
}
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) ~> sealRoute(routes(creds)) ~> check {
deleteTrigger(trigger.docid)
status should be(OK)
val response = responseAs[WhiskTrigger]
response should be(WhiskTrigger(trigger.namespace, trigger.name, trigger.parameters, version = trigger.version.upPatch).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) ~> sealRoute(routes(creds)) ~> check {
status should be(BadRequest)
}
}
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) ~> sealRoute(routes(creds)) ~> check {
status should be(BadRequest)
}
}
//// POST /triggers/name
it should "fire a trigger" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname, Parameters("x", "b"))
val content = JsObject("xxx" -> "yyy".toJson)
put(entityStore, trigger)
Post(s"$collectionPath/${trigger.name}", content) ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[JsObject]
val JsString(id) = response.fields("activationId")
val activationId = ActivationId(id)
response.fields("activationId") should not be None
val activationDoc = DocId(WhiskEntity.qualifiedName(namespace, activationId)).asDocInfo
val activation = get(activationStore, activationDoc, WhiskActivation, garbageCollect = false)
del(entityStore, DocId(WhiskEntity.qualifiedName(namespace, activationId)), WhiskActivation)
activation.end should be(Instant.EPOCH)
activation.response.result should be(Some(content))
}
}
it should "fire a trigger without args" in {
implicit val tid = transid()
val trigger = WhiskTrigger(namespace, aname, Parameters("x", "b"))
put(entityStore, trigger)
Post(s"$collectionPath/${trigger.name}") ~> sealRoute(routes(creds)) ~> check {
val response = responseAs[JsObject]
val JsString(id) = response.fields("activationId")
val activationId = ActivationId(id)
del(entityStore, DocId(WhiskEntity.qualifiedName(namespace, activationId)), WhiskActivation)
response.fields("activationId") should not be None
}
}
//// 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") ~> sealRoute(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}") ~> sealRoute(routes(creds)) ~> check {
val response = responseAs[WhiskTrigger]
status should be(OK)
response should be(trigger.toWhiskTrigger)
}
}
}