Allow unauthenticated invocation of actions.

Allow result projection by property name.
Fix view to lookup identity for namespace.
Add headers to parameters passed to action.
Pass request metadata to anonymous activations (previously was only for meta actions).
Pass projection path to action.
Support an extension following action name to specify content type for the response. Only supported values are accepted and others rejected with appropriate messages. Extension is requierd.
Allow http response type which sets headers and code directly from the action.
Provide default projection for supported media types.
Catch timeout exception due to long activation and return proper code.
Add test for web actions from CLI.
diff --git a/tests/src/common/Wsk.scala b/tests/src/common/Wsk.scala
index 15ee8a5..9b04a11 100644
--- a/tests/src/common/Wsk.scala
+++ b/tests/src/common/Wsk.scala
@@ -945,7 +945,7 @@
     }
 
     /**
-     * returns user 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/system/rest/RestUtil.scala b/tests/src/system/rest/RestUtil.scala
index 55a125d..d85155e 100644
--- a/tests/src/system/rest/RestUtil.scala
+++ b/tests/src/system/rest/RestUtil.scala
@@ -35,7 +35,10 @@
     private val trustStorePassword = WhiskProperties.getSslCertificateChallenge
 
     // force RestAssured to allow all hosts in SSL certificates
-    protected val sslconfig = new RestAssuredConfig().sslConfig(new SSLConfig().keystore("keystore", trustStorePassword).allowAllHostnames());
+    protected val sslconfig = {
+        new RestAssuredConfig().
+            sslConfig(new SSLConfig().keystore("keystore", trustStorePassword).allowAllHostnames());
+    }
 
     /**
      * @return the URL and port for the whisk service
diff --git a/tests/src/whisk/core/cli/test/WskWebActionsTests.scala b/tests/src/whisk/core/cli/test/WskWebActionsTests.scala
new file mode 100644
index 0000000..8c9e67f
--- /dev/null
+++ b/tests/src/whisk/core/cli/test/WskWebActionsTests.scala
@@ -0,0 +1,66 @@
+/*
+ * Copyright 2015-2016 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package whisk.core.cli.test
+
+import org.junit.runner.RunWith
+import org.scalatest.junit.JUnitRunner
+
+import com.jayway.restassured.RestAssured
+
+import common.TestHelpers
+import common.TestUtils
+import common.Wsk
+import common.WskAdmin
+import common.WskProps
+import common.WskTestHelpers
+import spray.json._
+import spray.json.DefaultJsonProtocol._
+import system.rest.RestUtil
+
+/**
+ * Tests for basic CLI usage. Some of these tests require a deployed backend.
+ */
+@RunWith(classOf[JUnitRunner])
+class WskWebActionsTests
+    extends TestHelpers
+    with WskTestHelpers
+    with RestUtil {
+
+    implicit val wskprops = WskProps()
+    val wsk = new Wsk
+    val namespace = WskAdmin.getUser(wskprops.authKey)._2
+
+    behavior of "Wsk Web Actions"
+
+    it should "create a web action 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))
+            }
+
+            val response = RestAssured.given().config(sslconfig).
+                get(getServiceURL() + s"/api/v1/experimental/web/$namespace/default/webaction.text/a?a=A")
+
+            response.statusCode() should be(200)
+            response.body().asString() shouldBe "A"
+    }
+
+}
diff --git a/tests/src/whisk/core/controller/test/MetaApiTests.scala b/tests/src/whisk/core/controller/test/MetaApiTests.scala
index 5a5434f..80567aa 100644
--- a/tests/src/whisk/core/controller/test/MetaApiTests.scala
+++ b/tests/src/whisk/core/controller/test/MetaApiTests.scala
@@ -27,6 +27,7 @@
 import org.scalatest.junit.JUnitRunner
 
 import akka.event.Logging.InfoLevel
+import spray.http.HttpMethods
 import spray.http.StatusCodes._
 import spray.httpx.SprayJsonSupport._
 import spray.httpx.SprayJsonSupport.sprayJsonMarshaller
@@ -34,6 +35,8 @@
 import spray.json._
 import spray.json.DefaultJsonProtocol._
 import whisk.common.TransactionId
+import whisk.core.controller.Context
+import whisk.core.controller.RejectRequest
 import whisk.core.controller.WhiskMetaApi
 import whisk.core.database.NoDocumentException
 import whisk.core.entity._
@@ -109,8 +112,59 @@
             EntityPath(systemId.asString),
             EntityName("bindingmeta"),
             Some(Binding(EntityName(systemId.asString), EntityName("heavymeta"))),
-            annotations = Parameters("meta", JsBoolean(true))))
+            annotations = Parameters("meta", JsBoolean(true))),
+        WhiskPackage(
+            EntityPath(systemId.asString),
+            EntityName("proxy"),
+            parameters = Parameters("x", JsString("X")) ++ Parameters("z", JsString("z"))))
 
+    override protected def getPackage(pkgName: FullyQualifiedEntityName)(
+        implicit transid: TransactionId) = {
+        Future {
+            packages.find(_.fullyQualifiedName(false) == pkgName).get
+        } recoverWith {
+            case _: NoSuchElementException => Future.failed(NoDocumentException("does not exist"))
+        }
+    }
+
+    val defaultActionParameters = {
+        Parameters("y", JsString("Y")) ++ Parameters("z", JsString("Z"))
+    }
+
+    override protected def getAction(actionName: FullyQualifiedEntityName)(
+        implicit transid: TransactionId) = {
+        if (!failActionLookup) {
+            def theAction = {
+                val annotations = Parameters(WhiskAction.finalParamsAnnotationName, JsBoolean(true))
+
+                WhiskAction(actionName.path, actionName.name, Exec.js("??"), defaultActionParameters, annotations = {
+                    if (actionName.name.asString.startsWith("export_")) {
+                        annotations ++ Parameters("web-export", JsBoolean(true))
+                    } else annotations
+                })
+            }
+
+            if (actionName.path.defaultPackage) {
+                Future.successful(theAction)
+            } else {
+                getPackage(actionName.path.toFullyQualifiedEntityName) map (_ => theAction)
+            }
+        } else {
+            Future.failed(NoDocumentException("doesn't exist"))
+        }
+    }
+
+    override protected def getIdentity(namespace: EntityName)(
+        implicit transid: TransactionId): Future[Identity] = {
+        if (namespace.asString == systemId.asString) {
+            systemIdentity
+        } else {
+            info(this, s"namespace has no identity")
+            Future.failed(RejectRequest(BadRequest))
+        }
+    }
+
+    var actionResult: Option[JsObject] = None
     override protected[controller] def invokeAction(user: Identity, action: WhiskAction, payload: Option[JsObject], blocking: Boolean, waitOverride: Option[FiniteDuration] = None)(
         implicit transid: TransactionId): Future[(ActivationId, Option[WhiskActivation])] = {
         if (failActivation == 0) {
@@ -118,7 +172,7 @@
             // 1. the package name for the action (to confirm that this resolved to systemId)
             // 2. the action name (to confirm that this resolved to the expected meta action)
             // 3. the payload received by the action which consists of the action.params + payload
-            val result = JsObject(
+            val result = actionResult getOrElse JsObject(
                 "pkg" -> action.namespace.toJson,
                 "action" -> action.name.toJson,
                 "content" -> action.parameters.merge(payload).get)
@@ -135,7 +189,7 @@
             // 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)
-            pkgLookup(packageName) foreach { pkg =>
+            getPackage(packageName) foreach { pkg =>
                 action.parameters shouldBe (pkg.parameters ++ defaultActionParameters)
                 action.parameters.get("z") shouldBe defaultActionParameters.get("z")
             }
@@ -148,44 +202,17 @@
         }
     }
 
-    protected def pkgLookup(pkgName: String) = packages.find(_.name == EntityName(pkgName))
-
-    override protected def pkgLookup(pkgName: FullyQualifiedEntityName)(
-        implicit transid: TransactionId) = {
-        Future {
-            packages.find(_.name == pkgName.name).get
-        } recoverWith {
-            case t => Future.failed(NoDocumentException("doesn't exist"))
-        }
-    }
-
-    val defaultActionParameters = {
-        Parameters("y", JsString("Y")) ++ Parameters("z", JsString("Z"))
-    }
-
-    override protected def actionLookup(pkgName: FullyQualifiedEntityName, actionName: EntityName)(
-        implicit transid: TransactionId) = {
-        if (!failActionLookup) {
-            Future.successful {
-                val annotations = Parameters(WhiskAction.finalParamsAnnotationName, JsBoolean(true))
-                WhiskAction(pkgName.fullPath, actionName, Exec.js("??"), defaultActionParameters, annotations = annotations)
-            }
-        } else {
-            Future.failed(NoDocumentException("doesn't exist"))
-        }
-    }
-
-    def metaPayload(method: String, params: Map[String, String], namespace: String, path: String = "", body: Option[JsObject] = None, pkg: Option[WhiskPackage] = None) = {
-        (pkg.map(_.parameters).getOrElse(Parameters()) ++ defaultActionParameters).merge {
-            Some {
-                JsObject(
-                    params.toJson.asJsObject.fields ++
-                        body.map(_.fields).getOrElse(Map()) ++
-                        Map("__ow_meta_verb" -> method.toLowerCase.toJson,
-                            "__ow_meta_path" -> path.toJson,
-                            "__ow_meta_namespace" -> namespace.toJson))
-            }
-        }.get
+    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)))
+            .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
     }
 
     var failActionLookup = false // toggle to cause action lookup to fail
@@ -194,6 +221,7 @@
     override def afterEach() = {
         failActionLookup = false
         failActivation = 0
+        actionResult = None
     }
 
     it should "resolve a meta package into the systemId namespace" in {
@@ -207,7 +235,7 @@
         val methods = Seq((Put, MethodNotAllowed))
         methods.foreach {
             case (m, code) =>
-                m("/experimental/partialmeta") ~> sealRoute(routes(creds)) ~> check {
+                m(s"/$routePath/partialmeta") ~> sealRoute(routes(creds)) ~> check {
                     status should be(code)
                 }
         }
@@ -218,29 +246,30 @@
         val methods = Seq(Get, Post, Delete)
 
         methods.foreach { m =>
-            m("/experimental") ~> sealRoute(routes(creds)) ~> check {
+            m(s"/$routePath") ~> sealRoute(routes(creds)) ~> check {
                 status shouldBe NotFound
             }
         }
 
         val paths = Seq(
-            "/experimental/doesntexist",
-            "/experimental/notmeta",
-            "/experimental/badmeta",
-            "/experimental/bindingmeta")
+            (s"/$routePath/doesntexist", NotFound),
+            (s"/$routePath/notmeta", NotFound),
+            (s"/$routePath/badmeta", MethodNotAllowed), // exists but has no mapping
+            (s"/$routePath/bindingmeta", NotFound))
 
-        paths.foreach { p =>
-            methods.foreach { m =>
-                m(p) ~> sealRoute(routes(creds)) ~> check {
-                    withClue(p) {
-                        status shouldBe MethodNotAllowed
+        paths.foreach {
+            case ((p, s)) =>
+                methods.foreach { m =>
+                    m(p) ~> sealRoute(routes(creds)) ~> check {
+                        withClue(p) {
+                            status shouldBe s
+                        }
                     }
                 }
-            }
         }
 
         failActionLookup = true
-        Get("/experimental/publicmeta") ~> sealRoute(routes(creds)) ~> check {
+        Get(s"/$routePath/publicmeta") ~> sealRoute(routes(creds)) ~> check {
             status should be(InternalServerError)
         }
     }
@@ -251,13 +280,13 @@
         val methods = Seq((Get, "getApi"), (Post, "createRoute"), (Delete, "deleteApi"))
         methods.foreach {
             case (m, name) =>
-                m("/experimental/heavymeta?a=b&c=d&namespace=xyz") ~> sealRoute(routes(creds)) ~> check {
+                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.namespace.name))
+                        "content" -> metaPayload(m.method.value, Map("a" -> "b", "c" -> "d", "namespace" -> "xyz"), creds))
                 }
         }
     }
@@ -268,14 +297,14 @@
         val methods = Seq((Get, OK), (Post, MethodNotAllowed), (Delete, MethodNotAllowed))
         methods.foreach {
             case (m, code) =>
-                m("/experimental/partialmeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
+                m(s"/$routePath/partialmeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
                     status should be(code)
                     if (status == 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.namespace.name))
+                            "content" -> metaPayload(m.method.value, Map("a" -> "b", "c" -> "d"), creds))
                     }
                 }
         }
@@ -287,13 +316,13 @@
         val paths = Seq("", "/", "/foo", "/foo/bar", "/foo/bar/", "/foo%20bar")
         paths.foreach { p =>
             withClue(s"failed on path: '$p'") {
-                Get(s"/experimental/partialmeta$p?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
+                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.namespace.name, p))
+                        "content" -> metaPayload("get", Map("a" -> "b", "c" -> "d"), creds, p))
                 }
             }
         }
@@ -303,7 +332,7 @@
         implicit val tid = transid()
 
         failActivation = 1
-        Get(s"/experimental/partialmeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
+        Get(s"/$routePath/partialmeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
             status should be(Accepted)
             val response = responseAs[JsObject]
             response.fields.size shouldBe 1
@@ -316,7 +345,7 @@
         implicit val tid = transid()
 
         failActivation = 2
-        Get(s"/experimental/partialmeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
+        Get(s"/$routePath/partialmeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
             status should be(InternalServerError)
             val response = responseAs[JsObject]
             response.fields.size shouldBe 2
@@ -328,9 +357,8 @@
 
     it should "merge package parameters with action, query params and content payload" in {
         implicit val tid = transid()
-
         val body = JsObject("foo" -> "bar".toJson)
-        Get("/experimental/packagemeta/extra/path?a=b&c=d", body) ~> sealRoute(routes(creds)) ~> check {
+        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(
@@ -339,10 +367,10 @@
                 "content" -> metaPayload(
                     "get",
                     Map("a" -> "b", "c" -> "d"),
-                    creds.namespace.name,
+                    creds,
                     path = "/extra/path",
                     body = Some(body),
-                    pkg = pkgLookup("packagemeta")))
+                    pkgName = "packagemeta"))
         }
     }
 
@@ -352,13 +380,13 @@
         val methods = Seq(Get, Post, Delete)
 
         methods.foreach { m =>
-            reservedProperties.foreach { p =>
-                m(s"/experimental/packagemeta/?$p=YYY") ~> sealRoute(routes(creds)) ~> check {
+            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("/experimental/packagemeta", JsObject(p -> "YYY".toJson)) ~> sealRoute(routes(creds)) ~> check {
+                m(s"/$routePath/packagemeta", JsObject(p -> "YYY".toJson)) ~> sealRoute(routes(creds)) ~> check {
                     status should be(BadRequest)
                     responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
                 }
@@ -370,13 +398,13 @@
         implicit val tid = transid()
 
         val content = JsObject("extra" -> "read all about it".toJson, "yummy" -> true.toJson)
-        Post(s"/experimental/heavymeta?a=b&c=d", content) ~> sealRoute(routes(creds)) ~> check {
+        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.namespace.name, body = Some(content)))
+                "content" -> metaPayload("post", Map("a" -> "b", "c" -> "d"), creds, body = Some(content)))
         }
     }
 
@@ -385,22 +413,22 @@
         val contentX = JsObject("x" -> "overriden".toJson)
         val contentZ = JsObject("z" -> "overriden".toJson)
 
-        Get(s"/experimental/packagemeta?x=overriden") ~> sealRoute(routes(creds)) ~> check {
+        Get(s"/$routePath/packagemeta?x=overriden") ~> sealRoute(routes(creds)) ~> check {
             status should be(BadRequest)
             responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
         }
 
-        Get(s"/experimental/packagemeta?y=overriden") ~> sealRoute(routes(creds)) ~> check {
+        Get(s"/$routePath/packagemeta?y=overriden") ~> sealRoute(routes(creds)) ~> check {
             status should be(BadRequest)
             responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
         }
 
-        Get(s"/experimental/packagemeta", contentX) ~> sealRoute(routes(creds)) ~> check {
+        Get(s"/$routePath/packagemeta", contentX) ~> sealRoute(routes(creds)) ~> check {
             status should be(BadRequest)
             responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
         }
 
-        Get(s"/experimental/packagemeta?y=overriden", contentZ) ~> sealRoute(routes(creds)) ~> check {
+        Get(s"/$routePath/packagemeta?y=overriden", contentZ) ~> sealRoute(routes(creds)) ~> check {
             status should be(BadRequest)
             responseAs[ErrorResponse].error shouldBe Messages.parametersNotAllowed
         }
@@ -409,16 +437,16 @@
     it should "rejection invoke action when receiving entity that is not a JsObject" in {
         implicit val tid = transid()
 
-        Post(s"/experimental/heavymeta?a=b&c=d", "1,2,3") ~> sealRoute(routes(creds)) ~> check {
+        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"/experimental/heavymeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
+        Post(s"/$routePath/heavymeta?a=b&c=d") ~> sealRoute(routes(creds)) ~> check {
             status should be(OK)
         }
 
-        Post(s"/experimental/heavymeta?a=b&c=d", JsObject()) ~> sealRoute(routes(creds)) ~> check {
+        Post(s"/$routePath/heavymeta?a=b&c=d", JsObject()) ~> sealRoute(routes(creds)) ~> check {
             status should be(OK)
         }
 
@@ -432,7 +460,7 @@
         this.outputStream = printstream
 
         try {
-            Get("/experimental/publicmeta") ~> sealRoute(routes(creds)) ~> check {
+            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()
@@ -443,4 +471,174 @@
         }
     }
 
+    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)
+                    responseAs[String] shouldBe Messages.contentTypeRequired
+                }
+            }
+
+        // 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")
+                }
+            }
+
+        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)
+                }
+            }
+
+        // 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"
+                    val response = responseAs[JsObject]
+                    response shouldBe JsObject()
+                }
+            }
+
+        // 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"
+                    val response = responseAs[JsObject]
+                    response shouldBe JsObject()
+                }
+            }
+
+        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>"
+                }
+            }
+
+        // reset the action result
+        actionResult = None
+
+        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"
+                }
+            }
+
+        // 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
+                }
+            }
+
+        // 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)
+                    responseAs[String] shouldBe Messages.propertyNotFound
+                }
+            }
+
+        // these fail with unsupported extension
+        Seq(s"$systemId/proxy/export_c.xyz", s"$systemId/proxy/export_c.xyz/", s"$systemId/proxy/export_c.xyz/content").
+            foreach { path =>
+                Get(s"$exports/$path") ~> sealRoute(routes()) ~> check {
+                    status should be(NotAcceptable)
+                    responseAs[String] shouldBe Messages.contentTypeNotSupported
+                }
+            }
+    }
 }
diff --git a/tests/src/whisk/core/database/test/DbUtils.scala b/tests/src/whisk/core/database/test/DbUtils.scala
index 6fc9970..82ec3b7 100644
--- a/tests/src/whisk/core/database/test/DbUtils.scala
+++ b/tests/src/whisk/core/database/test/DbUtils.scala
@@ -128,7 +128,7 @@
     def waitOnView(db: AuthStore, authkey: AuthKey, count: Int)(
         implicit context: ExecutionContext, transid: TransactionId, timeout: Duration) = {
         val success = retry(() => {
-            Identity.list(db, authkey) map { l =>
+            Identity.list(db, List(authkey.uuid.asString, authkey.key.asString)) map { l =>
                 if (l.length != count) {
                     throw RetryOp()
                 } else true
diff --git a/tests/src/whisk/core/entity/test/SchemaTests.scala b/tests/src/whisk/core/entity/test/SchemaTests.scala
index d659e42..958675e 100644
--- a/tests/src/whisk/core/entity/test/SchemaTests.scala
+++ b/tests/src/whisk/core/entity/test/SchemaTests.scala
@@ -160,7 +160,7 @@
 
     it should "accept well formed names" in {
         val paths = Seq("a", "a b", "a@b.c", "_a", "_", "_ _", "a0", "a 0", "a.0", "a@@", "0", "0.0", "0.0.0", "0a", "0.a")
-        val spaces = paths.foreach { n =>
+        paths.foreach { n =>
             assert(EntityName(n).toString == n)
         }
     }
@@ -405,7 +405,7 @@
 
     it should "filter immutable parameters" in {
         val params = Parameters("k", "v") ++ Parameters("ns", null: String) ++ Parameters("njs", JsNull)
-        params.immutableParameters shouldBe Set("k")
+        params.definedParameters shouldBe Set("k")
     }
 
     it should "reject malformed JSON" in {