| /* |
| * 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)) |
| 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) |
| } |
| } |
| } |