This is an automated email from the ASF dual-hosted git repository. rabbah pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/incubator-openwhisk.git
The following commit(s) were added to refs/heads/master by this push: new a8addf6 Extend system test suite (#3950) a8addf6 is described below commit a8addf629d56b789d0c1ce503cf6d691e6ecbf72 Author: Martin Gencur <mgen...@redhat.com> AuthorDate: Thu Oct 25 20:18:16 2018 +0200 Extend system test suite (#3950) --- .../src/main/resources/apiv1swagger.json | 6 +++ tests/dat/actions/argsPrint.js | 8 ++++ tests/dat/actions/runexception.js | 6 +++ tests/src/test/scala/common/WskCliOperations.scala | 10 ++++- tests/src/test/scala/common/WskOperations.scala | 1 + .../test/scala/common/rest/WskRestOperations.scala | 37 ++++++++++++---- .../test/scala/system/basic/WskActionTests.scala | 37 +++++++++++++++- .../whisk/core/cli/test/WskEntitlementTests.scala | 34 +++++++++++++++ .../core/cli/test/WskRestBasicUsageTests.scala | 1 + .../core/controller/test/ActivationsApiTests.scala | 51 ++++++++++++++++++++++ .../core/controller/test/PackagesApiTests.scala | 9 ++++ 11 files changed, 189 insertions(+), 11 deletions(-) diff --git a/core/controller/src/main/resources/apiv1swagger.json b/core/controller/src/main/resources/apiv1swagger.json index 989c5fa..8f3c332 100644 --- a/core/controller/src/main/resources/apiv1swagger.json +++ b/core/controller/src/main/resources/apiv1swagger.json @@ -1280,6 +1280,9 @@ "401": { "$ref": "#/responses/UnauthorizedRequest" }, + "403": { + "$ref": "#/responses/UnauthorizedRequest" + }, "500": { "$ref": "#/responses/ServerError" } @@ -1323,6 +1326,9 @@ "401": { "$ref": "#/responses/UnauthorizedRequest" }, + "403": { + "$ref": "#/responses/UnauthorizedRequest" + }, "404": { "$ref": "#/responses/ItemNotFound" }, diff --git a/tests/dat/actions/argsPrint.js b/tests/dat/actions/argsPrint.js new file mode 100644 index 0000000..6bd35ef --- /dev/null +++ b/tests/dat/actions/argsPrint.js @@ -0,0 +1,8 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more contributor +// license agreements; and to You under the Apache License, Version 2.0. + +function main(params) { + var param1 = params.param1 || ''; + var param2 = params.param2 || ''; + return {param1: param1, param2: param2}; +} diff --git a/tests/dat/actions/runexception.js b/tests/dat/actions/runexception.js new file mode 100644 index 0000000..4e15f9e --- /dev/null +++ b/tests/dat/actions/runexception.js @@ -0,0 +1,6 @@ +// Licensed to the Apache Software Foundation (ASF) under one or more contributor +// license agreements; and to You under the Apache License, Version 2.0. + +function main() { + throw "Extraordinary exception" +} diff --git a/tests/src/test/scala/common/WskCliOperations.scala b/tests/src/test/scala/common/WskCliOperations.scala index 459dc59..259ca9d 100644 --- a/tests/src/test/scala/common/WskCliOperations.scala +++ b/tests/src/test/scala/common/WskCliOperations.scala @@ -480,12 +480,14 @@ class CliActivationOperations(val wsk: RunCliCmd) extends ActivationOperations w * @param filter (optional) if define, must be a simple entity name * @param limit (optional) the maximum number of activation to return * @param since (optional) only the activations since this timestamp are included + * @param skip (optional) the number of activations to skip * @param expectedExitCode (optional) the expected exit code for the command * if the code is anything but DONTCARE_EXIT, assert the code is as expected */ def list(filter: Option[String] = None, limit: Option[Int] = None, since: Option[Instant] = None, + skip: Option[Int] = None, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RunResult = { val params = Seq(noun, "list", "--auth", wp.authKey) ++ { filter map { Seq(_) } getOrElse Seq.empty } ++ { limit map { l => @@ -495,6 +497,10 @@ class CliActivationOperations(val wsk: RunCliCmd) extends ActivationOperations w since map { i => Seq("--since", i.toEpochMilli.toString) } getOrElse Seq.empty + } ++ { + skip map { i => + Seq("--skip", i.toString) + } getOrElse Seq.empty } wsk.cli(wp.overrides ++ params, expectedExitCode) } @@ -606,6 +612,7 @@ class CliActivationOperations(val wsk: RunCliCmd) extends ActivationOperations w * @param entity the name of the entity to filter from activation list * @param limit the maximum number of entities to list (if entity name is not unique use Some(0)) * @param since (optional) only the activations since this timestamp are included + * @param skip (optional) the number of activations to skip * @param retries the maximum retries (total timeout is retries + 1 seconds) * @return activation ids found, caller must check length of sequence */ @@ -613,11 +620,12 @@ class CliActivationOperations(val wsk: RunCliCmd) extends ActivationOperations w entity: Option[String], limit: Option[Int] = None, since: Option[Instant] = None, + skip: Option[Int] = Some(0), retries: Int = 10, pollPeriod: Duration = 1.second)(implicit wp: WskProps): Seq[String] = { Try { retry({ - val result = ids(list(filter = entity, limit = limit, since = since)) + val result = ids(list(filter = entity, limit = limit, since = since, skip = skip)) if (result.length >= N) result else throw PartialResult(result) }, retries, waitBeforeRetry = Some(pollPeriod)) } match { diff --git a/tests/src/test/scala/common/WskOperations.scala b/tests/src/test/scala/common/WskOperations.scala index 9b012c7..5fb020e 100644 --- a/tests/src/test/scala/common/WskOperations.scala +++ b/tests/src/test/scala/common/WskOperations.scala @@ -314,6 +314,7 @@ trait ActivationOperations { entity: Option[String], limit: Option[Int] = None, since: Option[Instant] = None, + skip: Option[Int] = None, retries: Int, pollPeriod: Duration = 1.second)(implicit wp: WskProps): Seq[String] diff --git a/tests/src/test/scala/common/rest/WskRestOperations.scala b/tests/src/test/scala/common/rest/WskRestOperations.scala index 94023d7..66f6ef2 100644 --- a/tests/src/test/scala/common/rest/WskRestOperations.scala +++ b/tests/src/test/scala/common/rest/WskRestOperations.scala @@ -26,7 +26,6 @@ import org.apache.commons.io.{FileUtils, FilenameUtils} import org.scalatest.Matchers import org.scalatest.concurrent.ScalaFutures import org.scalatest.time.Span.convertDurationToSpan - import scala.collection.immutable.Seq import scala.concurrent.duration.Duration import scala.concurrent.duration.DurationInt @@ -40,7 +39,7 @@ import akka.http.scaladsl.model.StatusCodes.OK import akka.http.scaladsl.model.HttpRequest import akka.http.scaladsl.model.HttpMethod import akka.http.scaladsl.model.HttpResponse -import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials, HttpCredentials, OAuth2BearerToken} +import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials, OAuth2BearerToken} import akka.http.scaladsl.model.HttpEntity import akka.http.scaladsl.model.ContentTypes import akka.http.scaladsl.Http @@ -76,6 +75,7 @@ import akka.actor.ActorSystem import akka.util.ByteString import pureconfig.loadConfigOrThrow import whisk.common.Https.HttpsConfig +import whisk.common.AkkaLogging class AcceptAllHostNameVerifier extends HostnameVerifier { override def verify(s: String, sslSession: SSLSession): Boolean = true @@ -644,10 +644,12 @@ class RestActivationOperations(implicit val actorSystem: ActorSystem) def listActivation(filter: Option[String] = None, limit: Option[Int] = None, since: Option[Instant] = None, + skip: Option[Int] = None, docs: Boolean = true, expectedExitCode: Int = SUCCESS_EXIT)(implicit wp: WskProps): RestResult = { val entityPath = Path(s"${basePath}/namespaces/${wp.namespace}/$noun") - val paramMap = Map("skip" -> "0", "docs" -> docs.toString) ++ + val paramMap = Map("docs" -> docs.toString) ++ + skip.map(s => Map("skip" -> s.toString)).getOrElse(Map.empty) ++ limit.map(l => Map("limit" -> l.toString)).getOrElse(Map.empty) ++ filter.map(f => Map("name" -> f.toString)).getOrElse(Map.empty) ++ since.map(s => Map("since" -> s.toEpochMilli.toString)).getOrElse(Map.empty) @@ -706,6 +708,7 @@ class RestActivationOperations(implicit val actorSystem: ActorSystem) * @param entity the name of the entity to filter from activation list * @param limit the maximum number of entities to list (if entity name is not unique use Some(0)) * @param since (optional) only the activations since this timestamp are included + * @param skip (optional) the number of activations to skip * @param retries the maximum retries (total timeout is retries + 1 seconds) * @return activation ids found, caller must check length of sequence */ @@ -713,11 +716,13 @@ class RestActivationOperations(implicit val actorSystem: ActorSystem) entity: Option[String], limit: Option[Int] = Some(30), since: Option[Instant] = None, + skip: Option[Int] = Some(0), retries: Int = 10, pollPeriod: Duration = 1.second)(implicit wp: WskProps): Seq[String] = { Try { retry({ - val result = idsActivation(listActivation(filter = entity, limit = limit, since = since, docs = false)) + val result = + idsActivation(listActivation(filter = entity, limit = limit, since = since, skip = skip, docs = false)) if (result.length >= N) result else throw PartialResult(result) }, retries, waitBeforeRetry = Some(pollPeriod)) } match { @@ -732,13 +737,23 @@ class RestActivationOperations(implicit val actorSystem: ActorSystem) fieldFilter: Option[String] = None, last: Option[Boolean] = None, summary: Option[Boolean] = None)(implicit wp: WskProps): RestResult = { - val rr = activationId match { + val actId = activationId match { + case Some(id) => activationId + case None => + last match { + case Some(true) => { + val activations = pollFor(N = 1, entity = None, limit = Some(1)) + require(activations.size <= 1) + if (activations.isEmpty) None else Some(activations.head) + } + case _ => None + } + } + val rr = actId match { case Some(id) => val resp = requestEntity(GET, getNamePath(wp.namespace, noun, id)) new RestResult(resp.status, getRespData(resp)) - - case None => - new RestResult(NotFound) + case None => new RestResult(NotFound) } validateStatusCode(expectedExitCode, rr.statusCode.intValue) rr @@ -1141,6 +1156,7 @@ trait RunRestCmd extends Matchers with ScalaFutures with SwaggerValidator { val maxOpenRequest = 1024 val basePath = Path("/api/v1") val systemNamespace = "whisk.system" + val logger = new AkkaLogging(actorSystem.log) implicit val config = PatienceConfig(100 seconds, 15 milliseconds) implicit val actorSystem: ActorSystem @@ -1192,6 +1208,9 @@ trait RunRestCmd extends Matchers with ScalaFutures with SwaggerValidator { body.map(b => HttpEntity.Strict(ContentTypes.`application/json`, ByteString(b))).getOrElse(HttpEntity.Empty)) val response = Http().singleRequest(request, connectionContext).flatMap { _.toStrict(toStrictTimeout) }.futureValue + logger.debug(this, s"Request: $request") + logger.debug(this, s"Response: $response") + val validationErrors = validateRequestAndResponse(request, response) if (validationErrors.nonEmpty) { fail( @@ -1201,7 +1220,7 @@ trait RunRestCmd extends Matchers with ScalaFutures with SwaggerValidator { response } - private def getHttpCredentials(wp: WskProps): HttpCredentials = { + private def getHttpCredentials(wp: WskProps) = { if (wp.authKey.contains(":")) { val authKey = wp.authKey.split(":") new BasicHttpCredentials(authKey(0), authKey(1)) diff --git a/tests/src/test/scala/system/basic/WskActionTests.scala b/tests/src/test/scala/system/basic/WskActionTests.scala index 6db4b22..7830da7 100644 --- a/tests/src/test/scala/system/basic/WskActionTests.scala +++ b/tests/src/test/scala/system/basic/WskActionTests.scala @@ -32,7 +32,7 @@ import spray.json.DefaultJsonProtocol._ class WskActionTests extends TestHelpers with WskTestHelpers with JsHelpers with WskActorSystem { implicit val wskprops = WskProps() - val wsk: WskOperations = new WskRestOperations + val wsk = new WskRestOperations val testString = "this is a test" val testResult = JsObject("count" -> testString.split(" ").length.toJson) @@ -75,6 +75,21 @@ class WskActionTests extends TestHelpers with WskTestHelpers with JsHelpers with } } + it should "invoke an action that throws an uncaught exception and returns correct status code" in withAssetCleaner( + wskprops) { (wp, assetHelper) => + val name = "throwExceptionAction" + assetHelper.withCleaner(wsk.action, name) { (action, _) => + action.create(name, Some(TestUtils.getTestActionFilename("runexception.js"))) + } + + withActivation(wsk.activation, wsk.action.invoke(name)) { activation => + val response = activation.response + activation.response.status shouldBe "action developer error" + activation.response.result shouldBe Some( + JsObject("error" -> "An error has occurred: Extraordinary exception".toJson)) + } + } + it should "pass parameters bound on creation-time to the action" in withAssetCleaner(wskprops) { (wp, assetHelper) => val name = "printParams" val params = Map("param1" -> "test1", "param2" -> "test2") @@ -214,6 +229,26 @@ class WskActionTests extends TestHelpers with WskTestHelpers with JsHelpers with } } + it should "update an action with different language and check preserving params" in withAssetCleaner(wskprops) { + (wp, assetHelper) => + val name = "updatedAction" + + assetHelper.withCleaner(wsk.action, name, false) { (action, _) => + wsk.action.create( + name, + Some(TestUtils.getTestActionFilename("hello.js")), + parameters = Map("name" -> testString.toJson)) //unused in the first function + } + + wsk.action.create(name, Some(TestUtils.getTestActionFilename("hello.py")), update = true) + + val run = wsk.action.invoke(name) + withActivation(wsk.activation, run) { activation => + activation.response.status shouldBe "success" + activation.logs.get.mkString(" ") should include(s"Hello $testString") + } + } + it should "fail to invoke an action with an empty file" in withAssetCleaner(wskprops) { (wp, assetHelper) => val name = "empty" assetHelper.withCleaner(wsk.action, name) { (action, _) => diff --git a/tests/src/test/scala/whisk/core/cli/test/WskEntitlementTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskEntitlementTests.scala index 0e689c8..64a7793 100644 --- a/tests/src/test/scala/whisk/core/cli/test/WskEntitlementTests.scala +++ b/tests/src/test/scala/whisk/core/cli/test/WskEntitlementTests.scala @@ -160,6 +160,40 @@ abstract class WskEntitlementTests extends TestHelpers with WskTestHelpers with } } + it should "list shared packages when package is turned into public" in withAssetCleaner(guestWskProps) { + (wp, assetHelper) => + assetHelper.withCleaner(wsk.pkg, samplePackage) { (pkg, _) => + pkg.create(samplePackage)(wp) + } + + retry { + val packageList = wsk.pkg.list(Some(s"/$guestNamespace"))(defaultWskProps) + verifyPackageNotSharedList(packageList, guestNamespace, samplePackage) + } + + wsk.pkg.create(samplePackage, update = true, shared = Some(true))(wp) + + retry { + val packageList = wsk.pkg.list(Some(s"/$guestNamespace"))(defaultWskProps) + verifyPackageSharedList(packageList, guestNamespace, samplePackage) + } + } + + //TODO: convert to API-level test under whisk.core.controller once issues/3959 is resolved + it should "reject getting package from invalid namespace" in withAssetCleaner(guestWskProps) { (wp, assetHelper) => + val invalidNamespace = "whisk.systsdf" + wsk.pkg.get(s"/${invalidNamespace}/utils", expectedExitCode = forbiddenCode)(wp).stderr should include( + "not authorized") + } + + //TODO: convert to API-level test under whisk.core.controller once issues/3959 is resolved + it should "reject getting invalid package from valid namespace" in withAssetCleaner(guestWskProps) { + (wp, assetHelper) => + val invalidPackage = "utilssss" + wsk.pkg.get(s"/whisk.system/${invalidPackage}", expectedExitCode = forbiddenCode)(wp).stderr should include( + "not authorized") + } + def verifyPackageSharedList(packageList: RunResult, namespace: String, packageName: String): Unit = { val fullyQualifiedPackageName = s"/$namespace/$packageName" withClue(s"Packagelist is: ${packageList.stdout}; Packagename is: $fullyQualifiedPackageName")( diff --git a/tests/src/test/scala/whisk/core/cli/test/WskRestBasicUsageTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskRestBasicUsageTests.scala index 7fd950a..097ed25 100644 --- a/tests/src/test/scala/whisk/core/cli/test/WskRestBasicUsageTests.scala +++ b/tests/src/test/scala/whisk/core/cli/test/WskRestBasicUsageTests.scala @@ -342,6 +342,7 @@ class WskRestBasicUsageTests extends TestHelpers with WskTestHelpers with WskAct } val args = Map("hello" -> "Robert".toJson) val run = wsk.action.invoke(name, args, blocking = true, result = true) + //--result takes precedence over --blocking run.stdout.parseJson shouldBe args.toJson } diff --git a/tests/src/test/scala/whisk/core/controller/test/ActivationsApiTests.scala b/tests/src/test/scala/whisk/core/controller/test/ActivationsApiTests.scala index 6810ef2..7d5bf85 100644 --- a/tests/src/test/scala/whisk/core/controller/test/ActivationsApiTests.scala +++ b/tests/src/test/scala/whisk/core/controller/test/ActivationsApiTests.scala @@ -468,6 +468,57 @@ class ActivationsApiTests extends ControllerTestCommon with WhiskActivationsApi } } + it should "skip activations and return correct ones" in { + implicit val tid = transid() + val activations: Seq[WhiskActivation] = (1 to 3).map { i => + //make sure the time is different for each activation + val time = Instant.now.plusMillis(i) + WhiskActivation(namespace, aname(), creds.subject, ActivationId.generate(), start = time, end = time) + }.toList + + try { + activations.foreach(storeActivation(_, context)) + waitOnListActivationsInNamespace(namespace, activations.size, context) + + Get(s"$collectionPath?skip=1") ~> Route.seal(routes(creds)) ~> check { + status should be(OK) + val resultActivationIds = responseAs[List[JsObject]].map(_.fields("name")) + val expectedActivationIds = activations.map(_.toJson.fields("name")).reverse.drop(1) + resultActivationIds should be(expectedActivationIds) + } + } finally { + activations.foreach(a => deleteActivation(ActivationId(a.docid.asString), context)) + waitOnListActivationsInNamespace(namespace, 0, context) + } + } + + it should "return last activation" in { + implicit val tid = transid() + val activations = (1 to 3).map { i => + //make sure the time is different for each activation + val time = Instant.now.plusMillis(i) + WhiskActivation(namespace, aname(), creds.subject, ActivationId.generate(), start = time, end = time) + }.toList + + try { + activations.foreach(storeActivation(_, context)) + waitOnListActivationsInNamespace(namespace, activations.size, context) + + Get(s"$collectionPath?limit=1") ~> Route.seal(routes(creds)) ~> check { + status should be(OK) + val activationsJson = activations.map(_.toJson) + withClue(s"Original activations: ${activationsJson}") { + val respNames = responseAs[List[JsObject]].map(_.fields("name")) + val expectNames = activationsJson.map(_.fields("name")).drop(2) + respNames should be(expectNames) + } + } + } finally { + activations.foreach(a => deleteActivation(ActivationId(a.docid.asString), context)) + waitOnListActivationsInNamespace(namespace, 0, context) + } + } + //// GET /activations/id it should "get activation by id" in { implicit val tid = transid() diff --git a/tests/src/test/scala/whisk/core/controller/test/PackagesApiTests.scala b/tests/src/test/scala/whisk/core/controller/test/PackagesApiTests.scala index 88ad062..1be2065 100644 --- a/tests/src/test/scala/whisk/core/controller/test/PackagesApiTests.scala +++ b/tests/src/test/scala/whisk/core/controller/test/PackagesApiTests.scala @@ -797,6 +797,15 @@ class PackagesApiTests extends ControllerTestCommon with WhiskPackagesApi { } } + it should "return empty list for invalid namespace" in { + implicit val tid = transid() + val path = s"/whisk.systsdf/${collection.path}" + Get(path) ~> Route.seal(routes(creds)) ~> check { + status should be(OK) + responseAs[List[JsObject]] should be(List.empty) + } + } + it should "reject bind to non-package" in { implicit val tid = transid() val action = WhiskAction(namespace, aname(), jsDefault("??"))