blob: da02c1f4f5a54b7b36471976a4efb26e25d0b653 [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.Clock
import java.time.Instant
import org.junit.runner.RunWith
import org.scalatest.junit.JUnitRunner
import spray.http.StatusCodes._
import spray.httpx.SprayJsonSupport._
import spray.json._
import spray.json.DefaultJsonProtocol._
import whisk.core.controller.WhiskActivationsApi
import whisk.core.entity._
import whisk.http.ErrorResponse
import whisk.http.Messages
/**
* Tests Activations 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 ActivationsApiTests extends ControllerTestCommon with WhiskActivationsApi {
/** Activations API tests */
behavior of "Activations API"
val creds = WhiskAuth(Subject(), AuthKey()).toIdentity
val namespace = EntityPath(creds.subject.asString)
val collectionPath = s"/${EntityPath.DEFAULT}/${collection.path}"
def aname = MakeName.next("activations_tests")
//// GET /activations
it should "get summary activation by namespace" in {
implicit val tid = transid()
// create two sets of activation records, and check that only one set is served back
val creds1 = WhiskAuth(Subject(), AuthKey())
(1 to 2).map { i =>
WhiskActivation(EntityPath(creds1.subject.asString), aname, creds1.subject, ActivationId(), start = Instant.now, end = Instant.now)
} foreach { put(entityStore, _) }
val actionName = aname
val activations = (1 to 2).map { i =>
WhiskActivation(namespace, actionName, creds.subject, ActivationId(), start = Instant.now, end = Instant.now)
}.toList
activations foreach { put(entityStore, _) }
waitOnView(entityStore, WhiskActivation, namespace, 2)
whisk.utils.retry {
Get(s"$collectionPath") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val rawResponse = responseAs[List[JsObject]]
val response = responseAs[List[JsObject]]
activations.length should be(response.length)
activations forall { a => response contains a.summaryAsJson } should be(true)
rawResponse forall { a => a.getFields("for") match { case Seq(JsString(n)) => n == actionName.asString case _ => false } }
}
}
// it should "list activations with explicit namespace owned by subject" in {
whisk.utils.retry {
Get(s"/$namespace/${collection.path}") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val rawResponse = responseAs[List[JsObject]]
val response = responseAs[List[JsObject]]
activations.length should be(response.length)
activations forall { a => response contains a.summaryAsJson } should be(true)
rawResponse forall { a => a.getFields("for") match { case Seq(JsString(n)) => n == actionName.asString case _ => false } }
}
}
// it should "reject list activations with explicit namespace not owned by subject" in {
val auser = WhiskAuth(Subject(), AuthKey()).toIdentity
Get(s"/$namespace/${collection.path}") ~> sealRoute(routes(auser)) ~> check {
status should be(Forbidden)
}
}
//// GET /activations?docs=true
it should "get full activation by namespace" in {
implicit val tid = transid()
// create two sets of activation records, and check that only one set is served back
val creds1 = WhiskAuth(Subject(), AuthKey())
(1 to 2).map { i =>
WhiskActivation(EntityPath(creds1.subject.asString), aname, creds1.subject, ActivationId(), start = Instant.now, end = Instant.now)
} foreach { put(entityStore, _) }
val actionName = aname
val activations = (1 to 2).map { i =>
WhiskActivation(namespace, actionName, creds.subject, ActivationId(), start = Instant.now, end = Instant.now, response = ActivationResponse.success(Some(JsNumber(5))))
}.toList
activations foreach { put(entityStore, _) }
waitOnView(entityStore, WhiskActivation, namespace, 2)
whisk.utils.retry {
Get(s"$collectionPath?docs=true") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[JsObject]]
activations.length should be(response.length)
activations forall { a => response contains a.toExtendedJson } should be(true)
}
}
}
//// GET /activations?docs=true&since=xxx&upto=yyy
it should "get full activation by namespace within a date range" in {
implicit val tid = transid()
// create two sets of activation records, and check that only one set is served back
val creds1 = WhiskAuth(Subject(), AuthKey())
(1 to 2).map { i =>
WhiskActivation(EntityPath(creds1.subject.asString), aname, creds1.subject, ActivationId(), start = Instant.now, end = Instant.now)
} foreach { put(entityStore, _) }
val actionName = aname
val now = Instant.now(Clock.systemUTC())
val since = now.plusSeconds(10)
val upto = now.plusSeconds(30)
implicit val activations = Seq(
WhiskActivation(namespace, actionName, creds.subject, ActivationId(), start = now.plusSeconds(9), end = now),
WhiskActivation(namespace, actionName, creds.subject, ActivationId(), start = now.plusSeconds(20), end = now.plusSeconds(20)), // should match
WhiskActivation(namespace, actionName, creds.subject, ActivationId(), start = now.plusSeconds(10), end = now.plusSeconds(20)), // should match
WhiskActivation(namespace, actionName, creds.subject, ActivationId(), start = now.plusSeconds(31), end = now.plusSeconds(20)),
WhiskActivation(namespace, actionName, creds.subject, ActivationId(), start = now.plusSeconds(30), end = now.plusSeconds(20))) // should match
activations foreach { put(entityStore, _) }
waitOnView(entityStore, WhiskActivation, namespace, activations.length)
// get between two time stamps
whisk.utils.retry {
Get(s"$collectionPath?docs=true&since=${since.toEpochMilli}&upto=${upto.toEpochMilli}") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[JsObject]]
val expected = activations filter {
case e => (e.start.equals(since) || e.start.equals(upto) || (e.start.isAfter(since) && e.start.isBefore(upto)))
}
expected.length should be(response.length)
expected forall { a => response contains a.toExtendedJson } should be(true)
}
}
// get 'upto' with no defined since value should return all activation 'upto'
whisk.utils.retry {
Get(s"$collectionPath?docs=true&upto=${upto.toEpochMilli}") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[JsObject]]
val expected = activations filter {
case e => e.start.equals(upto) || e.start.isBefore(upto)
}
expected.length should be(response.length)
expected forall { a => response contains a.toExtendedJson } should be(true)
}
}
// get 'since' with no defined upto value should return all activation 'since'
whisk.utils.retry {
Get(s"$collectionPath?docs=true&since=${since.toEpochMilli}") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[JsObject]]
val expected = activations filter {
case e => e.start.equals(since) || e.start.isAfter(since)
}
expected.length should be(response.length)
expected forall { a => response contains a.toExtendedJson } should be(true)
}
}
}
//// GET /activations?name=xyz
it should "get summary activation by namespace and action name" in {
implicit val tid = transid()
// create two sets of activation records, and check that only one set is served back
val creds1 = WhiskAuth(Subject(), AuthKey())
(1 to 2).map { i =>
WhiskActivation(EntityPath(creds1.subject.asString), aname, creds1.subject, ActivationId(), start = Instant.now, end = Instant.now)
} foreach { put(entityStore, _) }
val activations = (1 to 2).map { i =>
WhiskActivation(namespace, EntityName(s"xyz"), creds.subject, ActivationId(), start = Instant.now, end = Instant.now)
}.toList
activations foreach { put(entityStore, _) }
waitOnView(entityStore, WhiskActivation, namespace, 2)
whisk.utils.retry {
Get(s"$collectionPath?name=xyz") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[List[JsObject]]
activations.length should be(response.length)
activations forall { a => response contains a.summaryAsJson } should be(true)
}
}
}
it should "reject get activation by namespace and action name when action name is not a valid name" in {
implicit val tid = transid()
Get(s"$collectionPath?name=0%20") ~> sealRoute(routes(creds)) ~> check {
status should be(BadRequest)
}
}
it should "reject get activation with invalid since/upto value" in {
implicit val tid = transid()
Get(s"$collectionPath?since=xxx") ~> sealRoute(routes(creds)) ~> check {
status should be(BadRequest)
}
Get(s"$collectionPath?upto=yyy") ~> sealRoute(routes(creds)) ~> check {
status should be(BadRequest)
}
}
//// GET /activations/id
it should "get activation by id" in {
implicit val tid = transid()
val activation = WhiskActivation(namespace, aname, creds.subject, ActivationId(), start = Instant.now, end = Instant.now)
put(entityStore, activation)
Get(s"$collectionPath/${activation.activationId.asString}") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[JsObject]
response should be(activation.toExtendedJson)
}
// it should "get activation by name in explicit namespace owned by subject" in
Get(s"/$namespace/${collection.path}/${activation.activationId.asString}") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[JsObject]
response should be(activation.toExtendedJson)
}
// it should "reject get activation by name in explicit namespace not owned by subject" in
val auser = WhiskAuth(Subject(), AuthKey()).toIdentity
Get(s"/$namespace/${collection.path}/${activation.activationId.asString}") ~> sealRoute(routes(auser)) ~> check {
status should be(Forbidden)
}
}
//// GET /activations/id/result
it should "get activation result by id" in {
implicit val tid = transid()
val activation = WhiskActivation(namespace, aname, creds.subject, ActivationId(), start = Instant.now, end = Instant.now)
put(entityStore, activation)
Get(s"$collectionPath/${activation.activationId.asString}/result") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[JsObject]
response should be(activation.response.toExtendedJson)
}
}
//// GET /activations/id/logs
it should "get activation logs by id" in {
implicit val tid = transid()
val activation = WhiskActivation(namespace, aname, creds.subject, ActivationId(), start = Instant.now, end = Instant.now)
put(entityStore, activation)
Get(s"$collectionPath/${activation.activationId.asString}/logs") ~> sealRoute(routes(creds)) ~> check {
status should be(OK)
val response = responseAs[JsObject]
response should be(activation.logs.toJsonObject)
}
}
//// GET /activations/id/bogus
it should "reject request to get invalid activation resource" in {
implicit val tid = transid()
val activation = WhiskActivation(namespace, aname, creds.subject, ActivationId(), start = Instant.now, end = Instant.now)
put(entityStore, activation)
Get(s"$collectionPath/${activation.activationId.asString}/bogus") ~> sealRoute(routes(creds)) ~> check {
status should be(NotFound)
}
}
it should "reject get requests with invalid activation ids" in {
implicit val tid = transid()
val activationId = ActivationId().toString
val tooshort = activationId.substring(0, 31)
val toolong = activationId + "xxx"
val malformed = tooshort + "z"
Get(s"$collectionPath/$tooshort") ~> sealRoute(routes(creds)) ~> check {
status should be(BadRequest)
responseAs[String] should include("too short")
}
Get(s"$collectionPath/$toolong") ~> sealRoute(routes(creds)) ~> check {
status should be(BadRequest)
responseAs[String] should include("too long")
}
Get(s"$collectionPath/$malformed") ~> sealRoute(routes(creds)) ~> check {
status should be(BadRequest)
}
}
it should "reject request with put" in {
implicit val tid = transid()
Put(s"$collectionPath/${ActivationId()}") ~> sealRoute(routes(creds)) ~> check {
status should be(MethodNotAllowed)
}
}
it should "reject request with post" in {
implicit val tid = transid()
Post(s"$collectionPath/${ActivationId()}") ~> sealRoute(routes(creds)) ~> check {
status should be(MethodNotAllowed)
}
}
it should "reject request with delete" in {
implicit val tid = transid()
Delete(s"$collectionPath/${ActivationId()}") ~> sealRoute(routes(creds)) ~> check {
status should be(MethodNotAllowed)
}
}
it should "report proper error when record is corrupted on get" in {
implicit val tid = transid()
val entity = BadEntity(namespace, EntityName(ActivationId().toString))
put(entityStore, entity)
Get(s"$collectionPath/${entity.name}") ~> sealRoute(routes(creds)) ~> check {
status should be(InternalServerError)
responseAs[ErrorResponse].error shouldBe Messages.corruptedEntity
}
}
}