csantanapr closed pull request #2944: Replace test cases of api gateway with REST URL: https://github.com/apache/incubator-openwhisk/pull/2944
This is a PR merged from a forked repository. As GitHub hides the original diff on merge, it is displayed below for the sake of provenance: As this is a foreign pull request (from a fork), the diff is supplied below (as it won't show otherwise due to GitHub magic): diff --git a/tests/src/test/scala/apigw/healthtests/ApiGwCliEndToEndTests.scala b/tests/src/test/scala/apigw/healthtests/ApiGwCliEndToEndTests.scala new file mode 100644 index 0000000000..3025fab242 --- /dev/null +++ b/tests/src/test/scala/apigw/healthtests/ApiGwCliEndToEndTests.scala @@ -0,0 +1,33 @@ +/* + * 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 apigw.healthtests + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +import common.Wsk +import common.TestUtils._ + +/** + * Basic tests of the download link for Go CLI binaries + */ +@RunWith(classOf[JUnitRunner]) +class ApiGwCliEndToEndTests extends ApiGwEndToEndTests { + override lazy val wsk: common.Wsk = new Wsk + override val createCode: Int = SUCCESS_EXIT +} diff --git a/tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala b/tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala index d0dbe92658..8995c7a7be 100644 --- a/tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala +++ b/tests/src/test/scala/apigw/healthtests/ApiGwEndToEndTests.scala @@ -34,7 +34,7 @@ import com.jayway.restassured.RestAssured import common.TestHelpers import common.TestUtils import common.TestUtils._ -import common.Wsk +import common.BaseWsk import common.WskProps import common.WskTestHelpers import spray.json._ @@ -45,7 +45,7 @@ import system.rest.RestUtil * Basic tests of the download link for Go CLI binaries */ @RunWith(classOf[JUnitRunner]) -class ApiGwEndToEndTests +abstract class ApiGwEndToEndTests extends FlatSpec with Matchers with RestUtil @@ -53,23 +53,58 @@ class ApiGwEndToEndTests with WskTestHelpers with BeforeAndAfterAll { - implicit val wskprops = WskProps() - val wsk = new Wsk - val clinamespace = wsk.namespace.whois() + implicit val wskprops: common.WskProps = WskProps() + val wsk: BaseWsk + val namespace: String = wsk.namespace.whois() + val createCode: Int // Custom CLI properties file - val cliWskPropsFile = File.createTempFile("wskprops", ".tmp") + val cliWskPropsFile: java.io.File = File.createTempFile("wskprops", ".tmp") /* * Create a CLI properties file for use by the tests */ - override def beforeAll() = { + override def beforeAll: Unit = { cliWskPropsFile.deleteOnExit() val wskprops = WskProps(token = "SOME TOKEN") wskprops.writeFile(cliWskPropsFile) println(s"wsk temporary props file created here: ${cliWskPropsFile.getCanonicalPath()}") } + def verifyAPICreated(rr: RunResult): Unit = { + rr.stdout should include("ok: created API") + val apiurl = rr.stdout.split("\n")(1) + println(s"apiurl: '$apiurl'") + } + + def verifyAPIList(rr: RunResult, + actionName: String, + testurlop: String, + testapiname: String, + testbasepath: String, + testrelpath: String): Unit = { + rr.stdout should include("ok: APIs") + rr.stdout should include regex (s"$actionName\\s+$testurlop\\s+$testapiname\\s+") + rr.stdout should include(testbasepath + testrelpath) + } + + def verifyAPISwaggerCreated(rr: RunResult): Unit = { + rr.stdout should include("ok: created API") + } + + def writeSwaggerFile(rr: RunResult): File = { + val swaggerfile = File.createTempFile("api", ".json") + swaggerfile.deleteOnExit() + val bw = new BufferedWriter(new FileWriter(swaggerfile)) + bw.write(rr.stdout) + bw.close() + return swaggerfile + } + + def getSwaggerApiUrl(rr: RunResult): String = { + return rr.stdout.split("\n")(1) + } + behavior of "Wsk api" it should s"create an API and successfully invoke that API" in { @@ -83,7 +118,7 @@ class ApiGwEndToEndTests val urlqueryvalue = testName try { - println("cli namespace: " + clinamespace) + println("Namespace: " + namespace) // Delete any lingering stale api from previous run that may not have been deleted properly wsk.api.delete( @@ -93,12 +128,14 @@ class ApiGwEndToEndTests // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo-web-http.js") + println("action creation Namespace: " + namespace) wsk.action.create( name = actionName, artifact = Some(file), - expectedExitCode = SUCCESS_EXIT, + expectedExitCode = createCode, annotations = Map("web-export" -> true.toJson)) + println("creation Namespace: " + namespace) // Create the API var rr = wsk.api.create( basepath = Some(testbasepath), @@ -108,9 +145,7 @@ class ApiGwEndToEndTests apiname = Some(testapiname), responsetype = Some("http"), cliCfgFile = Some(cliWskPropsFile.getCanonicalPath())) - rr.stdout should include("ok: created API") - val apiurl = rr.stdout.split("\n")(1) - println(s"apiurl: '$apiurl'") + verifyAPICreated(rr) // Validate the API was successfully created // List result will look like: @@ -122,17 +157,11 @@ class ApiGwEndToEndTests relpath = Some(testrelpath), operation = Some(testurlop), cliCfgFile = Some(cliWskPropsFile.getCanonicalPath())) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"$actionName\\s+$testurlop\\s+$testapiname\\s+") - rr.stdout should include(testbasepath + testrelpath) + verifyAPIList(rr, actionName, testurlop, testapiname, testbasepath, testrelpath) // Recreate the API using a JSON swagger file rr = wsk.api.get(basepathOrApiName = Some(testbasepath), cliCfgFile = Some(cliWskPropsFile.getCanonicalPath())) - val swaggerfile = File.createTempFile("api", ".json") - swaggerfile.deleteOnExit() - val bw = new BufferedWriter(new FileWriter(swaggerfile)) - bw.write(rr.stdout) - bw.close() + val swaggerfile = writeSwaggerFile(rr) // Delete API to that it can be recreated again using the generated swagger file val deleteApiResult = wsk.api.delete( @@ -143,8 +172,8 @@ class ApiGwEndToEndTests // Create the API again, but use the swagger file this time rr = wsk.api .create(swagger = Some(swaggerfile.getAbsolutePath()), cliCfgFile = Some(cliWskPropsFile.getCanonicalPath())) - rr.stdout should include("ok: created API") - val swaggerapiurl = rr.stdout.split("\n")(1) + verifyAPISwaggerCreated(rr) + val swaggerapiurl = getSwaggerApiUrl(rr) println(s"Returned api url: '${swaggerapiurl}'") // Call the API URL and validate the results diff --git a/tests/src/test/scala/apigw/healthtests/ApiGwRestEndToEndTests.scala b/tests/src/test/scala/apigw/healthtests/ApiGwRestEndToEndTests.scala new file mode 100644 index 0000000000..279da3cc09 --- /dev/null +++ b/tests/src/test/scala/apigw/healthtests/ApiGwRestEndToEndTests.scala @@ -0,0 +1,93 @@ +/* + * 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 apigw.healthtests + +import java.io.BufferedWriter +import java.io.File +import java.io.FileWriter + +import akka.http.scaladsl.model.StatusCodes.OK + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +import common.TestUtils._ +import common.rest.WskRest +import common.rest.RestResult + +@RunWith(classOf[JUnitRunner]) +class ApiGwRestEndToEndTests extends ApiGwEndToEndTests { + + override lazy val wsk: common.rest.WskRest = new WskRest + override val createCode: Int = OK.intValue + + override def verifyAPICreated(rr: RunResult): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + apiResultRest.statusCode shouldBe OK + val apiurl = apiResultRest.getField("gwApiUrl") + "/path" + println(s"apiurl: '$apiurl'") + } + + override def verifyAPIList(rr: RunResult, + actionName: String, + testurlop: String, + testapiname: String, + testbasepath: String, + testrelpath: String): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject("apis")(0), "value") + val apidoc = RestResult.getFieldJsObject(apiValue, "apidoc") + val basepath = RestResult.getField(apidoc, "basePath") + basepath shouldBe testbasepath + + val paths = RestResult.getFieldJsObject(apidoc, "paths") + paths.fields.contains(testrelpath) shouldBe true + + val info = RestResult.getFieldJsObject(apidoc, "info") + val title = RestResult.getField(info, "title") + title shouldBe testapiname + + val relpath = RestResult.getFieldJsObject(paths, testrelpath) + val urlop = RestResult.getFieldJsObject(relpath, testurlop) + val openwhisk = RestResult.getFieldJsObject(urlop, "x-openwhisk") + val actionN = RestResult.getField(openwhisk, "action") + actionN shouldBe actionName + } + + override def verifyAPISwaggerCreated(rr: RunResult): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + apiResultRest.statusCode shouldBe OK + } + + override def writeSwaggerFile(rr: RunResult): File = { + val swaggerfile = File.createTempFile("api", ".json") + swaggerfile.deleteOnExit() + val bw = new BufferedWriter(new FileWriter(swaggerfile)) + val apiResultRest = rr.asInstanceOf[RestResult] + val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject("apis")(0), "value") + val apidoc = RestResult.getFieldJsObject(apiValue, "apidoc") + bw.write(apidoc.toString()) + bw.close() + return swaggerfile + } + + override def getSwaggerApiUrl(rr: RunResult): String = { + val apiResultRest = rr.asInstanceOf[RestResult] + return apiResultRest.getField("gwApiUrl") + "/path" + } +} diff --git a/tests/src/test/scala/common/rest/WskRest.scala b/tests/src/test/scala/common/rest/WskRest.scala index 71551a65f4..0ae28f93a5 100644 --- a/tests/src/test/scala/common/rest/WskRest.scala +++ b/tests/src/test/scala/common/rest/WskRest.scala @@ -190,8 +190,8 @@ trait DeleteFromCollectionRest extends BaseDeleteFromCollection { * @param expectedExitCode (optional) the expected exit code for the command * if the code is anything but DONTCARE_EXIT, assert the code is as expected */ - override def delete(entity: String, expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = { - val (ns, entityName) = getNamespaceEntityName(entity) + override def delete(name: String, expectedExitCode: Int = OK.intValue)(implicit wp: WskProps): RestResult = { + val (ns, entityName) = getNamespaceEntityName(name) val path = Path(s"$basePath/namespaces/$ns/$noun/$entityName") val resp = requestEntity(DELETE, path)(wp) val r = new RestResult(resp.status, getRespData(resp)) @@ -482,7 +482,7 @@ class WskRestTrigger var body: Map[String, JsValue] = Map( "lifecycleEvent" -> "CREATE".toJson, "triggerName" -> s"/$ns/$triggerName".toJson, - "authKey" -> s"${getAuthKey(wp)}".toJson) + "authKey" -> s"${wp.authKey}".toJson) body = body ++ parameters val resp = requestEntity(POST, path, paramMap, Some(body.toJson.toString())) val resultInvoke = new RestResult(resp.status, getRespData(resp)) @@ -971,11 +971,10 @@ class WskRestApi extends RunWskRestCmd with BaseApi { val r = action match { case Some(action) => { val (ns, actionName) = this.getNamespaceEntityName(action) - val actionUrl = s"${WhiskProperties.getApiHostForAction}/$basePath/web/$ns/default/$actionName.http" - val actionAuthKey = this.getAuthKey(wp) + val actionUrl = s"${WhiskProperties.getApiHostForAction}$basePath/web/$ns/default/$actionName.http" + val actionAuthKey = wp.authKey val testaction = Some( - ApiAction(name = actionName, namespace = ns, backendUrl = actionUrl, authkey = actionAuthKey)) - + new ApiAction(name = actionName, namespace = ns, backendUrl = actionUrl, authkey = actionAuthKey)) val parms = Map[String, JsValue]() ++ { Map("namespace" -> ns.toJson) } ++ { basepath map { b => Map("gatewayBasePath" -> b.toJson) @@ -1003,14 +1002,16 @@ class WskRestApi extends RunWskRestCmd with BaseApi { } getOrElse Map[String, JsValue]() } - val parm = Map[String, JsValue]("apidoc" -> JsObject(parms)) ++ { Map("__ow_user" -> ns.toJson) } ++ { + val spaceguid = if (wp.authKey.contains(":")) wp.authKey.split(":")(0) else wp.authKey + + val parm = Map[String, JsValue]("apidoc" -> JsObject(parms)) ++ { responsetype map { r => Map("responsetype" -> r.toJson) } getOrElse Map[String, JsValue]() } ++ { Map("accesstoken" -> wp.authKey.toJson) } ++ { - Map("spaceguid" -> wp.authKey.split(":")(0).toJson) + Map("spaceguid" -> spaceguid.toJson) } invokeAction( @@ -1018,10 +1019,44 @@ class WskRestApi extends RunWskRestCmd with BaseApi { parameters = parm, blocking = true, result = true, + web = true, expectedExitCode = expectedExitCode)(wp) } case None => { - new RestResult(NotFound) + swagger match { + case Some(swaggerFile) => { + var file = "" + val fileName = swaggerFile.toString() + try { + file = FileUtils.readFileToString(new File(fileName)) + } catch { + case e: Throwable => + return new RestResult( + NotFound, + JsObject("error" -> s"Error reading swagger file '$fileName'".toJson).toString()) + } + val parms = Map("namespace" -> s"${wp.namespace}".toJson, "swagger" -> file.toJson) + val parm = Map[String, JsValue]("apidoc" -> JsObject(parms)) ++ { + responsetype map { r => + Map("responsetype" -> r.toJson) + } getOrElse Map[String, JsValue]() + } ++ { + Map("accesstoken" -> wp.authKey.toJson) + } ++ { + Map("spaceguid" -> wp.authKey.split(":")(0).toJson) + } + invokeAction( + name = "apimgmt/createApi", + parameters = parm, + blocking = true, + result = true, + web = true, + expectedExitCode = expectedExitCode)(wp) + } + case None => { + new RestResult(NotFound) + } + } } } r @@ -1043,8 +1078,7 @@ class WskRestApi extends RunWskRestCmd with BaseApi { expectedExitCode: Int = SUCCESS_EXIT, cliCfgFile: Option[String] = None)(implicit wp: WskProps): RestResult = { - val parms = Map[String, JsValue]() ++ - Map("__ow_user" -> wp.namespace.toJson) ++ { + val parms = Map[String, JsValue]() ++ { basepathOrApiName map { b => Map("basepath" -> b.toJson) } getOrElse Map[String, JsValue]() @@ -1061,11 +1095,13 @@ class WskRestApi extends RunWskRestCmd with BaseApi { } ++ { Map("spaceguid" -> wp.authKey.split(":")(0).toJson) } + val rr = invokeAction( name = "apimgmt/getApi", parameters = parms, blocking = true, result = true, + web = true, expectedExitCode = OK.intValue)(wp) rr } @@ -1082,8 +1118,7 @@ class WskRestApi extends RunWskRestCmd with BaseApi { expectedExitCode: Int = SUCCESS_EXIT, cliCfgFile: Option[String] = None, format: Option[String] = None)(implicit wp: WskProps): RestResult = { - val parms = Map[String, JsValue]() ++ - Map("__ow_user" -> wp.namespace.toJson) ++ { + val parms = Map[String, JsValue]() ++ { basepathOrApiName map { b => Map("basepath" -> b.toJson) } getOrElse Map[String, JsValue]() @@ -1098,6 +1133,7 @@ class WskRestApi extends RunWskRestCmd with BaseApi { parameters = parms, blocking = true, result = true, + web = true, expectedExitCode = OK.intValue)(wp) result } @@ -1113,7 +1149,7 @@ class WskRestApi extends RunWskRestCmd with BaseApi { operation: Option[String] = None, expectedExitCode: Int = SUCCESS_EXIT, cliCfgFile: Option[String] = None)(implicit wp: WskProps): RestResult = { - val parms = Map[String, JsValue]() ++ { Map("__ow_user" -> wp.namespace.toJson) } ++ { + val parms = Map[String, JsValue]() ++ { Map("basepath" -> basepathOrApiName.toJson) } ++ { relpath map { r => @@ -1134,18 +1170,10 @@ class WskRestApi extends RunWskRestCmd with BaseApi { parameters = parms, blocking = true, result = true, + web = true, expectedExitCode = expectedExitCode)(wp) return rr } - - def getApi(basepathOrApiName: String, params: Map[String, String] = Map(), expectedExitCode: Int = OK.intValue)( - implicit wp: WskProps): RestResult = { - val whiskUrl = Uri(WhiskProperties.getApiHostForAction) - val path = Path(s"/api/${wp.authKey.split(":")(0)}$basepathOrApiName/path") - val resp = requestEntity(GET, path, params, whiskUrl = whiskUrl) - val result = new RestResult(resp.status, getRespData(resp)) - result - } } class RunWskRestCmd() extends FlatSpec with RunWskCmd with Matchers with ScalaFutures with WskActorSystem { @@ -1155,6 +1183,7 @@ class RunWskRestCmd() extends FlatSpec with RunWskCmd with Matchers with ScalaFu val queueSize = 10 val maxOpenRequest = 1024 val basePath = Path("/api/v1") + val systemNamespace = "whisk.system" val sslConfig = AkkaSSLConfig().mapSettings { s => s.withHostnameVerifierClass(classOf[AcceptAllHostNameVerifier].asInstanceOf[Class[HostnameVerifier]]) @@ -1239,11 +1268,6 @@ class RunWskRestCmd() extends FlatSpec with RunWskCmd with Matchers with ScalaFu } } - def getAuthKey(wp: WskProps): String = { - val authKey = wp.authKey.split(":") - s"${authKey(0)}:${authKey(1)}" - } - def getParamsAnnos(parameters: Map[String, JsValue] = Map(), annotations: Map[String, JsValue] = Map(), parameterFile: Option[String] = None, @@ -1345,9 +1369,12 @@ class RunWskRestCmd() extends FlatSpec with RunWskCmd with Matchers with ScalaFu parameterFile: Option[String] = None, blocking: Boolean = false, result: Boolean = false, + web: Boolean = false, expectedExitCode: Int = Accepted.intValue)(implicit wp: WskProps): RestResult = { val (ns, actName) = this.getNamespaceEntityName(name) - val path = Path(s"$basePath/namespaces/$ns/actions/$actName") + val path = + if (web) Path(s"$basePath/web/$systemNamespace/$actName.http") + else Path(s"$basePath/namespaces/$ns/actions/$actName") var paramMap = Map("blocking" -> blocking.toString, "result" -> result.toString) val input = parameterFile map { pf => Some(FileUtils.readFileToString(new File(pf))) @@ -1496,11 +1523,11 @@ class RestResult(var statusCode: StatusCode, var respData: String = "", blocking } } -case class ApiAction(name: String, - namespace: String, - backendMethod: String = "POST", - backendUrl: String, - authkey: String) { +class ApiAction(var name: String, + var namespace: String, + var backendMethod: String = "POST", + var backendUrl: String, + var authkey: String) { def toJson(): JsObject = { return JsObject( "name" -> name.toJson, diff --git a/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwCliRoutemgmtActionTests.scala b/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwCliRoutemgmtActionTests.scala new file mode 100644 index 0000000000..4f0f131632 --- /dev/null +++ b/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwCliRoutemgmtActionTests.scala @@ -0,0 +1,28 @@ +/* + * 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 whisk.core.apigw.actions.test + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +import common.Wsk + +@RunWith(classOf[JUnitRunner]) +class ApiGwCliRoutemgmtActionTests extends ApiGwRoutemgmtActionTests { + override lazy val wsk = new Wsk +} diff --git a/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRestRoutemgmtActionTests.scala b/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRestRoutemgmtActionTests.scala new file mode 100644 index 0000000000..aff8072144 --- /dev/null +++ b/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRestRoutemgmtActionTests.scala @@ -0,0 +1,31 @@ +/* + * 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 whisk.core.apigw.actions.test + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +import common.rest.WskRest + +/** + * Tests for basic CLI usage. Some of these tests require a deployed backend. + */ +@RunWith(classOf[JUnitRunner]) +class ApiGwRestRoutemgmtActionTests extends ApiGwRoutemgmtActionTests { + override lazy val wsk = new WskRest +} diff --git a/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala b/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala index fa56de62a0..57449e6931 100644 --- a/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala +++ b/tests/src/test/scala/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala @@ -24,38 +24,23 @@ import org.scalatest.junit.JUnitRunner import common.JsHelpers import common.StreamLogging import common.TestHelpers -import common.TestUtils.ANY_ERROR_EXIT import common.TestUtils.DONTCARE_EXIT import common.TestUtils.RunResult import common.TestUtils.SUCCESS_EXIT -import common.Wsk +import common.BaseWsk import common.WskActorSystem import common.WskAdmin import common.WskProps +import common.rest.ApiAction import common.WskTestHelpers import spray.json._ import spray.json.DefaultJsonProtocol._ -case class ApiAction(name: String, - namespace: String, - backendMethod: String = "POST", - backendUrl: String, - authkey: String) { - def toJson(): JsObject = { - return JsObject( - "name" -> name.toJson, - "namespace" -> namespace.toJson, - "backendMethod" -> backendMethod.toJson, - "backendUrl" -> backendUrl.toJson, - "authkey" -> authkey.toJson) - } -} - /** * Tests for basic CLI usage. Some of these tests require a deployed backend. */ @RunWith(classOf[JUnitRunner]) -class ApiGwRoutemgmtActionTests +abstract class ApiGwRoutemgmtActionTests extends TestHelpers with BeforeAndAfterAll with WskActorSystem @@ -65,7 +50,7 @@ class ApiGwRoutemgmtActionTests val systemId = "whisk.system" implicit val wskprops = WskProps(authKey = WskAdmin.listKeys(systemId)(0)._1, namespace = systemId) - val wsk = new Wsk + val wsk: BaseWsk def getApis(bpOrName: Option[String], relpath: Option[String] = None, @@ -299,7 +284,7 @@ class ApiGwRoutemgmtActionTests val actionUrl = "https://some.whisk.host/api/v1/web/" + actionNamespace + "/default/" + actionName + ".json" val actionAuthKey = testName + "_authkey" val testaction = - ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey) + new ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey) try { val createResult = createApi( @@ -330,7 +315,7 @@ class ApiGwRoutemgmtActionTests val actionUrl = "https://some.whisk.host/api/v1/web/" + actionNamespace + "/default/" + actionName + ".json" val actionAuthKey = testName + "_authkey" val testaction = - ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey) + new ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey) try { val createResult = createApi( @@ -366,7 +351,7 @@ class ApiGwRoutemgmtActionTests val actionUrl = "https://some.whisk.host/api/v1/web/" + actionNamespace + "/default/" + actionName + ".json" val actionAuthKey = testName + "_authkey" val testaction = - ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey) + new ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey) try { var createResult = createApi( @@ -393,202 +378,4 @@ class ApiGwRoutemgmtActionTests deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT) } } - - it should "reject apimgmt actions that are invoked with not enough parameters" in { - val invalidArgs = Seq( - //getApi - ("/whisk.system/apimgmt/getApi", ANY_ERROR_EXIT, "Invalid authentication.", Seq()), - //deleteApi - ( - "/whisk.system/apimgmt/deleteApi", - ANY_ERROR_EXIT, - "Invalid authentication.", - Seq("-p", "basepath", "/ApiGwRoutemgmtActionTests_bp")), - ( - "/whisk.system/apimgmt/deleteApi", - ANY_ERROR_EXIT, - "basepath is required", - Seq("-p", "__ow_user", "_", "-p", "accesstoken", "TOKEN")), - ( - "/whisk.system/apimgmt/deleteApi", - ANY_ERROR_EXIT, - "When specifying an operation, the path is required", - Seq( - "-p", - "__ow_user", - "_", - "-p", - "accesstoken", - "TOKEN", - "-p", - "basepath", - "/ApiGwRoutemgmtActionTests_bp", - "-p", - "operation", - "get")), - //createApi - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "apidoc is required", - Seq("-p", "__ow_user", "_", "-p", "accesstoken", "TOKEN")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "apidoc is missing the namespace field", - Seq("-p", "__ow_user", "_", "-p", "accesstoken", "TOKEN", "-p", "apidoc", "{}")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "apidoc is missing the gatewayBasePath field", - Seq("-p", "__ow_user", "_", "-p", "accesstoken", "TOKEN", "-p", "apidoc", """{"namespace":"_"}""")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "apidoc is missing the gatewayPath field", - Seq( - "-p", - "__ow_user", - "_", - "-p", - "accesstoken", - "TOKEN", - "-p", - "apidoc", - """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp"}""")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "apidoc is missing the gatewayMethod field", - Seq( - "-p", - "__ow_user", - "_", - "-p", - "accesstoken", - "TOKEN", - "-p", - "apidoc", - """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp"}""")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "apidoc is missing the action field", - Seq( - "-p", - "__ow_user", - "_", - "-p", - "accesstoken", - "TOKEN", - "-p", - "apidoc", - """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get"}""")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "action is missing the backendMethod field", - Seq( - "-p", - "__ow_user", - "_", - "-p", - "accesstoken", - "TOKEN", - "-p", - "apidoc", - """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{}}""")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "action is missing the backendUrl field", - Seq( - "-p", - "__ow_user", - "_", - "-p", - "accesstoken", - "TOKEN", - "-p", - "apidoc", - """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post"}}""")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "action is missing the namespace field", - Seq( - "-p", - "__ow_user", - "_", - "-p", - "accesstoken", - "TOKEN", - "-p", - "apidoc", - """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL"}}""")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "action is missing the name field", - Seq( - "-p", - "__ow_user", - "_", - "-p", - "accesstoken", - "TOKEN", - "-p", - "apidoc", - """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_"}}""")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "action is missing the authkey field", - Seq( - "-p", - "__ow_user", - "_", - "-p", - "accesstoken", - "TOKEN", - "-p", - "apidoc", - """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_","name":"N"}}""")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "swagger and gatewayBasePath are mutually exclusive and cannot be specified together", - Seq( - "-p", - "__ow_user", - "_", - "-p", - "accesstoken", - "TOKEN", - "-p", - "apidoc", - """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_","name":"N","authkey":"XXXX"},"swagger":{}}""")), - ( - "/whisk.system/apimgmt/createApi", - ANY_ERROR_EXIT, - "apidoc field cannot be parsed. Ensure it is valid JSON", - Seq("-p", "__ow_user", "_", "-p", "accesstoken", "TOKEN", "-p", "apidoc", "{1:[}}}"))) - - invalidArgs foreach { - case (action: String, exitcode: Int, errmsg: String, params: Seq[String]) => - val cmd: Seq[String] = Seq( - "action", - "invoke", - action, - "-i", - "-b", - "-r", - "--apihost", - wskprops.apihost, - "--auth", - wskprops.authKey) ++ params - val rr = wsk.cli(cmd, expectedExitCode = exitcode) - rr.stderr should include regex (errmsg) - } - } } diff --git a/tests/src/test/scala/whisk/core/cli/test/ApiGwCliTests.scala b/tests/src/test/scala/whisk/core/cli/test/ApiGwCliTests.scala new file mode 100644 index 0000000000..ba886c0f0b --- /dev/null +++ b/tests/src/test/scala/whisk/core/cli/test/ApiGwCliTests.scala @@ -0,0 +1,33 @@ +/* + * 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 whisk.core.cli.test + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +import common.Wsk +import common.TestUtils.SUCCESS_EXIT + +/** + * Tests for basic CLI usage. Some of these tests require a deployed backend. + */ +@RunWith(classOf[JUnitRunner]) +class ApiGwCliTests extends ApiGwTests { + override lazy val wsk: common.Wsk = new Wsk + override lazy val createCode = SUCCESS_EXIT +} diff --git a/tests/src/test/scala/whisk/core/cli/test/ApiGwRestTests.scala b/tests/src/test/scala/whisk/core/cli/test/ApiGwRestTests.scala new file mode 100644 index 0000000000..88099537f8 --- /dev/null +++ b/tests/src/test/scala/whisk/core/cli/test/ApiGwRestTests.scala @@ -0,0 +1,248 @@ +/* + * 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 whisk.core.cli.test + +import akka.http.scaladsl.model.StatusCodes.OK + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner +import spray.json.JsObject + +import common.rest.WskRest +import common.rest.RestResult +import common.TestUtils.RunResult + +/** + * Tests for testing the CLI "api" subcommand. Most of these tests require a deployed backend. + */ +@RunWith(classOf[JUnitRunner]) +class ApiGwRestTests extends ApiGwTests { + override lazy val wsk = new WskRest + override lazy val createCode = OK.intValue + + override def verifyBadCommands(rr: RunResult, badpath: String): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val error = RestResult.getField(apiResultRest.respBody, "error") + error should include("Error: Resource path must begin with '/'.") + } + + override def verifyBadCommandsDelete(rr: RunResult, badpath: String): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val error = RestResult.getField(apiResultRest.respBody, "error") + error should include(s"API deletion failure: API '/basepath' does not exist") + } + + override def verifyBadCommandsList(rr: RunResult, badpath: String): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val apis = apiResultRest.getFieldListJsObject("apis") + apis.size shouldBe 0 + } + + override def verifyInvalidCommands(rr: RunResult, badverb: String): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val error = apiResultRest.getField("error") + error should include(s"Error: Resource verb '${badverb}' not supported") + } + + override def verifyInvalidCommandsDelete(rr: RunResult, badverb: String): Unit = { + verifyBadCommandsDelete(rr, badverb) + } + + override def verifyInvalidCommandsList(rr: RunResult, badverb: String): Unit = { + verifyBadCommandsList(rr, badverb) + } + + override def verifyNonJsonSwagger(rr: RunResult, filename: String): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val error = apiResultRest.getField("error") + error should include(s"swagger field cannot be parsed. Ensure it is valid JSON") + } + + override def verifyMissingField(rr: RunResult): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val error = apiResultRest.getField("error") + error should include(s"swagger is missing the basePath field.") + } + + override def verifyApiCreated(rr: RunResult): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + apiResultRest.statusCode shouldBe OK + } + + def verifyList(rr: RunResult, + namespace: String, + actionName: String, + testurlop: String, + testbasepath: String, + testrelpath: String, + testapiname: String, + newEndpoint: String = ""): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject("apis")(0), "value") + val apidoc = RestResult.getFieldJsObject(apiValue, "apidoc") + val basepath = RestResult.getField(apidoc, "basePath") + basepath shouldBe testbasepath + + val paths = RestResult.getFieldJsObject(apidoc, "paths") + paths.fields.contains(testrelpath) shouldBe true + + val info = RestResult.getFieldJsObject(apidoc, "info") + val title = RestResult.getField(info, "title") + title shouldBe testapiname + + verifyPaths(paths, testrelpath, testurlop, actionName, namespace) + + if (newEndpoint != "") { + verifyPaths(paths, newEndpoint, testurlop, actionName, namespace) + } + } + + def verifyPaths(paths: JsObject, + testrelpath: String, + testurlop: String, + actionName: String, + namespace: String = "") = { + val relpath = RestResult.getFieldJsObject(paths, testrelpath) + val urlop = RestResult.getFieldJsObject(relpath, testurlop) + val openwhisk = RestResult.getFieldJsObject(urlop, "x-openwhisk") + val actionN = RestResult.getField(openwhisk, "action") + actionN shouldBe actionName + + if (namespace != "") { + val namespaceS = RestResult.getField(openwhisk, "namespace") + namespaceS shouldBe namespace + } + } + + override def verifyApiList(rr: RunResult, + clinamespace: String, + actionName: String, + testurlop: String, + testbasepath: String, + testrelpath: String, + testapiname: String): Unit = { + verifyList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname) + } + + override def verifyApiGet(rr: RunResult): Unit = { + rr.stdout should include regex (s""""operationId":"getPathWithSub_pathsInIt"""") + } + + override def verifyApiFullList(rr: RunResult, + clinamespace: String, + actionName: String, + testurlop: String, + testbasepath: String, + testrelpath: String, + testapiname: String): Unit = { + verifyList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname) + } + + override def verifyApiFullListDouble(rr: RunResult, + clinamespace: String, + actionName: String, + testurlop: String, + testbasepath: String, + testrelpath: String, + testapiname: String, + newEndpoint: String): Unit = { + verifyList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname, newEndpoint) + } + + override def verifyApiDeleted(rr: RunResult): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + apiResultRest.statusCode shouldBe OK + } + + override def verifyApiDeletedRelpath(rr: RunResult, + testrelpath: String, + testbasepath: String, + op: String = ""): Unit = { + verifyApiDeleted(rr) + } + + override def verifyApiNameGet(rr: RunResult, + testbasepath: String, + actionName: String, + responseType: String = "json"): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + + val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject("apis")(0), "value") + val apidoc = RestResult.getFieldJsObject(apiValue, "apidoc") + + val config = RestResult.getFieldJsObject(apidoc, "x-ibm-configuration") + + val cors = RestResult.getFieldJsObject(config, "cors") + val enabled = RestResult.getFieldJsValue(cors, "enabled").toString() + enabled shouldBe "true" + + val basepath = RestResult.getField(apidoc, "basePath") + basepath shouldBe testbasepath + + val paths = RestResult.getFieldJsObject(apidoc, "paths") + val relpath = RestResult.getFieldJsObject(paths, "/path") + val urlop = RestResult.getFieldJsObject(relpath, "get") + val openwhisk = RestResult.getFieldJsObject(urlop, "x-openwhisk") + val actionN = RestResult.getField(openwhisk, "action") + actionN shouldBe actionName + rr.stdout should include regex (s""""target-url":".*${actionName}.${responseType}"""") + } + + override def verifyInvalidSwagger(rr: RunResult): Unit = { + verifyMissingField(rr) + } + + override def verifyApiOp(rr: RunResult, testurlop: String, testapiname: String): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject("apis")(0), "value") + val apidoc = RestResult.getFieldJsObject(apiValue, "apidoc") + val info = RestResult.getFieldJsObject(apidoc, "info") + val title = RestResult.getField(info, "title") + title shouldBe testapiname + val paths = RestResult.getFieldJsObject(apidoc, "paths") + val relpath = RestResult.getFieldJsObject(paths, "/") + val urlop = RestResult.getFieldJsObject(relpath, testurlop) + relpath.fields.contains(testurlop) shouldBe true + } + + override def verifyApiBaseRelPath(rr: RunResult, testbasepath: String, testrelpath: String): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject("apis")(0), "value") + val apidoc = RestResult.getFieldJsObject(apiValue, "apidoc") + val basepath = RestResult.getField(apidoc, "basePath") + basepath shouldBe testbasepath + + val paths = RestResult.getFieldJsObject(apidoc, "paths") + paths.fields.contains(testrelpath) shouldBe true + } + + override def verifyApiOpVerb(rr: RunResult, testurlop: String): Unit = { + val apiResultRest = rr.asInstanceOf[RestResult] + val apiValue = RestResult.getFieldJsObject(apiResultRest.getFieldListJsObject("apis")(0), "value") + val apidoc = RestResult.getFieldJsObject(apiValue, "apidoc") + val paths = RestResult.getFieldJsObject(apidoc, "paths") + val relpath = RestResult.getFieldJsObject(paths, "/") + val urlop = RestResult.getFieldJsObject(relpath, testurlop) + relpath.fields.contains(testurlop) shouldBe true + } + + override def verifyInvalidKey(rr: RunResult): Unit = { + rr.stderr should include("A valid auth key is required") + } + +} diff --git a/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala b/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala index 9f152b08e5..145ea6bb00 100644 --- a/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala +++ b/tests/src/test/scala/whisk/core/cli/test/ApiGwTests.scala @@ -20,149 +20,147 @@ package whisk.core.cli.test import java.io.File import java.io.BufferedWriter import java.io.FileWriter -import java.time.Instant - -import scala.collection.mutable.ArrayBuffer -import scala.concurrent.duration._ import org.junit.runner.RunWith -import org.scalatest.BeforeAndAfterAll -import org.scalatest.BeforeAndAfterEach import org.scalatest.junit.JUnitRunner -import common.TestHelpers import common.TestUtils._ import common.TestUtils -import common.WhiskProperties -import common.Wsk import common.WskProps -import common.WskTestHelpers /** * Tests for testing the CLI "api" subcommand. Most of these tests require a deployed backend. */ @RunWith(classOf[JUnitRunner]) -class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach with BeforeAndAfterAll { +abstract class ApiGwTests extends BaseApiGwTests { - implicit val wskprops = WskProps() - val wsk = new Wsk val clinamespace = wsk.namespace.whois() + val createCode: Int - // This test suite makes enough CLI invocations in 60 seconds to trigger the OpenWhisk - // throttling restriction. To avoid CLI failures due to being throttled, track the - // CLI invocation calls and when at the throttle limit, pause the next CLI invocation - // with exactly enough time to relax the throttling. - val maxActionsPerMin = WhiskProperties.getMaxActionInvokesPerMinute() - val invocationTimes = new ArrayBuffer[Instant]() - - // Custom CLI properties file - val cliWskPropsFile = File.createTempFile("wskprops", ".tmp") - - /** - * Expected to be called before each test. - * Assumes that each test will not invoke more than 5 actions and - * settle the throttle when there isn't enough capacity to handle the test. - */ - def checkThrottle(maxInvocationsBeforeThrottle: Int = maxActionsPerMin, expectedActivationsPerTest: Int = 5) = { - val t = Instant.now - val tminus60 = t.minusSeconds(60) - val invocationsLast60Seconds = invocationTimes.filter(_.isAfter(tminus60)).sorted - val invocationCount = invocationsLast60Seconds.length - println(s"Action invokes within last minute: ${invocationCount}") - - if (invocationCount >= maxInvocationsBeforeThrottle) { - // Instead of waiting a fixed 60 seconds to settle the throttle, - // calculate a wait time that will clear out about half of the - // current invocations (assuming even distribution) from the - // next 60 second period. - val oldestInvocationInLast60Seconds = invocationsLast60Seconds.head - - // Take the oldest invocation time in this 60 second period. To clear - // this invocation from the next 60 second period, the wait time will be - // (60sec - oldest invocation's delta time away from the period end). - // This will clear all of the invocations from the next period at the - // expense of potentially waiting uncessarily long. Instead, this calculation - // halves the delta time as a compromise. - val throttleTime = 60.seconds.toMillis - ((t.toEpochMilli - oldestInvocationInLast60Seconds.toEpochMilli) / 2) - println(s"Waiting ${throttleTime} milliseconds to settle the throttle") - Thread.sleep(throttleTime) - } + def verifyBadCommands(rr: RunResult, badpath: String): Unit = { + rr.stderr should include(s"'${badpath}' must begin with '/'") + } + + def verifyBadCommandsDelete(rr: RunResult, badpath: String): Unit = { + verifyBadCommands(rr, badpath) + } + + def verifyBadCommandsList(rr: RunResult, badpath: String): Unit = { + verifyBadCommands(rr, badpath) + } + + def verifyInvalidCommands(rr: RunResult, badverb: String): Unit = { + rr.stderr should include(s"'${badverb}' is not a valid API verb. Valid values are:") + } + + def verifyInvalidCommandsDelete(rr: RunResult, badverb: String): Unit = { + verifyInvalidCommands(rr, badverb) + } + + def verifyInvalidCommandsList(rr: RunResult, badverb: String): Unit = { + verifyInvalidCommands(rr, badverb) + } + + def verifyNonJsonSwagger(rr: RunResult, filename: String): Unit = { + rr.stderr should include(s"Error parsing swagger file '${filename}':") + } + + def verifyMissingField(rr: RunResult): Unit = { + rr.stderr should include(s"Swagger file is invalid (missing basePath, info, paths, or swagger fields") + } + + def verifyApiCreated(rr: RunResult): Unit = { + rr.stdout should include("ok: created API") + } - invocationTimes += Instant.now + def verifyApiList(rr: RunResult, + clinamespace: String, + actionName: String, + testurlop: String, + testbasepath: String, + testrelpath: String, + testapiname: String): Unit = { + rr.stdout should include("ok: APIs") + rr.stdout should include regex (s"Action:\\s+/${clinamespace}/${actionName}\n") + rr.stdout should include regex (s"Verb:\\s+${testurlop}\n") + rr.stdout should include regex (s"Base path:\\s+${testbasepath}\n") + rr.stdout should include regex (s"Path:\\s+${testrelpath}\n") + rr.stdout should include regex (s"API Name:\\s+${testapiname}\n") + rr.stdout should include regex (s"URL:\\s+") + rr.stdout should include(testbasepath + testrelpath) } - override def beforeEach() = { - //checkThrottle() + def verifyApiBaseRelPath(rr: RunResult, testbasepath: String, testrelpath: String): Unit = { + rr.stdout should include(testbasepath + testrelpath) } - /* - * Create a CLI properties file for use by the tests - */ - override def beforeAll() = { - cliWskPropsFile.deleteOnExit() - val wskprops = WskProps(token = "SOME TOKEN") - wskprops.writeFile(cliWskPropsFile) - println(s"wsk temporary props file created here: ${cliWskPropsFile.getCanonicalPath()}") + def verifyApiGet(rr: RunResult): Unit = { + rr.stdout should include regex (s""""operationId":\\s+"getPathWithSub_pathsInIt"""") } - /* - * Forcibly clear the throttle so that downstream tests are not affected by - * this test suite - */ - override def afterAll() = { - // Check and settle the throttle so that this test won't cause issues with and follow on tests - checkThrottle(30) + def verifyApiFullList(rr: RunResult, + clinamespace: String, + actionName: String, + testurlop: String, + testbasepath: String, + testrelpath: String, + testapiname: String): Unit = { + + rr.stdout should include("ok: APIs") + if (clinamespace == "") { + rr.stdout should include regex (s"/[@\\w._\\-]+/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") + } else { + rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") + } + rr.stdout should include(testbasepath + testrelpath) + + } + + def verifyApiFullListDouble(rr: RunResult, + clinamespace: String, + actionName: String, + testurlop: String, + testbasepath: String, + testrelpath: String, + testapiname: String, + newEndpoint: String): Unit = { + verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname) + rr.stdout should include(testbasepath + newEndpoint) + } + + def verifyApiDeleted(rr: RunResult): Unit = { + rr.stdout should include("ok: deleted API") } - def apiCreate(basepath: Option[String] = None, - relpath: Option[String] = None, - operation: Option[String] = None, - action: Option[String] = None, - apiname: Option[String] = None, - swagger: Option[String] = None, - responsetype: Option[String] = None, - expectedExitCode: Int = SUCCESS_EXIT, - cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath()))( - implicit wskpropsOverride: WskProps): RunResult = { - - checkThrottle() - wsk.api.create(basepath, relpath, operation, action, apiname, swagger, responsetype, expectedExitCode, cliCfgFile)( - wskpropsOverride) + def verifyApiDeletedRelpath(rr: RunResult, testrelpath: String, testbasepath: String, op: String = ""): Unit = { + if (op != "") + rr.stdout should include("ok: deleted " + testrelpath + " " + op.toUpperCase() + " from " + testbasepath) + else + rr.stdout should include("ok: deleted " + testrelpath + " from " + testbasepath) } - def apiList(basepathOrApiName: Option[String] = None, - relpath: Option[String] = None, - operation: Option[String] = None, - limit: Option[Int] = None, - since: Option[Instant] = None, - full: Option[Boolean] = None, - nameSort: Option[Boolean] = None, - expectedExitCode: Int = SUCCESS_EXIT, - cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath())): RunResult = { - - checkThrottle() - wsk.api.list(basepathOrApiName, relpath, operation, limit, since, full, nameSort, expectedExitCode, cliCfgFile) + def verifyApiNameGet(rr: RunResult, testbasepath: String, actionName: String, responseType: String = "json"): Unit = { + rr.stdout should include(testbasepath) + rr.stdout should include(s"${actionName}") + rr.stdout should include regex (""""cors":\s*\{\s*\n\s*"enabled":\s*true""") + rr.stdout should include regex (s""""target-url":\\s+.*${actionName}.${responseType}""") } - def apiGet(basepathOrApiName: Option[String] = None, - full: Option[Boolean] = None, - expectedExitCode: Int = SUCCESS_EXIT, - cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath()), - format: Option[String] = None): RunResult = { + def verifyInvalidSwagger(rr: RunResult): Unit = { + rr.stderr should include(s"Swagger file is invalid") + } - checkThrottle() - wsk.api.get(basepathOrApiName, full, expectedExitCode, cliCfgFile, format) + def verifyApiOp(rr: RunResult, testurlop: String, testapiname: String): Unit = { + rr.stdout should include regex (s"\\s+${testurlop}\\s+${testapiname}\\s+") } - def apiDelete(basepathOrApiName: String, - relpath: Option[String] = None, - operation: Option[String] = None, - expectedExitCode: Int = SUCCESS_EXIT, - cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath())): RunResult = { + def verifyApiOpVerb(rr: RunResult, testurlop: String): Unit = { + rr.stdout should include regex (s"Verb:\\s+${testurlop}") + } - checkThrottle() - wsk.api.delete(basepathOrApiName, relpath, operation, expectedExitCode, cliCfgFile) + def verifyInvalidKey(rr: RunResult): Unit = { + rr.stderr should include("The supplied authentication is invalid") } behavior of "Wsk api" @@ -176,21 +174,21 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some("GET"), action = Some("action"), expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include(s"'${badpath}' must begin with '/'") + verifyBadCommands(rr, badpath) rr = apiDelete( basepathOrApiName = "/basepath", relpath = Some(badpath), operation = Some("GET"), expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include(s"'${badpath}' must begin with '/'") + verifyBadCommandsDelete(rr, badpath) rr = apiList( basepathOrApiName = Some("/basepath"), relpath = Some(badpath), operation = Some("GET"), expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include(s"'${badpath}' must begin with '/'") + verifyBadCommandsList(rr, badpath) } it should "reject an api commands with an invalid verb parameter" in { @@ -202,28 +200,28 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(badverb), action = Some("action"), expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include(s"'${badverb}' is not a valid API verb. Valid values are:") + verifyInvalidCommands(rr, badverb) rr = apiDelete( basepathOrApiName = "/basepath", relpath = Some("/path"), operation = Some(badverb), expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include(s"'${badverb}' is not a valid API verb. Valid values are:") + verifyInvalidCommandsDelete(rr, badverb) rr = apiList( basepathOrApiName = Some("/basepath"), relpath = Some("/path"), operation = Some(badverb), expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include(s"'${badverb}' is not a valid API verb. Valid values are:") + verifyInvalidCommandsList(rr, badverb) } it should "reject an api create command that specifies a nonexistent configuration file" in { val configfile = "/nonexistent/file" val rr = apiCreate(swagger = Some(configfile), expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include(s"Error reading swagger file '${configfile}':") + rr.stderr should include(s"Error reading swagger file '${configfile}'") } it should "reject an api create command specifying a non-JSON configuration file" in { @@ -236,7 +234,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach bw.close() val rr = apiCreate(swagger = Some(filename), expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include(s"Error parsing swagger file '${filename}':") + verifyNonJsonSwagger(rr, filename) } it should "reject an api create command specifying a non-swagger JSON configuration file" in { @@ -261,7 +259,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach bw.close() val rr = apiCreate(swagger = Some(filename), expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include(s"Swagger file is invalid (missing basePath, info, paths, or swagger fields") + verifyMissingField(rr) } it should "verify full list output" in { @@ -276,7 +274,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach println("cli namespace: " + clinamespace) // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -285,21 +283,14 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach action = Some(actionName), apiname = Some(testapiname)) println("api create: " + rr.stdout) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiList( basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), full = Some(true)) println("api list: " + rr.stdout) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"Action:\\s+/${clinamespace}/${actionName}\n") - rr.stdout should include regex (s"Verb:\\s+${testurlop}\n") - rr.stdout should include regex (s"Base path:\\s+${testbasepath}\n") - rr.stdout should include regex (s"Path:\\s+${testrelpath}\n") - rr.stdout should include regex (s"API Name:\\s+${testapiname}\n") - rr.stdout should include regex (s"URL:\\s+") - rr.stdout should include(testbasepath + testrelpath) + verifyApiList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath) @@ -319,7 +310,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -327,15 +318,13 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop)) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath + testrelpath) + verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname) rr = apiGet(basepathOrApiName = Some(testbasepath)) - rr.stdout should include regex (s""""operationId":\\s+"getPathWithSub_pathsInIt"""") + verifyApiGet(rr) val deleteresult = apiDelete(basepathOrApiName = testbasepath) - deleteresult.stdout should include("ok: deleted API") + verifyApiDeleted(deleteresult) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -353,7 +342,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach try { // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -361,12 +350,9 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiGet(basepathOrApiName = Some(testapiname)) - rr.stdout should include(testbasepath) - rr.stdout should include(s"${actionName}") - rr.stdout should include regex (""""cors":\s*\{\s*\n\s*"enabled":\s*true""") - rr.stdout should include regex (s""""target-url":\\s+.*${actionName}.json""") + verifyApiNameGet(rr, testbasepath, actionName) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -384,7 +370,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach try { // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -392,9 +378,9 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiDelete(basepathOrApiName = testapiname) - rr.stdout should include("ok: deleted API") + verifyApiDeleted(rr) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -412,7 +398,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach try { // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -420,9 +406,9 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiDelete(basepathOrApiName = testbasepath) - rr.stdout should include("ok: deleted API") + verifyApiDeleted(rr) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -441,7 +427,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach try { // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -449,19 +435,24 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiCreate( basepath = Some(testbasepath), relpath = Some(newEndpoint), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiList(basepathOrApiName = Some(testbasepath)) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath + testrelpath) - rr.stdout should include(testbasepath + newEndpoint) + verifyApiFullListDouble( + rr, + clinamespace, + actionName, + testurlop, + testbasepath, + newEndpoint, + testapiname, + newEndpoint) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -479,14 +470,12 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach val swaggerPath = TestUtils.getTestApiGwFilename("testswaggerdoc1") try { var rr = apiCreate(swagger = Some(swaggerPath)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop)) println("list stdout: " + rr.stdout) println("list stderr: " + rr.stderr) - rr.stdout should include("ok: APIs") - // Actual CLI namespace will vary from local dev to automated test environments, so don't check - rr.stdout should include regex (s"/[@\\w._\\-]+/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath + testrelpath) + verifyApiFullList(rr, "", actionName, testurlop, testbasepath, testrelpath, testapiname) + } finally { apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) } @@ -506,7 +495,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach try { // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -514,14 +503,14 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiCreate( basepath = Some(testbasepath2), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname2)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) // Update both APIs - each with a new endpoint rr = apiCreate( @@ -529,25 +518,36 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach relpath = Some(newEndpoint), operation = Some(testurlop), action = Some(actionName)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiCreate( basepath = Some(testbasepath2), relpath = Some(newEndpoint), operation = Some(testurlop), action = Some(actionName)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiList(basepathOrApiName = Some(testbasepath)) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath + testrelpath) - rr.stdout should include(testbasepath + newEndpoint) + verifyApiFullListDouble( + rr, + clinamespace, + actionName, + testurlop, + testbasepath, + testrelpath, + testapiname, + newEndpoint) rr = apiList(basepathOrApiName = Some(testbasepath2)) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath2 + testrelpath) - rr.stdout should include(testbasepath2 + newEndpoint) + verifyApiFullListDouble( + rr, + clinamespace, + actionName, + testurlop, + testbasepath2, + testrelpath, + testapiname2, + newEndpoint) + } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -570,7 +570,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -578,13 +578,11 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop)) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath + testrelpath) + verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname) val deleteresult = apiDelete(basepathOrApiName = testbasepath) - deleteresult.stdout should include("ok: deleted API") + verifyApiDeleted(deleteresult) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -604,7 +602,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach val rr = apiCreate(swagger = Some(swaggerPath), expectedExitCode = ANY_ERROR_EXIT) println("api create stdout: " + rr.stdout) println("api create stderr: " + rr.stderr) - rr.stderr should include(s"Swagger file is invalid") + verifyInvalidSwagger(rr) } finally { apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) } @@ -621,7 +619,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach try { // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -629,20 +627,18 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) var rr2 = apiCreate( basepath = Some(testbasepath), relpath = Some(testnewrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr2.stdout should include("ok: created API") + verifyApiCreated(rr2) rr = apiDelete(basepathOrApiName = testbasepath, relpath = Some(testrelpath)) - rr.stdout should include("ok: deleted " + testrelpath + " from " + testbasepath) + verifyApiDeletedRelpath(rr, testrelpath, testbasepath) rr2 = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testnewrelpath)) - rr2.stdout should include("ok: APIs") - rr2.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr2.stdout should include(testbasepath + testnewrelpath) + verifyApiFullList(rr2, clinamespace, actionName, testurlop, testbasepath, testnewrelpath, testapiname) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -661,7 +657,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach try { // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -669,22 +665,22 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiCreate( basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop2), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiList(basepathOrApiName = Some(testbasepath)) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath + testrelpath) + verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname) + verifyApiFullList(rr, clinamespace, actionName, testurlop2, testbasepath, testrelpath, testapiname) rr = apiDelete(basepathOrApiName = testbasepath, relpath = Some(testrelpath), operation = Some(testurlop2)) - rr.stdout should include("ok: deleted " + testrelpath + " " + "POST" + " from " + testbasepath) + verifyApiDeletedRelpath(rr, testrelpath, testbasepath, testurlop2) + rr = apiList(basepathOrApiName = Some(testbasepath)) - rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") + verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -697,6 +693,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach val testrelpath = "/whisk_system/utils/echo" val testrelpath2 = "/whisk_system/utils/split" val testurlop = "get" + val testurlop2 = "post" val testapiname = testName + " API Name" val actionName = "test1a" val swaggerPath = TestUtils.getTestApiGwFilename(s"testswaggerdoc2") @@ -704,13 +701,10 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach var rr = apiCreate(swagger = Some(swaggerPath)) println("api create stdout: " + rr.stdout) println("api create stderror: " + rr.stderr) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiList(basepathOrApiName = Some(testbasepath)) - rr.stdout should include("ok: APIs") - // Actual CLI namespace will vary from local dev to automated test environments, so don't check - rr.stdout should include regex (s"/[@\\w._\\-]+/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath + testrelpath) - rr.stdout should include(testbasepath + testrelpath2) + verifyApiFullList(rr, "", actionName, testurlop, testbasepath, testrelpath, testapiname) + verifyApiFullList(rr, "", actionName, testurlop2, testbasepath, testrelpath2, testapiname) } finally { apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) } @@ -729,7 +723,7 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach try { // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) var rr = apiCreate( basepath = Some(testbasepath), @@ -737,30 +731,24 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop)) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath + testrelpath) + verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname) rr = apiCreate( basepath = Some(testbasepath2), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname2)) - rr.stdout should include("ok: created API") + verifyApiCreated(rr) rr = apiList(basepathOrApiName = Some(testbasepath2), relpath = Some(testrelpath), operation = Some(testurlop)) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath2 + testrelpath) + verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath2, testrelpath, testapiname2) rr = apiDelete(basepathOrApiName = testbasepath2) - rr.stdout should include("ok: deleted API") + verifyApiDeleted(rr) rr = apiList(basepathOrApiName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop)) - rr.stdout should include("ok: APIs") - rr.stdout should include regex (s"/${clinamespace}/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath + testrelpath) + verifyApiFullList(rr, clinamespace, actionName, testurlop, testbasepath, testrelpath, testapiname) rr = apiDelete(basepathOrApiName = testbasepath) - rr.stdout should include("ok: deleted API") + verifyApiDeleted(rr) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -768,55 +756,6 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach } } - it should "reject an API created with a non-existent action" in { - val testName = "CLI_APIGWTEST15" - val testbasepath = "/" + testName + "_bp" - val testrelpath = "/path" - val testnewrelpath = "/path_new" - val testurlop = "get" - val testapiname = testName + " API Name" - val actionName = testName + "_action" - try { - val rr = apiCreate( - basepath = Some(testbasepath), - relpath = Some(testrelpath), - operation = Some(testurlop), - action = Some(actionName), - apiname = Some(testapiname), - expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include("does not exist") - } finally { - apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) - } - } - - it should "reject an API created with an action that is not a web action" in { - val testName = "CLI_APIGWTEST16" - val testbasepath = "/" + testName + "_bp" - val testrelpath = "/path" - val testnewrelpath = "/path_new" - val testurlop = "get" - val testapiname = testName + " API Name" - val actionName = testName + "_action" - try { - // Create the action for the API. It must NOT be a "web-action" action for this test - val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT) - - val rr = apiCreate( - basepath = Some(testbasepath), - relpath = Some(testrelpath), - operation = Some(testurlop), - action = Some(actionName), - apiname = Some(testapiname), - expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include("is not a web action") - } finally { - wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) - apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) - } - } - it should "verify API with http response type " in { val testName = "CLI_APIGWTEST17" val testbasepath = "/" + testName + "_bp" @@ -829,133 +768,25 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach try { // Create the action for the API. It must be a "web-action" action. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) - - apiCreate( - basepath = Some(testbasepath), - relpath = Some(testrelpath), - operation = Some(testurlop), - action = Some(actionName), - apiname = Some(testapiname), - responsetype = Some(responseType)).stdout should include("ok: created API") - - val rr = apiGet(basepathOrApiName = Some(testapiname)) - rr.stdout should include(testbasepath) - rr.stdout should include(s"${actionName}") - rr.stdout should include regex (s""""target-url":\\s+.*${actionName}.${responseType}""") - } finally { - wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) - apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) - } - } - - it should "reject API export when export type is invalid" in { - val testName = "CLI_APIGWTEST18" - val testbasepath = "/" + testName + "_bp" - - val rr = apiGet(basepathOrApiName = Some(testbasepath), format = Some("BadType"), expectedExitCode = ANY_ERROR_EXIT) - rr.stderr should include("Invalid format type") - } - - it should "successfully export an API in YAML format" in { - val testName = "CLI_APIGWTEST19" - val testbasepath = "/" + testName + "_bp" - val testrelpath = "/path" - val testnewrelpath = "/path_new" - val testurlop = "get" - val testapiname = testName + " API Name" - val actionName = testName + "_action" - val responseType = "http" - try { - // Create the action for the API. It must be a "web-action" action. - val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) - apiCreate( - basepath = Some(testbasepath), - relpath = Some(testrelpath), - operation = Some(testurlop), - action = Some(actionName), - apiname = Some(testapiname), - responsetype = Some(responseType)).stdout should include("ok: created API") - - val rr = apiGet(basepathOrApiName = Some(testapiname), format = Some("yaml")) - rr.stdout should include(s"basePath: ${testbasepath}") - } finally { - wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) - apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) - } - } - - it should "successfully export an API when JSON format is explcitly specified" in { - val testName = "CLI_APIGWTEST20" - val testbasepath = "/" + testName + "_bp" - val testrelpath = "/path" - val testnewrelpath = "/path_new" - val testurlop = "get" - val testapiname = testName + " API Name" - val actionName = testName + "_action" - val responseType = "http" - try { - // Create the action for the API. It must be a "web-action" action. - val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) - - apiCreate( + var rr = apiCreate( basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname), - responsetype = Some(responseType)).stdout should include("ok: created API") + responsetype = Some(responseType)) + verifyApiCreated(rr) - val rr = apiGet(basepathOrApiName = Some(testapiname), format = Some("json")) - rr.stdout should include(testbasepath) - rr.stdout should include(s"${actionName}") - rr.stdout should include regex (s""""target-url":\\s+.*${actionName}.${responseType}""") + rr = apiGet(basepathOrApiName = Some(testapiname)) + verifyApiNameGet(rr, testbasepath, actionName, responseType) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) } } - it should "successfully create an API from a YAML formatted API configuration file" in { - val testName = "CLI_APIGWTEST21" - val testbasepath = "/bp" - val testrelpath = "/rp" - val testurlop = "get" - val testapiname = testbasepath - val actionName = "webhttpecho" - val swaggerPath = TestUtils.getTestApiGwFilename(s"local.api.yaml") - try { - var rr = apiCreate(swagger = Some(swaggerPath)) - println("api create stdout: " + rr.stdout) - println("api create stderror: " + rr.stderr) - rr.stdout should include("ok: created API") - rr = apiList(basepathOrApiName = Some(testbasepath)) - rr.stdout should include("ok: APIs") - // Actual CLI namespace will vary from local dev to automated test environments, so don't check - rr.stdout should include regex (s"/[@\\w._\\-]+/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") - rr.stdout should include(testbasepath + testrelpath) - } finally { - apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) - } - } - - it should "reject creation of an API from invalid YAML formatted API configuration file" in { - val testName = "CLI_APIGWTEST22" - val testbasepath = "/" + testName + "_bp" - val swaggerPath = TestUtils.getTestApiGwFilename(s"local.api.bad.yaml") - try { - val rr = apiCreate(swagger = Some(swaggerPath), expectedExitCode = ANY_ERROR_EXIT) - println("api create stdout: " + rr.stdout) - println("api create stderror: " + rr.stderr) - rr.stderr should include("Unable to parse YAML configuration file") - } finally { - apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) - } - } - it should "reject deletion of a non-existent api" in { val nonexistentApi = "/not-there" @@ -975,21 +806,21 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach var rr = apiCreate(swagger = Some(swaggerPath)) println("api create stdout: " + rr.stdout) println("api create stderror: " + rr.stderr) - rr.stdout should include("ok: created API") + this.verifyApiCreated(rr) rr = apiList(basepathOrApiName = Some(testbasepath)) println("api list:\n" + rr.stdout) testops foreach { testurlop => - rr.stdout should include regex (s"\\s+${testurlop}\\s+${testapiname}\\s+") + verifyApiOp(rr, testurlop, testapiname) } - rr.stdout should include(testbasepath + testrelpath) + verifyApiBaseRelPath(rr, testbasepath, testrelpath) rr = apiList(basepathOrApiName = Some(testbasepath), full = Some(true)) println("api full list:\n" + rr.stdout) testops foreach { testurlop => - rr.stdout should include regex (s"Verb:\\s+${testurlop}") + verifyApiOpVerb(rr, testurlop) } - rr.stdout should include(testbasepath + testrelpath) + verifyApiBaseRelPath(rr, testbasepath, testrelpath) } finally { apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) @@ -1007,58 +838,22 @@ class ApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach try { // Create the action for the API. val file = TestUtils.getTestActionFilename(s"echo.js") - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = createCode, web = Some("true")) // Set an invalid auth key val badWskProps = WskProps(authKey = "bad-auth-key") - apiCreate( + val rr = apiCreate( basepath = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop), action = Some(actionName), apiname = Some(testapiname), - expectedExitCode = ANY_ERROR_EXIT)(badWskProps).stderr should include("The supplied authentication is invalid") + expectedExitCode = ANY_ERROR_EXIT)(badWskProps) + verifyInvalidKey(rr) } finally { wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) } } - - it should "list api alphabetically by Base/Rel/Verb" in { - val baseName = "/BaseTestPathApiList" - val actionName = "actionName" - val file = TestUtils.getTestActionFilename(s"echo-web-http.js") - try { - // Create Action for apis - var action = - wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) - println("action creation: " + action.stdout) - // Create apis - for (i <- 1 to 3) { - val base = s"$baseName$i" - var api = apiCreate( - basepath = Some(base), - relpath = Some("/relPath"), - operation = Some("GET"), - action = Some(actionName)) - println("api creation: " + api.stdout) - } - val original = apiList(nameSort = Some(true)).stdout - val originalFull = apiList(full = Some(true), nameSort = Some(true)).stdout - val scalaSorted = List(s"${baseName}1" + "/", s"${baseName}2" + "/", s"${baseName}3" + "/") - val regex = s"${baseName}[1-3]/".r - val list = (regex.findAllMatchIn(original)).toList - val listFull = (regex.findAllMatchIn(originalFull)).toList - - scalaSorted.toString shouldEqual list.toString - scalaSorted.toString shouldEqual listFull.toString - } finally { - // Clean up Apis - for (i <- 1 to 3) { - apiDelete(basepathOrApiName = s"${baseName}$i", expectedExitCode = DONTCARE_EXIT) - } - wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) - } - } } diff --git a/tests/src/test/scala/whisk/core/cli/test/BaseApiGwTests.scala b/tests/src/test/scala/whisk/core/cli/test/BaseApiGwTests.scala new file mode 100644 index 0000000000..f55ab797eb --- /dev/null +++ b/tests/src/test/scala/whisk/core/cli/test/BaseApiGwTests.scala @@ -0,0 +1,165 @@ +/* + * 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 whisk.core.cli.test + +import java.io.File +import java.time.Instant + +import scala.collection.mutable.ArrayBuffer +import scala.concurrent.duration._ + +import org.junit.runner.RunWith + +import org.scalatest.BeforeAndAfterAll +import org.scalatest.BeforeAndAfterEach +import org.scalatest.junit.JUnitRunner + +import common.TestHelpers +import common.TestUtils._ +import common.WhiskProperties +import common.BaseWsk +import common.WskProps +import common.WskTestHelpers + +/** + * Tests for testing the CLI "api" subcommand. Most of these tests require a deployed backend. + */ +@RunWith(classOf[JUnitRunner]) +abstract class BaseApiGwTests extends TestHelpers with WskTestHelpers with BeforeAndAfterEach with BeforeAndAfterAll { + + implicit val wskprops = WskProps() + val wsk: BaseWsk + + // This test suite makes enough CLI invocations in 60 seconds to trigger the OpenWhisk + // throttling restriction. To avoid CLI failures due to being throttled, track the + // CLI invocation calls and when at the throttle limit, pause the next CLI invocation + // with exactly enough time to relax the throttling. + val maxActionsPerMin = WhiskProperties.getMaxActionInvokesPerMinute() + val invocationTimes = new ArrayBuffer[Instant]() + + // Custom CLI properties file + val cliWskPropsFile = File.createTempFile("wskprops", ".tmp") + + /** + * Expected to be called before each test. + * Assumes that each test will not invoke more than 5 actions and + * settle the throttle when there isn't enough capacity to handle the test. + */ + def checkThrottle(maxInvocationsBeforeThrottle: Int = maxActionsPerMin, expectedActivationsPerTest: Int = 5) = { + val t = Instant.now + val tminus60 = t.minusSeconds(60) + val invocationsLast60Seconds = invocationTimes.filter(_.isAfter(tminus60)).sorted + val invocationCount = invocationsLast60Seconds.length + println(s"Action invokes within last minute: ${invocationCount}") + + if (invocationCount >= maxInvocationsBeforeThrottle) { + // Instead of waiting a fixed 60 seconds to settle the throttle, + // calculate a wait time that will clear out about half of the + // current invocations (assuming even distribution) from the + // next 60 second period. + val oldestInvocationInLast60Seconds = invocationsLast60Seconds.head + + // Take the oldest invocation time in this 60 second period. To clear + // this invocation from the next 60 second period, the wait time will be + // (60sec - oldest invocation's delta time away from the period end). + // This will clear all of the invocations from the next period at the + // expense of potentially waiting uncessarily long. Instead, this calculation + // halves the delta time as a compromise. + val throttleTime = 60.seconds.toMillis - ((t.toEpochMilli - oldestInvocationInLast60Seconds.toEpochMilli) / 2) + println(s"Waiting ${throttleTime} milliseconds to settle the throttle") + Thread.sleep(throttleTime) + } + + invocationTimes += Instant.now + } + + override def beforeEach() = { + //checkThrottle() + } + + /* + * Create a CLI properties file for use by the tests + */ + override def beforeAll() = { + cliWskPropsFile.deleteOnExit() + val wskprops = WskProps(token = "SOME TOKEN") + wskprops.writeFile(cliWskPropsFile) + println(s"wsk temporary props file created here: ${cliWskPropsFile.getCanonicalPath()}") + } + + /* + * Forcibly clear the throttle so that downstream tests are not affected by + * this test suite + */ + override def afterAll() = { + // Check and settle the throttle so that this test won't cause issues with and follow on tests + checkThrottle(30) + } + + def apiCreate(basepath: Option[String] = None, + relpath: Option[String] = None, + operation: Option[String] = None, + action: Option[String] = None, + apiname: Option[String] = None, + swagger: Option[String] = None, + responsetype: Option[String] = None, + expectedExitCode: Int = SUCCESS_EXIT, + cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath()))( + implicit wskpropsOverride: WskProps): RunResult = { + + println("parmssss is fdasdadasdddddddddsfsd") + checkThrottle() + println("create is fdasdadasdddddddddsfsd") + wsk.api.create(basepath, relpath, operation, action, apiname, swagger, responsetype, expectedExitCode, cliCfgFile)( + wskpropsOverride) + } + + def apiList(basepathOrApiName: Option[String] = None, + relpath: Option[String] = None, + operation: Option[String] = None, + limit: Option[Int] = None, + since: Option[Instant] = None, + full: Option[Boolean] = None, + nameSort: Option[Boolean] = None, + expectedExitCode: Int = SUCCESS_EXIT, + cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath())): RunResult = { + + checkThrottle() + wsk.api.list(basepathOrApiName, relpath, operation, limit, since, full, nameSort, expectedExitCode, cliCfgFile) + } + + def apiGet(basepathOrApiName: Option[String] = None, + full: Option[Boolean] = None, + expectedExitCode: Int = SUCCESS_EXIT, + cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath()), + format: Option[String] = None): RunResult = { + + checkThrottle() + wsk.api.get(basepathOrApiName, full, expectedExitCode, cliCfgFile, format) + } + + def apiDelete(basepathOrApiName: String, + relpath: Option[String] = None, + operation: Option[String] = None, + expectedExitCode: Int = SUCCESS_EXIT, + cliCfgFile: Option[String] = Some(cliWskPropsFile.getCanonicalPath())): RunResult = { + + checkThrottle() + wsk.api.delete(basepathOrApiName, relpath, operation, expectedExitCode, cliCfgFile) + } +} diff --git a/tests/src/test/scala/whisk/core/cli/test/WskCliApiGwTests.scala b/tests/src/test/scala/whisk/core/cli/test/WskCliApiGwTests.scala new file mode 100644 index 0000000000..088b6c5a06 --- /dev/null +++ b/tests/src/test/scala/whisk/core/cli/test/WskCliApiGwTests.scala @@ -0,0 +1,438 @@ +/* + * 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 whisk.core.cli.test + +import org.junit.runner.RunWith +import org.scalatest.junit.JUnitRunner + +import common.JsHelpers +import common.StreamLogging +import common.TestUtils +import common.TestUtils.ANY_ERROR_EXIT +import common.TestUtils.DONTCARE_EXIT +import common.TestUtils.SUCCESS_EXIT +import common.Wsk +import common.WskActorSystem +import common.WskAdmin +import common.WskProps + +/** + * Tests for basic CLI usage. Some of these tests require a deployed backend. + */ +@RunWith(classOf[JUnitRunner]) +class WskCliApiGwTests extends BaseApiGwTests with WskActorSystem with JsHelpers with StreamLogging { + + val systemId: String = "whisk.system" + override implicit val wskprops = WskProps(authKey = WskAdmin.listKeys(systemId)(0)._1, namespace = systemId) + val wsk: common.Wsk = new Wsk + + it should "reject apimgmt actions that are invoked with not enough parameters" in { + val invalidArgs = Seq( + //getApi + ("/whisk.system/apimgmt/getApi", ANY_ERROR_EXIT, "Invalid authentication.", Seq()), + //deleteApi + ( + "/whisk.system/apimgmt/deleteApi", + ANY_ERROR_EXIT, + "Invalid authentication.", + Seq("-p", "basepath", "/ApiGwRoutemgmtActionTests_bp")), + ( + "/whisk.system/apimgmt/deleteApi", + ANY_ERROR_EXIT, + "basepath is required", + Seq("-p", "__ow_user", "_", "-p", "accesstoken", "TOKEN")), + ( + "/whisk.system/apimgmt/deleteApi", + ANY_ERROR_EXIT, + "When specifying an operation, the path is required", + Seq( + "-p", + "__ow_user", + "_", + "-p", + "accesstoken", + "TOKEN", + "-p", + "basepath", + "/ApiGwRoutemgmtActionTests_bp", + "-p", + "operation", + "get")), + //createApi + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "apidoc is required", + Seq("-p", "__ow_user", "_", "-p", "accesstoken", "TOKEN")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "apidoc is missing the namespace field", + Seq("-p", "__ow_user", "_", "-p", "accesstoken", "TOKEN", "-p", "apidoc", "{}")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "apidoc is missing the gatewayBasePath field", + Seq("-p", "__ow_user", "_", "-p", "accesstoken", "TOKEN", "-p", "apidoc", """{"namespace":"_"}""")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "apidoc is missing the gatewayPath field", + Seq( + "-p", + "__ow_user", + "_", + "-p", + "accesstoken", + "TOKEN", + "-p", + "apidoc", + """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp"}""")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "apidoc is missing the gatewayMethod field", + Seq( + "-p", + "__ow_user", + "_", + "-p", + "accesstoken", + "TOKEN", + "-p", + "apidoc", + """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp"}""")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "apidoc is missing the action field", + Seq( + "-p", + "__ow_user", + "_", + "-p", + "accesstoken", + "TOKEN", + "-p", + "apidoc", + """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get"}""")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "action is missing the backendMethod field", + Seq( + "-p", + "__ow_user", + "_", + "-p", + "accesstoken", + "TOKEN", + "-p", + "apidoc", + """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{}}""")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "action is missing the backendUrl field", + Seq( + "-p", + "__ow_user", + "_", + "-p", + "accesstoken", + "TOKEN", + "-p", + "apidoc", + """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post"}}""")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "action is missing the namespace field", + Seq( + "-p", + "__ow_user", + "_", + "-p", + "accesstoken", + "TOKEN", + "-p", + "apidoc", + """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL"}}""")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "action is missing the name field", + Seq( + "-p", + "__ow_user", + "_", + "-p", + "accesstoken", + "TOKEN", + "-p", + "apidoc", + """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_"}}""")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "action is missing the authkey field", + Seq( + "-p", + "__ow_user", + "_", + "-p", + "accesstoken", + "TOKEN", + "-p", + "apidoc", + """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_","name":"N"}}""")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "swagger and gatewayBasePath are mutually exclusive and cannot be specified together", + Seq( + "-p", + "__ow_user", + "_", + "-p", + "accesstoken", + "TOKEN", + "-p", + "apidoc", + """{"namespace":"_","gatewayBasePath":"/ApiGwRoutemgmtActionTests_bp","gatewayPath":"ApiGwRoutemgmtActionTests_rp","gatewayMethod":"get","action":{"backendMethod":"post","backendUrl":"URL","namespace":"_","name":"N","authkey":"XXXX"},"swagger":{}}""")), + ( + "/whisk.system/apimgmt/createApi", + ANY_ERROR_EXIT, + "apidoc field cannot be parsed. Ensure it is valid JSON", + Seq("-p", "__ow_user", "_", "-p", "accesstoken", "TOKEN", "-p", "apidoc", "{1:[}}}"))) + + invalidArgs foreach { + case (action: String, exitcode: Int, errmsg: String, params: Seq[String]) => + val cmd: Seq[String] = Seq( + "action", + "invoke", + action, + "-i", + "-b", + "-r", + "--apihost", + wskprops.apihost, + "--auth", + wskprops.authKey) ++ params + val rr = wsk.cli(cmd, expectedExitCode = exitcode) + rr.stderr should include regex (errmsg) + } + } + + it should "reject an API created with a non-existent action" in { + val testName = "CLI_APIGWTEST15" + val testbasepath = "/" + testName + "_bp" + val testrelpath = "/path" + val testnewrelpath = "/path_new" + val testurlop = "get" + val testapiname = testName + " API Name" + val actionName = testName + "_action" + try { + val rr = apiCreate( + basepath = Some(testbasepath), + relpath = Some(testrelpath), + operation = Some(testurlop), + action = Some(actionName), + apiname = Some(testapiname), + expectedExitCode = ANY_ERROR_EXIT) + rr.stderr should include("does not exist") + } finally { + apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) + } + } + + it should "reject an API created with an action that is not a web action" in { + val testName = "CLI_APIGWTEST16" + val testbasepath = "/" + testName + "_bp" + val testrelpath = "/path" + val testnewrelpath = "/path_new" + val testurlop = "get" + val testapiname = testName + " API Name" + val actionName = testName + "_action" + try { + // Create the action for the API. It must NOT be a "web-action" action for this test + val file = TestUtils.getTestActionFilename(s"echo.js") + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT) + + val rr = apiCreate( + basepath = Some(testbasepath), + relpath = Some(testrelpath), + operation = Some(testurlop), + action = Some(actionName), + apiname = Some(testapiname), + expectedExitCode = ANY_ERROR_EXIT) + rr.stderr should include("is not a web action") + } finally { + wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) + apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) + } + } + + it should "reject API export when export type is invalid" in { + val testName = "CLI_APIGWTEST18" + val testbasepath = "/" + testName + "_bp" + + val rr = apiGet(basepathOrApiName = Some(testbasepath), format = Some("BadType"), expectedExitCode = ANY_ERROR_EXIT) + rr.stderr should include("Invalid format type") + } + + it should "list api alphabetically by Base/Rel/Verb" in { + val baseName = "/BaseTestPathApiList" + val actionName = "actionName" + val file = TestUtils.getTestActionFilename(s"echo-web-http.js") + try { + // Create Action for apis + var action = + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + println("action creation: " + action.stdout) + // Create apis + for (i <- 1 to 3) { + val base = s"$baseName$i" + var api = apiCreate( + basepath = Some(base), + relpath = Some("/relPath"), + operation = Some("GET"), + action = Some(actionName)) + println("api creation: " + api.stdout) + } + val original = apiList(nameSort = Some(true)) + val originalFull = apiList(full = Some(true), nameSort = Some(true)) + val scalaSorted = List(s"${baseName}1" + "/", s"${baseName}2" + "/", s"${baseName}3" + "/") + + val regex = s"${baseName}[1-3]/".r + val list = (regex.findAllMatchIn(original.stdout)).toList + val listFull = (regex.findAllMatchIn(originalFull.stdout)).toList + + scalaSorted.toString shouldEqual list.toString + scalaSorted.toString shouldEqual listFull.toString + + } finally { + // Clean up Apis + for (i <- 1 to 3) { + apiDelete(basepathOrApiName = s"${baseName}$i", expectedExitCode = DONTCARE_EXIT) + } + wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) + } + } + + it should "successfully export an API in YAML format" in { + val testName = "CLI_APIGWTEST19" + val testbasepath = "/" + testName + "_bp" + val testrelpath = "/path" + val testnewrelpath = "/path_new" + val testurlop = "get" + val testapiname = testName + " API Name" + val actionName = testName + "_action" + val responseType = "http" + try { + // Create the action for the API. It must be a "web-action" action. + val file = TestUtils.getTestActionFilename(s"echo.js") + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + + var rr = apiCreate( + basepath = Some(testbasepath), + relpath = Some(testrelpath), + operation = Some(testurlop), + action = Some(actionName), + apiname = Some(testapiname), + responsetype = Some(responseType)) + rr.stdout should include("ok: created API") + + rr = apiGet(basepathOrApiName = Some(testapiname), format = Some("yaml")) + rr.stdout should include(s"basePath: ${testbasepath}") + } finally { + wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) + apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) + } + } + + it should "successfully export an API when JSON format is explcitly specified" in { + val testName = "CLI_APIGWTEST20" + val testbasepath = "/" + testName + "_bp" + val testrelpath = "/path" + val testnewrelpath = "/path_new" + val testurlop = "get" + val testapiname = testName + " API Name" + val actionName = testName + "_action" + val responseType = "http" + try { + // Create the action for the API. It must be a "web-action" action. + val file = TestUtils.getTestActionFilename(s"echo.js") + wsk.action.create(name = actionName, artifact = Some(file), expectedExitCode = SUCCESS_EXIT, web = Some("true")) + + var rr = apiCreate( + basepath = Some(testbasepath), + relpath = Some(testrelpath), + operation = Some(testurlop), + action = Some(actionName), + apiname = Some(testapiname), + responsetype = Some(responseType)) + rr.stdout should include("ok: created API") + + rr = apiGet(basepathOrApiName = Some(testapiname), format = Some("json")) + rr.stdout should include(testbasepath) + rr.stdout should include(s"${actionName}") + rr.stdout should include regex (""""cors":\s*\{\s*\n\s*"enabled":\s*true""") + rr.stdout should include regex (s""""target-url":\\s+.*${actionName}.${responseType}""") + } finally { + wsk.action.delete(name = actionName, expectedExitCode = DONTCARE_EXIT) + apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) + } + } + + it should "successfully create an API from a YAML formatted API configuration file" in { + val testName = "CLI_APIGWTEST21" + val testbasepath = "/bp" + val testrelpath = "/rp" + val testurlop = "get" + val testapiname = testbasepath + val actionName = "webhttpecho" + val swaggerPath = TestUtils.getTestApiGwFilename(s"local.api.yaml") + try { + var rr = apiCreate(swagger = Some(swaggerPath)) + println("api create stdout: " + rr.stdout) + println("api create stderror: " + rr.stderr) + rr.stdout should include("ok: created API") + rr = apiList(basepathOrApiName = Some(testbasepath)) + rr.stdout should include("ok: APIs") + rr.stdout should include regex (s"/[@\\w._\\-]+/${actionName}\\s+${testurlop}\\s+${testapiname}\\s+") + rr.stdout should include(testbasepath + testrelpath) + } finally { + apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) + } + } + + it should "reject creation of an API from invalid YAML formatted API configuration file" in { + val testName = "CLI_APIGWTEST22" + val testbasepath = "/" + testName + "_bp" + val swaggerPath = TestUtils.getTestApiGwFilename(s"local.api.bad.yaml") + try { + val rr = apiCreate(swagger = Some(swaggerPath), expectedExitCode = ANY_ERROR_EXIT) + println("api create stdout: " + rr.stdout) + println("api create stderror: " + rr.stderr) + rr.stderr should include("Unable to parse YAML configuration file") + } finally { + apiDelete(basepathOrApiName = testbasepath, expectedExitCode = DONTCARE_EXIT) + } + } +} ---------------------------------------------------------------- This is an automated message from the Apache Git Service. To respond to the message, please log on GitHub and use the URL above to go to the specific comment. For queries about this service, please contact Infrastructure at: [email protected] With regards, Apache Git Services
