Remove meta packages, normalize on web actions.
Adds (whisk) authenticated web actions.
Removes result projection for http actions.
diff --git a/tests/src/common/Wsk.scala b/tests/src/common/Wsk.scala
index d99ecf9..df890ef 100644
--- a/tests/src/common/Wsk.scala
+++ b/tests/src/common/Wsk.scala
@@ -746,11 +746,11 @@
protected val noun = "api-experimental"
/**
- * Creates and API endpoint. Parameters mirror those available in the CLI.
- *
- * @param expectedExitCode (optional) the expected exit code for the command
- * if the code is anything but DONTCARE_EXIT, assert the code is as expected
- */
+ * Creates and API endpoint. Parameters mirror those available in the CLI.
+ *
+ * @param expectedExitCode (optional) the expected exit code for the command
+ * if the code is anything but DONTCARE_EXIT, assert the code is as expected
+ */
def create(
basepath: Option[String] = None,
relpath: Option[String] = None,
@@ -761,21 +761,21 @@
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "create", "--auth", wp.authKey) ++
- { basepath map { b => Seq(b) } getOrElse Seq() } ++
- { relpath map { r => Seq(r) } getOrElse Seq() } ++
- { operation map { o => Seq(o) } getOrElse Seq() } ++
- { action map { aa => Seq(aa) } getOrElse Seq() } ++
- { apiname map { a => Seq("--apiname", a) } getOrElse Seq() } ++
- { swagger map { s => Seq("--config-file", s) } getOrElse Seq() }
+ { basepath map { b => Seq(b) } getOrElse Seq() } ++
+ { relpath map { r => Seq(r) } getOrElse Seq() } ++
+ { operation map { o => Seq(o) } getOrElse Seq() } ++
+ { action map { aa => Seq(aa) } getOrElse Seq() } ++
+ { apiname map { a => Seq("--apiname", a) } getOrElse Seq() } ++
+ { swagger map { s => Seq("--config-file", s) } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode, showCmd = true)
}
/**
- * Retrieve a list of API endpoints. Parameters mirror those available in the CLI.
- *
- * @param expectedExitCode (optional) the expected exit code for the command
- * if the code is anything but DONTCARE_EXIT, assert the code is as expected
- */
+ * Retrieve a list of API endpoints. Parameters mirror those available in the CLI.
+ *
+ * @param expectedExitCode (optional) the expected exit code for the command
+ * if the code is anything but DONTCARE_EXIT, assert the code is as expected
+ */
def list(
basepathOrApiName: Option[String] = None,
relpath: Option[String] = None,
@@ -786,39 +786,39 @@
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "list", "--auth", wp.authKey) ++
- { basepathOrApiName map { b => Seq(b) } getOrElse Seq() } ++
- { relpath map { r => Seq(r) } getOrElse Seq() } ++
- { operation map { o => Seq(o) } getOrElse Seq() } ++
- { limit map { l => Seq("--limit", l.toString) } getOrElse Seq() } ++
- { since map { i => Seq("--since", i.toEpochMilli.toString) } getOrElse Seq() } ++
- { full map { r => Seq("--full") } getOrElse Seq() }
+ { basepathOrApiName map { b => Seq(b) } getOrElse Seq() } ++
+ { relpath map { r => Seq(r) } getOrElse Seq() } ++
+ { operation map { o => Seq(o) } getOrElse Seq() } ++
+ { limit map { l => Seq("--limit", l.toString) } getOrElse Seq() } ++
+ { since map { i => Seq("--since", i.toEpochMilli.toString) } getOrElse Seq() } ++
+ { full map { r => Seq("--full") } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode, showCmd = true)
}
/**
- * Retieves an API's configuration. Parameters mirror those available in the CLI.
- * Runs a command wsk [params] where the arguments come in as a sequence.
- *
- * @param expectedExitCode (optional) the expected exit code for the command
- * if the code is anything but DONTCARE_EXIT, assert the code is as expected
- */
+ * Retieves an API's configuration. Parameters mirror those available in the CLI.
+ * Runs a command wsk [params] where the arguments come in as a sequence.
+ *
+ * @param expectedExitCode (optional) the expected exit code for the command
+ * if the code is anything but DONTCARE_EXIT, assert the code is as expected
+ */
def get(
basepathOrApiName: Option[String] = None,
full: Option[Boolean] = None,
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "get", "--auth", wp.authKey) ++
- { basepathOrApiName map { b => Seq(b) } getOrElse Seq() } ++
- { full map { f => if (f) Seq("--full") else Seq() } getOrElse Seq() }
+ { basepathOrApiName map { b => Seq(b) } getOrElse Seq() } ++
+ { full map { f => if (f) Seq("--full") else Seq() } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode, showCmd = true)
}
/**
- * Delete an entire API or a subset of API endpoints. Parameters mirror those available in the CLI.
- *
- * @param expectedExitCode (optional) the expected exit code for the command
- * if the code is anything but DONTCARE_EXIT, assert the code is as expected
- */
+ * Delete an entire API or a subset of API endpoints. Parameters mirror those available in the CLI.
+ *
+ * @param expectedExitCode (optional) the expected exit code for the command
+ * if the code is anything but DONTCARE_EXIT, assert the code is as expected
+ */
def delete(
basepathOrApiName: String,
relpath: Option[String] = None,
@@ -826,8 +826,8 @@
expectedExitCode: Int = SUCCESS_EXIT)(
implicit wp: WskProps): RunResult = {
val params = Seq(noun, "delete", "--auth", wp.authKey, basepathOrApiName) ++
- { relpath map { r => Seq(r) } getOrElse Seq() } ++
- { operation map { o => Seq(o) } getOrElse Seq() }
+ { relpath map { r => Seq(r) } getOrElse Seq() } ++
+ { operation map { o => Seq(o) } getOrElse Seq() }
cli(wp.overrides ++ params, expectedExitCode, showCmd = true)
}
}
@@ -946,8 +946,18 @@
Buffer(WhiskProperties.python, new File(binDir, binaryName).toString)
}
+ def listKeys(namespace: String, pick: Integer = 1): List[(String, String)] = {
+ val wskadmin = new RunWskAdminCmd {}
+ wskadmin.cli(Seq("user", "list", namespace, "--pick", pick.toString))
+ .stdout
+ .split("\n")
+ .map("""\s+""".r.split(_))
+ .map(parts => (parts(0), parts(1)))
+ .toList
+ }
+
/**
- * returns (subject, namespace) pair given the auth key
+ * @returns (subject, namespace) pair given the auth key
*/
def getUser(authKey: String): (String, String) = {
val wskadmin = new RunWskAdminCmd {}
diff --git a/tests/src/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala b/tests/src/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala
index 4b5dab8..d289d7a 100644
--- a/tests/src/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala
+++ b/tests/src/whisk/core/apigw/actions/test/ApiGwRoutemgmtActionTests.scala
@@ -34,7 +34,6 @@
import common.WskTestHelpers
import spray.json._
import spray.json.DefaultJsonProtocol._
-import whisk.core.WhiskConfig
case class ApiAction(
name: String,
@@ -64,13 +63,9 @@
with JsHelpers
with StreamLogging {
- implicit val wskprops = WskProps()
+ val systemId = "whisk.system"
+ implicit val wskprops = WskProps(authKey = WskAdmin.listKeys(systemId)(0)._1, namespace = systemId)
val wsk = new Wsk
- val (cliuser, clinamespace) = WskAdmin.getUser(wskprops.authKey)
-
- // Use the whisk.system authentication id and pwd for invoking whisk.system private actions
- val config = new WhiskConfig(Map(WhiskConfig.systemKey -> null))
- val systemKey = config.systemKey
def getApis(
bpOrName: Option[String],
@@ -78,12 +73,12 @@
operation: Option[String] = None,
docid: Option[String] = None): Vector[JsValue] = {
val parms = Map[String, JsValue]() ++
- Map("__ow_meta_namespace" -> clinamespace.toJson) ++
+ Map("__ow_meta_namespace" -> wskprops.namespace.toJson) ++
{ bpOrName map { b => Map("basepath" -> b.toJson) } getOrElse Map[String, JsValue]() } ++
{ relpath map { r => Map("relpath" -> r.toJson) } getOrElse Map[String, JsValue]() } ++
{ operation map { o => Map("operation" -> o.toJson) } getOrElse Map[String, JsValue]() } ++
{ docid map { d => Map("docid" -> d.toJson) } getOrElse Map[String, JsValue]() }
- val wskprops = new WskProps(authKey = systemKey)
+
val rr = wsk.action.invoke(
name = "routemgmt/getApi",
parameters = parms,
@@ -120,7 +115,7 @@
{ swagger map { s => Map("swagger" -> s.toJson) } getOrElse Map[String, JsValue]() }
val parm = Map[String, JsValue]("apidoc" -> JsObject(parms)) ++
{ namespace map { n => Map("__ow_meta_namespace" -> n.toJson) } getOrElse Map[String, JsValue]() }
- val wskprops = new WskProps(authKey = systemKey)
+
val rr = wsk.action.invoke(
name = "routemgmt/createApi",
parameters = parm,
@@ -143,7 +138,7 @@
{ relpath map { r => Map("relpath" -> r.toJson) } getOrElse Map[String, JsValue]() } ++
{ operation map { o => Map("operation" -> o.toJson) } getOrElse Map[String, JsValue]() } ++
{ apiname map { an => Map("apiname" -> an.toJson) } getOrElse Map[String, JsValue]() }
- val wskprops = new WskProps(authKey = systemKey)
+
val rr = wsk.action.invoke(
name = "routemgmt/deleteApi",
parameters = parms,
@@ -211,20 +206,20 @@
val testurlop = "get"
val testapiname = testName + " API Name"
val actionName = testName + "_action"
- val actionNamespace = clinamespace
+ val actionNamespace = wskprops.namespace
val actionUrl = "http://some.whisk.host/api/v1/namespaces/" + actionNamespace + "/actions/" + actionName
val actionAuthKey = testName + "_authkey"
val testaction = ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)
try {
- val createResult = createApi(namespace = Some(clinamespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
+ val createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
operation = Some(testurlop), apiname = Some(testapiname), action = Some(testaction))
JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists("apidoc") should be(true)
val apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
apiVector.size should be > 0
apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)
} finally {
- val deleteResult = deleteApi(namespace = Some(clinamespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
+ val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
}
}
@@ -235,23 +230,23 @@
val testurlop = "get"
val testapiname = testName + " API Name"
val actionName = testName + "_action"
- val actionNamespace = clinamespace
+ val actionNamespace = wskprops.namespace
val actionUrl = "http://some.whisk.host/api/v1/namespaces/" + actionNamespace + "/actions/" + actionName
val actionAuthKey = testName + "_authkey"
val testaction = ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)
try {
- val createResult = createApi(namespace = Some(clinamespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
+ val createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
operation = Some(testurlop), apiname = Some(testapiname), action = Some(testaction))
JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists("apidoc") should be(true)
var apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
apiVector.size should be > 0
apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)
- val deleteResult = deleteApi(namespace = Some(clinamespace), basepath = Some(testbasepath))
+ val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath))
apiVector = getApis(bpOrName = Some(testbasepath), relpath = Some(testrelpath), operation = Some(testurlop))
apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(false)
} finally {
- val deleteResult = deleteApi(namespace = Some(clinamespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
+ val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
}
}
@@ -264,15 +259,15 @@
val testnewurlop = "delete"
val testapiname = testName + " API Name"
val actionName = testName + "_action"
- val actionNamespace = clinamespace
+ val actionNamespace = wskprops.namespace
val actionUrl = "http://some.whisk.host/api/v1/namespaces/" + actionNamespace + "/actions/" + actionName
val actionAuthKey = testName + "_authkey"
val testaction = ApiAction(name = actionName, namespace = actionNamespace, backendUrl = actionUrl, authkey = actionAuthKey)
try {
- var createResult = createApi(namespace = Some(clinamespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
+ var createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testrelpath),
operation = Some(testurlop), apiname = Some(testapiname), action = Some(testaction))
- createResult = createApi(namespace = Some(clinamespace), basepath = Some(testbasepath), relpath = Some(testnewrelpath),
+ createResult = createApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), relpath = Some(testnewrelpath),
operation = Some(testnewurlop), apiname = Some(testapiname), action = Some(testaction))
JsObjectHelper(createResult.stdout.parseJson.asJsObject).fieldPathExists("apidoc") should be(true)
var apiVector = getApis(bpOrName = Some(testbasepath))
@@ -280,7 +275,7 @@
apiMatch(apiVector, testbasepath, testrelpath, testurlop, testapiname, testaction) should be(true)
apiMatch(apiVector, testbasepath, testnewrelpath, testnewurlop, testapiname, testaction) should be(true)
} finally {
- val deleteResult = deleteApi(namespace = Some(clinamespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
+ val deleteResult = deleteApi(namespace = Some(wskprops.namespace), basepath = Some(testbasepath), expectedExitCode = DONTCARE_EXIT)
}
}
@@ -324,12 +319,12 @@
invalidArgs foreach {
case (action: String, exitcode: Int, errmsg: String, params: Seq[String]) =>
- var cmd: Seq[String] = Seq("action",
+ val cmd: Seq[String] = Seq("action",
"invoke",
action,
"-i", "-b", "-r",
"--apihost", wskprops.apihost,
- "--auth", systemKey) ++ params
+ "--auth", wskprops.authKey) ++ params
val rr = wsk.cli(cmd, expectedExitCode = exitcode)
rr.stderr should include regex (errmsg)
}
diff --git a/tests/src/whisk/core/cli/test/WskWebActionsTests.scala b/tests/src/whisk/core/cli/test/WskWebActionsTests.scala
index 83cd073..bdb75f7 100644
--- a/tests/src/whisk/core/cli/test/WskWebActionsTests.scala
+++ b/tests/src/whisk/core/cli/test/WskWebActionsTests.scala
@@ -32,7 +32,7 @@
import system.rest.RestUtil
/**
- * Tests for basic CLI usage. Some of these tests require a deployed backend.
+ * Tests web actions.
*/
@RunWith(classOf[JUnitRunner])
class WskWebActionsTests
@@ -48,6 +48,9 @@
behavior of "Wsk Web Actions"
+ /**
+ * Tests web actions, plus max url limit.
+ */
it should "create a web action accessible via HTTPS" in withAssetCleaner(wskprops) {
(wp, assetHelper) =>
val name = "webaction"
@@ -83,4 +86,32 @@
}
}
}
+
+ /**
+ * Tests web action requiring authentication.
+ */
+ it should "create a web action requiring authentication accessible via HTTPS" in withAssetCleaner(wskprops) {
+ (wp, assetHelper) =>
+ val name = "webaction"
+ val file = Some(TestUtils.getTestActionFilename("echo.js"))
+
+ assetHelper.withCleaner(wsk.action, name) {
+ (action, _) =>
+ action.create(name, file, annotations = Map("web-export" -> true.toJson, "require-whisk-auth" -> true.toJson))
+ }
+
+ val host = getServiceURL()
+ val url = host + s"/api/v1/experimental/web/$namespace/default/webaction.text/__ow_meta_namespace"
+
+ val unauthorizedResponse = RestAssured.given().config(sslconfig).get(url)
+ unauthorizedResponse.statusCode shouldBe 401
+
+ val authorizedResponse = RestAssured
+ .given()
+ .config(sslconfig)
+ .auth().preemptive().basic(wskprops.authKey.split(":")(0), wskprops.authKey.split(":")(1))
+ .get(url)
+ authorizedResponse.statusCode shouldBe 200
+ authorizedResponse.body().asString() shouldBe namespace
+ }
}
diff --git a/tests/src/whisk/core/controller/test/MetaApiTests.scala b/tests/src/whisk/core/controller/test/MetaApiTests.scala
index 7952cb5..38b294c 100644
--- a/tests/src/whisk/core/controller/test/MetaApiTests.scala
+++ b/tests/src/whisk/core/controller/test/MetaApiTests.scala
@@ -19,7 +19,6 @@
import java.time.Instant
import java.util.Base64
-import scala.concurrent.Await
import scala.concurrent.Future
import scala.concurrent.duration.FiniteDuration
@@ -73,68 +72,33 @@
override val apiversion = "v1"
val systemId = Subject()
- override lazy val systemKey = AuthKey()
- override lazy val systemIdentity = Future.successful(Identity(systemId, EntityName(systemId.asString), systemKey, Privilege.ALL))
+ val systemKey = AuthKey()
+ val systemIdentity = Future.successful(Identity(systemId, EntityName(systemId.asString), systemKey, Privilege.ALL))
override lazy val entitlementProvider = new TestingEntitlementProvider(whiskConfig, loadBalancer, iam)
/** Meta API tests */
behavior of "Meta API"
- val creds = WhiskAuth(Subject(), AuthKey()).toIdentity
- val namespace = EntityPath(creds.subject.asString)
-
var failActionLookup = false // toggle to cause action lookup to fail
var failActivation = 0 // toggle to cause action to fail
var failThrottleForSubject: Option[Subject] = None // toggle to cause throttle to fail for subject
var actionResult: Option[JsObject] = None
+ var requireAuthentication = false // toggle require-whisk-auth annotation on action
override def afterEach() = {
failActionLookup = false
failActivation = 0
failThrottleForSubject = None
actionResult = None
+ requireAuthentication = false
}
+ val allowedMethods = Seq(Get, Post, Put, Delete)
+
+ // there is only one package that is predefined 'proxy'
val packages = Seq(
WhiskPackage(
EntityPath(systemId.asString),
- EntityName("notmeta"),
- annotations = Parameters("meta", JsBoolean(false))),
- WhiskPackage(
- EntityPath(systemId.asString),
- EntityName("badmeta"),
- annotations = Parameters("meta", JsBoolean(true))),
- WhiskPackage(
- EntityPath(systemId.asString),
- EntityName("heavymeta"),
- annotations = Parameters("meta", JsBoolean(true)) ++
- Parameters("get", JsString("getApi")) ++
- Parameters("post", JsString("createRoute")) ++
- Parameters("delete", JsString("deleteApi"))),
- WhiskPackage(
- EntityPath(systemId.asString),
- EntityName("partialmeta"),
- annotations = Parameters("meta", JsBoolean(true)) ++
- Parameters("get", JsString("getApi"))),
- WhiskPackage(
- EntityPath(systemId.asString),
- EntityName("packagemeta"),
- parameters = Parameters("x", JsString("X")) ++ Parameters("z", JsString("z")),
- annotations = Parameters("meta", JsBoolean(true)) ++
- Parameters("get", JsString("getApi"))),
- WhiskPackage(
- EntityPath(systemId.asString),
- EntityName("publicmeta"),
- publish = true,
- annotations = Parameters("meta", JsBoolean(true)) ++
- Parameters("get", JsString("getApi"))),
- WhiskPackage(
- EntityPath(systemId.asString),
- EntityName("bindingmeta"),
- Some(Binding(EntityName(systemId.asString), EntityName("heavymeta"))),
- annotations = Parameters("meta", JsBoolean(true))),
- WhiskPackage(
- EntityPath(systemId.asString),
EntityName("proxy"),
parameters = Parameters("x", JsString("X")) ++ Parameters("z", JsString("z"))))
@@ -148,9 +112,10 @@
}
val defaultActionParameters = {
- Parameters("y", JsString("Y")) ++ Parameters("z", JsString("Z"))
+ Parameters("y", JsString("Y")) ++ Parameters("z", JsString("Z")) ++ Parameters("empty", JsNull)
}
+ // action names that start with 'export_' will automatically have an web-export annotation added by the test harness
override protected def getAction(actionName: FullyQualifiedEntityName)(
implicit transid: TransactionId) = {
if (!failActionLookup) {
@@ -159,7 +124,12 @@
WhiskAction(actionName.path, actionName.name, Exec.js("??"), defaultActionParameters, annotations = {
if (actionName.name.asString.startsWith("export_")) {
- annotations ++ Parameters("web-export", JsBoolean(true))
+ annotations ++
+ Parameters("web-export", JsBoolean(true)) ++ {
+ if (requireAuthentication) {
+ Parameters("require-whisk-auth", JsBoolean(true))
+ } else Parameters()
+ }
} else annotations
})
}
@@ -174,6 +144,7 @@
}
}
+ // there is only one identity defined for the fully qualified name of the web action: 'systemId'
override protected def getIdentity(namespace: EntityName)(
implicit transid: TransactionId): Future[Identity] = {
if (namespace.asString == systemId.asString) {
@@ -217,11 +188,14 @@
// check that action parameters were merged with package
// all actions have default parameters (see actionLookup stub)
- val packageName = Await.result(resolvePackageName(action.namespace.last), dbOpTimeout)
- getPackage(packageName) foreach { pkg =>
- action.parameters shouldBe (pkg.parameters ++ defaultActionParameters)
- action.parameters.get("z") shouldBe defaultActionParameters.get("z")
+ if (!action.namespace.defaultPackage) {
+ getPackage(action.namespace.toFullyQualifiedEntityName) foreach { pkg =>
+ action.parameters shouldBe (pkg.parameters ++ defaultActionParameters)
+ }
+ } else {
+ action.parameters shouldBe defaultActionParameters
}
+ action.parameters.get("z") shouldBe defaultActionParameters.get("z")
Future.successful(activation.activationId, Some(activation))
} else if (failActivation == 1) {
@@ -231,17 +205,19 @@
}
}
- def metaPayload(method: String, params: Map[String, String], identity: Identity, path: String = "", body: Option[JsObject] = None, pkgName: String = null) = {
- (Option(pkgName).filter(_ != null).flatMap(n => packages.find(_.name == EntityName(n)))
+ def metaPayload(method: String, params: Map[String, String], identity: Option[Identity], path: String = "", body: Option[JsObject] = None, pkgName: String = null) = {
+ val packageActionParams = Option(pkgName).filter(_ != null).flatMap(n => packages.find(_.name == EntityName(n)))
.map(_.parameters)
- .getOrElse(Parameters()) ++ defaultActionParameters).merge {
- Some {
- JsObject(
- params.toJson.asJsObject.fields ++
- body.map(_.fields).getOrElse(Map()) ++
- Context(HttpMethods.getForKey(method.toUpperCase).get, List(), path, Map()).metadata(Option(identity)))
- }
- }.get
+ .getOrElse(Parameters())
+
+ (packageActionParams ++ defaultActionParameters).merge {
+ Some {
+ JsObject(
+ params.toJson.asJsObject.fields ++
+ body.map(_.fields).getOrElse(Map()) ++
+ Context(HttpMethods.getForKey(method.toUpperCase).get, List(), path, Map()).metadata(identity))
+ }
+ }.get
}
def confirmErrorWithTid(error: JsObject, message: Option[String] = None) = {
@@ -252,590 +228,579 @@
error.fields.get("code").get shouldBe an[JsNumber]
}
- it should "resolve a meta package into the systemId namespace" in {
- val packageName = Await.result(resolvePackageName(EntityName("foo")), dbOpTimeout)
- packageName.fullPath.asString shouldBe s"$systemId/foo"
- }
+ val testRoutePath = s"/$routePath/$webInvokePath"
- it should "reject unsupported http verbs" in {
- implicit val tid = transid()
+ Seq(None, Some(WhiskAuth(Subject(), AuthKey()).toIdentity)).foreach { creds =>
- val methods = Seq((Head, MethodNotAllowed), (Patch, MethodNotAllowed))
- methods.foreach {
- case (m, code) =>
- m(s"/$routePath/partialmeta") ~> sealRoute(routes(creds)) ~> check {
- status should be(code)
+ it should s"split action name and extension (auth? ${creds.isDefined})" in {
+ val r = WhiskMetaApi.extensionSplitter
+ Seq("t.j.http", "t.js.http", "tt.j.http", "tt.js.http").foreach { s =>
+ val r(n, e) = s
+ val i = s.lastIndexOf(".")
+ n shouldBe s.substring(0, i)
+ e shouldBe s.substring(i + 1)
+ }
+
+ Seq("t.js", "t.js.htt", "t.js.httpz").foreach { s =>
+ a[MatchError] should be thrownBy {
+ val r(n, e) = s
+ }
+ }
+
+ }
+
+ it should s"not match invalid routes (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ // none of these should match a route
+ Seq("a", "a/b", "/a", s"$systemId/c", s"$systemId/export_c").
+ foreach { path =>
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(NotFound)
+ }
}
}
- }
- it should "reject access to unknown package or missing package action" in {
- implicit val tid = transid()
- val methods = Seq(Get, Post, Delete)
+ it should s"reject unsupported http verbs (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
- methods.foreach { m =>
- m(s"/$routePath") ~> sealRoute(routes(creds)) ~> check {
- status shouldBe NotFound
- }
+ Seq((Head, MethodNotAllowed), (Patch, MethodNotAllowed)).
+ foreach {
+ case (m, code) =>
+ m(s"$systemId/proxy/export_c.json") ~> sealRoute(routes(creds)) ~> check {
+ status should be(code)
+ }
+ }
}
- val paths = Seq(
- (s"/$routePath/doesntexist", NotFound),
- (s"/$routePath/notmeta", NotFound),
- (s"/$routePath/badmeta", MethodNotAllowed), // exists but has no mapping
- (s"/$routePath/bindingmeta", NotFound))
+ it should s"reject requests when identity, package or action lookup fail or missing annotation (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
- paths.foreach {
- case ((p, s)) =>
- methods.foreach { m =>
- m(p) ~> sealRoute(routes(creds)) ~> check {
- withClue(p) {
- status shouldBe s
+ // the first of these fails in the identity lookup,
+ // the second in the package lookup (does not exist),
+ // the third fails the annotation check (no web-export annotation because name doesn't start with export_c)
+ // the fourth fails the action lookup
+ Seq("guest/proxy/c", s"$systemId/doesnotexist/c", s"$systemId/default/c", s"$systemId/proxy/export_fail").
+ foreach { path =>
+ failActionLookup = path.endsWith("fail")
+
+ Get(s"$testRoutePath/${path}.json") ~> sealRoute(routes(creds)) ~> check {
+ status should be(NotFound)
+ }
+
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(NotAcceptable)
+ confirmErrorWithTid(responseAs[JsObject], Some(Messages.contentTypeNotSupported))
+ }
+ }
+ }
+
+ it should s"reject requests when authentication is required but none given (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_auth").
+ foreach { path =>
+ requireAuthentication = true
+
+ Get(s"$testRoutePath/${path}.json") ~> sealRoute(routes(creds)) ~> check {
+ creds match {
+ case None => status should be(Unauthorized)
+ case Some(user) =>
+ status should be(OK)
+ val response = responseAs[JsObject]
+ response shouldBe JsObject(
+ "pkg" -> s"$systemId/proxy".toJson,
+ "action" -> "export_auth".toJson,
+ "content" -> metaPayload("get", Map(), creds, pkgName = "proxy"))
+ response.fields("content").asJsObject.fields("__ow_meta_namespace") shouldBe user.namespace.toJson
}
}
}
}
- failActionLookup = true
- Get(s"/$routePath/publicmeta") ~> sealRoute(routes(creds)) ~> check {
- status should be(InternalServerError)
+ it should s"invoke action that times out and provide a code (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ failActivation = 1
+ Get(s"$testRoutePath/$systemId/proxy/export_c.json") ~> sealRoute(routes(creds)) ~> check {
+ status should be(Accepted)
+ val response = responseAs[JsObject]
+ confirmErrorWithTid(response, Some("Response not yet ready."))
+ }
}
- }
- it should "invoke action for allowed verbs on meta handler" in {
- implicit val tid = transid()
+ it should s"invoke action that errors and response with error and code (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
- val methods = Seq((Get, "getApi"), (Post, "createRoute"), (Delete, "deleteApi"))
- methods.foreach {
- case (m, name) =>
- m(s"/$routePath/heavymeta?a=b&c=d&namespace=xyz") ~> sealRoute(routes(creds)) ~> check {
- status should be(OK)
- val response = responseAs[JsObject]
- response shouldBe JsObject(
- "pkg" -> s"$systemId/heavymeta".toJson,
- "action" -> name.toJson,
- "content" -> metaPayload(m.method.value, Map("a" -> "b", "c" -> "d", "namespace" -> "xyz"), creds))
+ failActivation = 2
+ Get(s"$testRoutePath/$systemId/proxy/export_c.json") ~> sealRoute(routes(creds)) ~> check {
+ status should be(InternalServerError)
+ val response = responseAs[JsObject]
+ confirmErrorWithTid(response)
+ }
+ }
+
+ it should s"invoke action and merge query parameters (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.json?a=b&c=d")
+ .foreach { path =>
+ allowedMethods.foreach { m =>
+ m(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ val response = responseAs[JsObject]
+ response shouldBe JsObject(
+ "pkg" -> s"$systemId/proxy".toJson,
+ "action" -> "export_c".toJson,
+ "content" -> metaPayload(
+ m.method.name.toLowerCase,
+ Map("a" -> "b", "c" -> "d"),
+ creds,
+ pkgName = "proxy"))
+ }
+ }
}
}
- }
- it should "invoke action for allowed verbs on meta handler with partial mapping" in {
- implicit val tid = transid()
+ it should s"invoke action and merge body parameters (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
- val methods = Seq((Get, OK), (Post, MethodNotAllowed), (Delete, MethodNotAllowed))
- methods.foreach {
- case (m, code) =>
- m(s"/$routePath/partialmeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
- status should be(code)
- if (status == OK) {
+ // both of these should produce full result objects (trailing slash is ok)
+ Seq(s"$systemId/proxy/export_c.json", s"$systemId/proxy/export_c.json/")
+ .foreach { path =>
+ allowedMethods.foreach { m =>
+ val content = JsObject("extra" -> "read all about it".toJson, "yummy" -> true.toJson)
+ val p = if (path.endsWith("/")) "/" else ""
+ m(s"$testRoutePath/$path", content) ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ val response = responseAs[JsObject]
+ response shouldBe JsObject(
+ "pkg" -> s"$systemId/proxy".toJson,
+ "action" -> "export_c".toJson,
+ "content" -> metaPayload(
+ m.method.name.toLowerCase,
+ Map(),
+ creds,
+ body = Some(content),
+ path = p,
+ pkgName = "proxy"))
+ }
+ }
+ }
+ }
+
+ it should s"invoke action and merge query and body parameters (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.json?a=b&c=d")
+ .foreach { path =>
+ allowedMethods.foreach { m =>
+ val content = JsObject("extra" -> "read all about it".toJson, "yummy" -> true.toJson)
+ m(s"$testRoutePath/$path", content) ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ val response = responseAs[JsObject]
+ response shouldBe JsObject(
+ "pkg" -> s"$systemId/proxy".toJson,
+ "action" -> "export_c".toJson,
+ "content" -> metaPayload(
+ m.method.name.toLowerCase,
+ Map("a" -> "b", "c" -> "d"),
+ creds,
+ body = Some(content),
+ pkgName = "proxy"))
+ }
+ }
+ }
+ }
+
+ it should s"invoke action in default package (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/default/export_c.json").
+ foreach { path =>
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
val response = responseAs[JsObject]
response shouldBe JsObject(
- "pkg" -> s"$systemId/partialmeta".toJson,
- "action" -> "getApi".toJson,
- "content" -> metaPayload(m.method.value, Map("a" -> "b", "c" -> "d"), creds))
+ "pkg" -> s"$systemId".toJson,
+ "action" -> "export_c".toJson,
+ "content" -> metaPayload("get", Map(), creds))
}
}
}
- }
- it should "invoke action for allowed verbs on meta handler and pass unmatched path to action" in {
- implicit val tid = transid()
+ it should s"project a field from the result object (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
- val paths = Seq("", "/", "/foo", "/foo/bar", "/foo/bar/", "/foo%20bar")
- paths.foreach { p =>
- withClue(s"failed on path: '$p'") {
- Get(s"/$routePath/partialmeta$p?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
- status should be(OK)
- val response = responseAs[JsObject]
- response shouldBe JsObject(
- "pkg" -> s"$systemId/partialmeta".toJson,
- "action" -> "getApi".toJson,
- "content" -> metaPayload("get", Map("a" -> "b", "c" -> "d"), creds, p))
- }
- }
- }
- }
-
- it should "invoke action that times out and provide a code" in {
- implicit val tid = transid()
-
- failActivation = 1
- Get(s"/$routePath/partialmeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
- status should be(Accepted)
- val response = responseAs[JsObject]
- confirmErrorWithTid(response, Some("Response not yet ready."))
- }
- }
-
- it should "invoke action that errors and response with error and code" in {
- implicit val tid = transid()
-
- failActivation = 2
- Get(s"/$routePath/partialmeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
- status should be(InternalServerError)
- val response = responseAs[JsObject]
- confirmErrorWithTid(response)
- }
- }
-
- it should "merge package parameters with action, query params and content payload" in {
- implicit val tid = transid()
- val body = JsObject("foo" -> "bar".toJson)
- Get(s"/$routePath/packagemeta/extra/path?a=b&c=d", body) ~> sealRoute(routes(creds)) ~> check {
- status should be(OK)
- val response = responseAs[JsObject]
- response shouldBe JsObject(
- "pkg" -> s"$systemId/packagemeta".toJson,
- "action" -> "getApi".toJson,
- "content" -> metaPayload(
- "get",
- Map("a" -> "b", "c" -> "d"),
- creds,
- path = "/extra/path",
- body = Some(body),
- pkgName = "packagemeta"))
- }
- }
-
- it should "reject request that defined reserved properties" in {
- implicit val tid = transid()
-
- val methods = Seq(Get, Post, Delete)
-
- methods.foreach { m =>
- WhiskMetaApi.reservedProperties.foreach { p =>
- m(s"/$routePath/packagemeta/?$p=YYY") ~> sealRoute(routes(creds)) ~> check {
- status should be(BadRequest)
- responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
- }
-
- m(s"/$routePath/packagemeta", JsObject(p -> "YYY".toJson)) ~> sealRoute(routes(creds)) ~> check {
- status should be(BadRequest)
- responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
- }
- }
- }
- }
-
- it should "invoke action and pass content body as string to action" in {
- implicit val tid = transid()
-
- val content = JsObject("extra" -> "read all about it".toJson, "yummy" -> true.toJson)
- Post(s"/$routePath/heavymeta?a=b&c=d", content) ~> sealRoute(routes(creds)) ~> check {
- status should be(OK)
- val response = responseAs[JsObject]
- response shouldBe JsObject(
- "pkg" -> s"$systemId/heavymeta".toJson,
- "action" -> "createRoute".toJson,
- "content" -> metaPayload("post", Map("a" -> "b", "c" -> "d"), creds, body = Some(content)))
- }
- }
-
- it should "invoke action and ignore invoke parameters that are immutable" in {
- implicit val tid = transid()
- val contentX = JsObject("x" -> "overriden".toJson)
- val contentZ = JsObject("z" -> "overriden".toJson)
-
- Get(s"/$routePath/packagemeta?x=overriden") ~> sealRoute(routes(creds)) ~> check {
- status should be(BadRequest)
- responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
- }
-
- Get(s"/$routePath/packagemeta?y=overriden") ~> sealRoute(routes(creds)) ~> check {
- status should be(BadRequest)
- responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
- }
-
- Get(s"/$routePath/packagemeta", contentX) ~> sealRoute(routes(creds)) ~> check {
- status should be(BadRequest)
- responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
- }
-
- Get(s"/$routePath/packagemeta?y=overriden", contentZ) ~> sealRoute(routes(creds)) ~> check {
- status should be(BadRequest)
- responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
- }
- }
-
- it should "rejection invoke action when receiving entity that is not a JsObject" in {
- implicit val tid = transid()
-
- Post(s"/$routePath/heavymeta?a=b&c=d", "1,2,3") ~> sealRoute(routes(creds)) ~> check {
- status should be(UnsupportedMediaType)
- responseAs[String] should include("application/json")
- }
-
- Post(s"/$routePath/heavymeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
- status should be(OK)
- }
-
- Post(s"/$routePath/heavymeta?a=b&c=d", JsObject()) ~> sealRoute(routes(creds)) ~> check {
- status should be(OK)
- }
-
- }
-
- it should "throttle authenticated user" in {
- implicit val tid = transid()
-
- Seq(systemId, creds.subject).foreach { subject =>
- failThrottleForSubject = Some(subject)
- val content = JsObject("extra" -> "read all about it".toJson, "yummy" -> true.toJson)
- Post(s"/$routePath/heavymeta?a=b&c=d", content) ~> sealRoute(routes(creds)) ~> check {
- status shouldBe {
- // activations are counted against to the authenticated user's quota
- if (subject == systemId) OK else {
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.tooManyRequests))
- TooManyRequests
+ Seq(s"$systemId/proxy/export_c.json/content").
+ foreach { path =>
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ val response = responseAs[JsObject]
+ response shouldBe metaPayload("get", Map(), creds, path = "/content", pkgName = "proxy")
}
}
- }
- }
- }
- it should "warn if meta package is public" in {
- implicit val tid = transid()
-
- Get(s"/$routePath/publicmeta") ~> sealRoute(routes(creds)) ~> check {
- status should be(OK)
- stream.toString should include regex (s"""[WARN] *.*publicmeta@0.0.1' is public""")
- stream.reset()
- }
- }
-
- it should "split action name and extenstion" in {
- val r = WhiskMetaApi.extensionSplitter
- Seq("t.j.http", "t.js.http", "tt.j.http", "tt.js.http").foreach { s =>
- val r(n, e) = s
- val i = s.lastIndexOf(".")
- n shouldBe s.substring(0, i)
- e shouldBe s.substring(i + 1)
+ Seq(s"$systemId/proxy/export_c.text/content/z", s"$systemId/proxy/export_c.text/content/z/", s"$systemId/proxy/export_c.text/content/z//").
+ foreach { path =>
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ val response = responseAs[String]
+ response shouldBe "Z"
+ }
+ }
}
- Seq("t.js", "t.js.htt", "t.js.httpz").foreach { s =>
- a[MatchError] should be thrownBy {
- val r(n, e) = s
- }
- }
+ it should s"reject when projecting a result which does not match expected type (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
- }
-
- it should "allow anonymous acccess to fully qualified name" in {
- implicit val tid = transid()
- val exports = s"/$routePath/$anonymousInvokePath"
-
- // none of these should match a route
- Seq("a", "a/b", "/a", s"$systemId/c", s"$systemId/export_c").
- foreach { path =>
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(NotFound)
- }
- }
-
- // the first of these fails in the identity lookup,
- // the second in the package lookup (does not exist),
- // the third and fourth fail the annotation check
- Seq("guest/proxy/c", s"$systemId/doesnotexist/c", s"$systemId/publicmeta/c", s"$systemId/default/c").
- foreach { path =>
- Get(s"$exports/${path}.json") ~> sealRoute(routes()) ~> check {
- status should be(NotFound)
- }
-
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(NotAcceptable)
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.contentTypeNotSupported))
- }
- }
-
- // both of these should produce full result objects (trailing slash is ok)
- // action name starting with export_ will have required annotation
- Seq(s"$systemId/proxy/export_c.json", s"$systemId/proxy/export_c.json/").
- foreach { path =>
- val p = if (path.endsWith("/")) "/" else ""
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(OK)
- val response = responseAs[JsObject]
- response shouldBe JsObject(
- "pkg" -> s"$systemId/proxy".toJson,
- "action" -> "export_c".toJson,
- "content" -> metaPayload("get", Map(), null, p, pkgName = "proxy"))
- }
- }
-
- // these should match action in default package
- Seq(s"$systemId/default/export_c.json").
- foreach { path =>
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(OK)
- val response = responseAs[JsObject]
- response shouldBe JsObject(
- "pkg" -> s"$systemId".toJson,
- "action" -> "export_c".toJson,
- "content" -> metaPayload("get", Map(), null))
- }
- }
-
- // these should project a field from the result object
- Seq(s"$systemId/proxy/export_c.json/content").
- foreach { path =>
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(OK)
- val response = responseAs[JsObject]
- response shouldBe metaPayload("get", Map(), null, "/content", pkgName = "proxy")
- }
- }
-
- // these project a result which does not match expected type
- Seq(s"$systemId/proxy/export_c.json/a").
- foreach { path =>
- actionResult = Some(JsObject("a" -> JsString("b")))
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(BadRequest)
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.invalidMedia(MediaTypes.`application/json`)))
- }
- }
-
- // reset the action result
- actionResult = None
-
- // these test an http response
- Seq(s"$systemId/proxy/export_c.http/content/response").
- foreach { path =>
- val httpResponse = JsObject("response" -> JsObject("headers" -> JsObject("location" -> "http://openwhisk.org".toJson), "code" -> Found.intValue.toJson))
- Get(s"$exports/$path", httpResponse) ~> sealRoute(routes()) ~> check {
- status should be(Found)
- header("location").get.toString shouldBe "location: http://openwhisk.org"
- }
- }
-
- // these test default projection for extension
- Seq(s"$systemId/proxy/export_c.http").
- foreach { path =>
- actionResult = Some(JsObject("headers" -> JsObject("location" -> "http://openwhisk.org".toJson), "code" -> Found.intValue.toJson))
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(Found)
- header("location").get.toString shouldBe "location: http://openwhisk.org"
- }
- }
-
- Seq(s"$systemId/proxy/export_c.text").
- foreach { path =>
- actionResult = Some(JsObject("text" -> JsString("default text")))
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(OK)
- val response = responseAs[String]
- response shouldBe "default text"
- }
- }
-
- Seq(s"$systemId/proxy/export_c.json").
- foreach { path =>
- actionResult = Some(JsObject("foobar" -> JsString("foobar")))
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(OK)
- val response = responseAs[JsObject]
- response shouldBe actionResult.get
- }
- }
-
- Seq(s"$systemId/proxy/export_c.html").
- foreach { path =>
- actionResult = Some(JsObject("html" -> JsString("<html>hi</htlml>")))
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(OK)
- val response = responseAs[String]
- response shouldBe "<html>hi</htlml>"
- }
- }
-
- // http web action with base64 encoded response
- Seq(s"$systemId/proxy/export_c.http").
- foreach { path =>
- actionResult = Some(JsObject(
- "headers" -> JsObject(
- "content-type" -> "application/json".toJson),
- "code" -> OK.intValue.toJson,
- "body" -> Base64.getEncoder.encodeToString {
- JsObject("field" -> "value".toJson).compactPrint.getBytes
- }.toJson))
-
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(OK)
- header("content-type").get.toString shouldBe "content-type: application/json"
- responseAs[JsObject] shouldBe JsObject("field" -> "value".toJson)
- }
- }
-
- // http web action with text response
- Seq(s"$systemId/proxy/export_c.http").
- foreach { path =>
- actionResult = Some(JsObject(
- "code" -> OK.intValue.toJson,
- "body" -> "hello world".toJson))
-
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(OK)
- responseAs[String] shouldBe "hello world"
- }
- }
-
- // http web action with mimatch between header and response
- Seq(s"$systemId/proxy/export_c.http").
- foreach { path =>
- actionResult = Some(JsObject(
- "headers" -> JsObject(
- "content-type" -> "application/json".toJson),
- "code" -> OK.intValue.toJson,
- "body" -> "hello world".toJson))
-
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(BadRequest)
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.httpContentTypeError))
- }
- }
-
- // http web action with unknown header
- Seq(s"$systemId/proxy/export_c.http").
- foreach { path =>
- actionResult = Some(JsObject(
- "headers" -> JsObject(
- "content-type" -> "xyz/bar".toJson),
- "code" -> OK.intValue.toJson,
- "body" -> "hello world".toJson))
-
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(BadRequest)
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.httpUnknownContentType))
- }
- }
-
- // an activation that results in application error and response matches extension
- Seq(s"$systemId/proxy/export_c.http", s"$systemId/proxy/export_c.http/ignoreme").
- foreach { path =>
- actionResult = Some(JsObject(
- "application_error" -> JsObject(
- "code" -> OK.intValue.toJson,
- "body" -> "no hello for you".toJson)))
-
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(OK)
- responseAs[String] shouldBe "no hello for you"
- }
- }
-
- // an activation that results in application error but where response does not match extension
- Seq(s"$systemId/proxy/export_c.json", s"$systemId/proxy/export_c.json/ignoreme").
- foreach { path =>
- actionResult = Some(JsObject("application_error" -> "bad response type".toJson))
-
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(BadRequest)
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.invalidMedia(MediaTypes.`application/json`)))
- }
- }
-
- // an activation that results in developer or system error
- Seq(s"$systemId/proxy/export_c.json", s"$systemId/proxy/export_c.json/ignoreme", s"$systemId/proxy/export_c.text").
- foreach { path =>
- Seq("developer_error", "whisk_error").foreach { e =>
- actionResult = Some(JsObject(e -> "bad response type".toJson))
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
+ // these project a result which does not match expected type
+ Seq(s"$systemId/proxy/export_c.json/a").
+ foreach { path =>
+ actionResult = Some(JsObject("a" -> JsString("b")))
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
status should be(BadRequest)
- if (e == "application_error") {
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.invalidMedia(MediaTypes.`application/json`)))
- } else {
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.errorProcessingRequest))
+ confirmErrorWithTid(responseAs[JsObject], Some(Messages.invalidMedia(MediaTypes.`application/json`)))
+ }
+ }
+ }
+
+ it should s"reject when projecting a field from the result object that does not exist (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.text/foobar", s"$systemId/proxy/export_c.text/content/z/x").
+ foreach { path =>
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(NotFound)
+ confirmErrorWithTid(responseAs[JsObject], Some(Messages.propertyNotFound))
+ }
+ }
+ }
+
+ it should s"not project an http response (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ // http extension does not project
+ Seq(s"$systemId/proxy/export_c.http/content/response").
+ foreach { path =>
+ actionResult = Some(JsObject("headers" -> JsObject("location" -> "http://openwhisk.org".toJson), "code" -> Found.intValue.toJson))
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(Found)
+ header("location").get.toString shouldBe "location: http://openwhisk.org"
+ }
+ }
+ }
+
+ it should s"use default projection for extension (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.http").
+ foreach { path =>
+ actionResult = Some(JsObject("headers" -> JsObject("location" -> "http://openwhisk.org".toJson), "code" -> Found.intValue.toJson))
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(Found)
+ header("location").get.toString shouldBe "location: http://openwhisk.org"
+ }
+ }
+
+ Seq(s"$systemId/proxy/export_c.text").
+ foreach { path =>
+ actionResult = Some(JsObject("text" -> JsString("default text")))
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ val response = responseAs[String]
+ response shouldBe "default text"
+ }
+ }
+
+ Seq(s"$systemId/proxy/export_c.json").
+ foreach { path =>
+ actionResult = Some(JsObject("foobar" -> JsString("foobar")))
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ val response = responseAs[JsObject]
+ response shouldBe actionResult.get
+ }
+ }
+
+ Seq(s"$systemId/proxy/export_c.html").
+ foreach { path =>
+ actionResult = Some(JsObject("html" -> JsString("<html>hi</htlml>")))
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ val response = responseAs[String]
+ response shouldBe "<html>hi</htlml>"
+ }
+ }
+ }
+
+ it should s"handle http web action with base64 encoded response (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.http").
+ foreach { path =>
+ actionResult = Some(JsObject(
+ "headers" -> JsObject(
+ "content-type" -> "application/json".toJson),
+ "code" -> OK.intValue.toJson,
+ "body" -> Base64.getEncoder.encodeToString {
+ JsObject("field" -> "value".toJson).compactPrint.getBytes
+ }.toJson))
+
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ header("content-type").get.toString shouldBe "content-type: application/json"
+ responseAs[JsObject] shouldBe JsObject("field" -> "value".toJson)
+ }
+ }
+ }
+
+ it should s"handle http web action with html/text response (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.http").
+ foreach { path =>
+ actionResult = Some(JsObject(
+ "code" -> OK.intValue.toJson,
+ "body" -> "hello world".toJson))
+
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ responseAs[String] shouldBe "hello world"
+ }
+ }
+ }
+
+ it should s"reject http web action with mimatch between header and response (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.http").
+ foreach { path =>
+ actionResult = Some(JsObject(
+ "headers" -> JsObject(
+ "content-type" -> "application/json".toJson),
+ "code" -> OK.intValue.toJson,
+ "body" -> "hello world".toJson))
+
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(BadRequest)
+ confirmErrorWithTid(responseAs[JsObject], Some(Messages.httpContentTypeError))
+ }
+ }
+ }
+
+ it should s"reject http web action with unknown header (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.http").
+ foreach { path =>
+ actionResult = Some(JsObject(
+ "headers" -> JsObject(
+ "content-type" -> "xyz/bar".toJson),
+ "code" -> OK.intValue.toJson,
+ "body" -> "hello world".toJson))
+
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(BadRequest)
+ confirmErrorWithTid(responseAs[JsObject], Some(Messages.httpUnknownContentType))
+ }
+ }
+ }
+
+ it should s"handle an activation that results in application error and response matches extension (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.http", s"$systemId/proxy/export_c.http/ignoreme").
+ foreach { path =>
+ actionResult = Some(JsObject(
+ "application_error" -> JsObject(
+ "code" -> OK.intValue.toJson,
+ "body" -> "no hello for you".toJson)))
+
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ responseAs[String] shouldBe "no hello for you"
+ }
+ }
+ }
+
+ it should s"handle an activation that results in application error but where response does not match extension (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.json", s"$systemId/proxy/export_c.json/ignoreme").
+ foreach { path =>
+ actionResult = Some(JsObject("application_error" -> "bad response type".toJson))
+
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(BadRequest)
+ confirmErrorWithTid(responseAs[JsObject], Some(Messages.invalidMedia(MediaTypes.`application/json`)))
+ }
+ }
+ }
+
+ it should s"handle an activation that results in developer or system error (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.json", s"$systemId/proxy/export_c.json/ignoreme", s"$systemId/proxy/export_c.text").
+ foreach { path =>
+ Seq("developer_error", "whisk_error").foreach { e =>
+ actionResult = Some(JsObject(e -> "bad response type".toJson))
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(BadRequest)
+ if (e == "application_error") {
+ confirmErrorWithTid(responseAs[JsObject], Some(Messages.invalidMedia(MediaTypes.`application/json`)))
+ } else {
+ confirmErrorWithTid(responseAs[JsObject], Some(Messages.errorProcessingRequest))
+ }
}
}
}
- }
+ }
- // reset the action result
- actionResult = None
+ it should s"support formdata (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
- // these reject the request because entity size exceeds allowed limit
- Seq(s"$systemId/proxy/export_c.json").
- foreach { path =>
- val largeEntity = "a" * (allowedActivationEntitySize.toInt + 1)
-
- val content = s"""{"a":"$largeEntity"}"""
- Post(s"$exports/$path", content.parseJson.asJsObject) ~> sealRoute(routes()) ~> check {
- status should be(RequestEntityTooLarge)
- val expectedErrorMsg = Messages.entityTooBig(SizeError(
- fieldDescriptionForSizeError,
- (largeEntity.length + 13).B,
- allowedActivationEntitySize.B))
- confirmErrorWithTid(responseAs[JsObject], Some(expectedErrorMsg))
+ Seq(s"$systemId/proxy/export_c.text/content/field1", s"$systemId/proxy/export_c.text/content/field2").
+ foreach { path =>
+ val form = FormData(Seq("field1" -> "value1", "field2" -> "value2"))
+ Post(s"$testRoutePath/$path", form) ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ responseAs[String] should (be("value1") or be("value2"))
+ }
}
+ }
- val form = FormData(Seq("a" -> largeEntity))
- Post(s"$exports/$path", form) ~> sealRoute(routes()) ~> check {
- status should be(RequestEntityTooLarge)
- val expectedErrorMsg = Messages.entityTooBig(SizeError(
- fieldDescriptionForSizeError,
- (largeEntity.length + 2).B,
- allowedActivationEntitySize.B))
- confirmErrorWithTid(responseAs[JsObject], Some(expectedErrorMsg))
+ it should s"reject requests when entity size exceeds allowed limit (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.json").
+ foreach { path =>
+ val largeEntity = "a" * (allowedActivationEntitySize.toInt + 1)
+
+ val content = s"""{"a":"$largeEntity"}"""
+ Post(s"$testRoutePath/$path", content.parseJson.asJsObject) ~> sealRoute(routes(creds)) ~> check {
+ status should be(RequestEntityTooLarge)
+ val expectedErrorMsg = Messages.entityTooBig(SizeError(
+ fieldDescriptionForSizeError,
+ (largeEntity.length + 13).B,
+ allowedActivationEntitySize.B))
+ confirmErrorWithTid(responseAs[JsObject], Some(expectedErrorMsg))
+ }
+
+ val form = FormData(Seq("a" -> largeEntity))
+ Post(s"$testRoutePath/$path", form) ~> sealRoute(routes(creds)) ~> check {
+ status should be(RequestEntityTooLarge)
+ val expectedErrorMsg = Messages.entityTooBig(SizeError(
+ fieldDescriptionForSizeError,
+ (largeEntity.length + 2).B,
+ allowedActivationEntitySize.B))
+ confirmErrorWithTid(responseAs[JsObject], Some(expectedErrorMsg))
+ }
+ }
+ }
+
+ it should s"reject unknown extensions (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Seq(s"$systemId/proxy/export_c.xyz", s"$systemId/proxy/export_c.xyz/", s"$systemId/proxy/export_c.xyz/content",
+ s"$systemId/proxy/export_c.xyzz", s"$systemId/proxy/export_c.xyzz/", s"$systemId/proxy/export_c.xyzz/content").
+ foreach { path =>
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(NotAcceptable)
+ confirmErrorWithTid(responseAs[JsObject], Some(Messages.contentTypeNotSupported))
+ }
+ }
+ }
+
+ it should s"reject request that tries to override reserved properties (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ allowedMethods.foreach { m =>
+ WhiskMetaApi.reservedProperties.foreach { p =>
+ m(s"$testRoutePath/$systemId/proxy/export_c.json?$p=YYY") ~> sealRoute(routes(creds)) ~> check {
+ status should be(BadRequest)
+ responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
+ }
+
+ m(s"$testRoutePath/$systemId/proxy/export_c.json", JsObject(p -> "YYY".toJson)) ~> sealRoute(routes(creds)) ~> check {
+ status should be(BadRequest)
+ responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
+ }
}
}
+ }
- Seq(s"$systemId/proxy/export_c.text/content/field1", s"$systemId/proxy/export_c.text/content/field2").
- foreach { path =>
- val form = FormData(Seq("field1" -> "value1", "field2" -> "value2"))
- Post(s"$exports/$path", form) ~> sealRoute(routes()) ~> check {
- status should be(OK)
- responseAs[String] should (be("value1") or be("value2"))
- }
+ it should s"reject request that tries to override final parameters (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+ val contentX = JsObject("x" -> "overriden".toJson)
+ val contentZ = JsObject("z" -> "overriden".toJson)
+
+ Get(s"$testRoutePath/$systemId/proxy/export_c.json?x=overriden") ~> sealRoute(routes(creds)) ~> check {
+ status should be(BadRequest)
+ responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
}
- Seq(s"$systemId/proxy/export_c.text/content/z", s"$systemId/proxy/export_c.text/content/z/", s"$systemId/proxy/export_c.text/content/z//").
- foreach { path =>
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(OK)
- val response = responseAs[String]
- response shouldBe "Z"
- }
+ Get(s"$testRoutePath/$systemId/proxy/export_c.json?y=overriden") ~> sealRoute(routes(creds)) ~> check {
+ status should be(BadRequest)
+ responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
}
- // this should fail for exceeding quota
- Seq(s"$systemId/proxy/export_c.text/content/z").
- foreach { path =>
- failThrottleForSubject = Some(systemId)
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(TooManyRequests)
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.tooManyRequests))
- }
- failThrottleForSubject = None
+ Get(s"$testRoutePath/$systemId/proxy/export_c.json", contentX) ~> sealRoute(routes(creds)) ~> check {
+ status should be(BadRequest)
+ responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
}
- // these should fail because parameter override is not allowed
- // ?x=overriden
- Seq(s"$systemId/proxy/export_c.text/content/z?x=overriden").
- foreach { path =>
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(BadRequest)
- responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
- }
+ Get(s"$testRoutePath/$systemId/proxy/export_c.json?y=overriden", contentZ) ~> sealRoute(routes(creds)) ~> check {
+ status should be(BadRequest)
+ responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
}
- // these fail to project a field from the result object (doesn't exist)
- Seq(s"$systemId/proxy/export_c.text/foobar", s"$systemId/proxy/export_c.text/content/z/x").
- foreach { path =>
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(NotFound)
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.propertyNotFound))
- }
+ Get(s"$testRoutePath/$systemId/proxy/export_c.json?empty=overriden") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ val response = responseAs[JsObject]
+ response shouldBe JsObject(
+ "pkg" -> s"$systemId/proxy".toJson,
+ "action" -> "export_c".toJson,
+ "content" -> metaPayload(
+ "get",
+ Map("empty" -> "overriden"),
+ creds,
+ pkgName = "proxy"))
+ }
+ }
+
+ it should s"rejection invoke action when receiving entity that is not a JsObject (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ Post(s"$testRoutePath/$systemId/proxy/export_c.json?a=b&c=d", "1,2,3") ~> sealRoute(routes(creds)) ~> check {
+ status should be(UnsupportedMediaType)
+ responseAs[String] should include("application/json")
}
- // these fail with content type required in known set
- Seq(s"$systemId/proxy/export_c.xyz", s"$systemId/proxy/export_c.xyz/", s"$systemId/proxy/export_c.xyz/content",
- s"$systemId/proxy/export_c.xyzz", s"$systemId/proxy/export_c.xyzz/", s"$systemId/proxy/export_c.xyzz/content").
- foreach { path =>
- Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
- status should be(NotAcceptable)
- confirmErrorWithTid(responseAs[JsObject], Some(Messages.contentTypeNotSupported))
- }
+ Post(s"$testRoutePath/$systemId/proxy/export_c.json?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
}
+
+ Post(s"$testRoutePath/$systemId/proxy/export_c.json?a=b&c=d", JsObject()) ~> sealRoute(routes(creds)) ~> check {
+ status should be(OK)
+ }
+
+ }
+
+ it should s"throttle subject owning namespace for web action (auth? ${creds.isDefined})" in {
+ implicit val tid = transid()
+
+ // this should fail for exceeding quota
+ Seq(s"$systemId/proxy/export_c.text/content/z").
+ foreach { path =>
+ failThrottleForSubject = Some(systemId)
+ Get(s"$testRoutePath/$path") ~> sealRoute(routes(creds)) ~> check {
+ status should be(TooManyRequests)
+ confirmErrorWithTid(responseAs[JsObject], Some(Messages.tooManyRequests))
+ }
+ failThrottleForSubject = None
+ }
+ }
}
class TestingEntitlementProvider(
diff --git a/tests/src/whisk/test/http/RESTProxy.scala b/tests/src/whisk/test/http/RESTProxy.scala
index d361616..fbd2d76 100644
--- a/tests/src/whisk/test/http/RESTProxy.scala
+++ b/tests/src/whisk/test/http/RESTProxy.scala
@@ -36,8 +36,9 @@
case class UnbindFor(duration: FiniteDuration)
}
-/** A simple REST proxy that can receive commands to change its behavior (e.g.
- * simulate failures of the proxied service). Not for use in production.
+/**
+ * A simple REST proxy that can receive commands to change its behavior (e.g.
+ * simulate failures of the proxied service). Not for use in production.
*/
class RESTProxy(val host: String, val port: Int)(val serviceAuthority: Uri.Authority, val useHTTPS: Boolean) extends Actor with ActorLogging {
private implicit val actorSystem = context.system