From af8591b5e9150c80ccb0073d485eceb82b1263fd Mon Sep 17 00:00:00 2001 From: Mathias Date: Thu, 11 Sep 2014 17:14:19 +0200 Subject: [PATCH] +htk Add akka-http-testkit as a port of spray-testkit Currently the Specs2 support is still missing. --- .../akka/http/testkit/RequestBuilding.scala | 99 ++++++++++++ .../scala/akka/http/testkit/RouteTest.scala | 144 ++++++++++++++++++ .../testkit/RouteTestResultComponent.scala | 102 +++++++++++++ .../akka/http/testkit/RouteTestTimeout.scala | 15 ++ .../http/testkit/TestFrameworkInterface.scala | 30 ++++ .../testkit/TransformerPipelineSupport.scala | 50 ++++++ .../src/test/resources/reference.conf | 2 + .../http/testkit/ScalatestRouteTestSpec.scala | 81 ++++++++++ 8 files changed, 523 insertions(+) create mode 100644 akka-http-testkit/src/main/scala/akka/http/testkit/RequestBuilding.scala create mode 100644 akka-http-testkit/src/main/scala/akka/http/testkit/RouteTest.scala create mode 100644 akka-http-testkit/src/main/scala/akka/http/testkit/RouteTestResultComponent.scala create mode 100644 akka-http-testkit/src/main/scala/akka/http/testkit/RouteTestTimeout.scala create mode 100644 akka-http-testkit/src/main/scala/akka/http/testkit/TestFrameworkInterface.scala create mode 100644 akka-http-testkit/src/main/scala/akka/http/testkit/TransformerPipelineSupport.scala create mode 100644 akka-http-testkit/src/test/resources/reference.conf create mode 100644 akka-http-testkit/src/test/scala/akka/http/testkit/ScalatestRouteTestSpec.scala diff --git a/akka-http-testkit/src/main/scala/akka/http/testkit/RequestBuilding.scala b/akka-http-testkit/src/main/scala/akka/http/testkit/RequestBuilding.scala new file mode 100644 index 0000000000..f6f7d0421c --- /dev/null +++ b/akka-http-testkit/src/main/scala/akka/http/testkit/RequestBuilding.scala @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.testkit + +import scala.collection.immutable +import scala.concurrent.{ Await, ExecutionContext } +import scala.concurrent.duration._ +import scala.reflect.ClassTag +import akka.util.Timeout +import akka.event.{ Logging, LoggingAdapter } +import akka.http.model.parser.HeaderParser +import akka.http.marshalling._ +import akka.http.model._ +import headers.{ HttpCredentials, RawHeader } +import HttpMethods._ + +trait RequestBuilding extends TransformerPipelineSupport { + type RequestTransformer = HttpRequest ⇒ HttpRequest + + class RequestBuilder(val method: HttpMethod) { + def apply(): HttpRequest = + apply("/") + + def apply(uri: String): HttpRequest = + apply(uri, HttpEntity.Empty) + + def apply[T](uri: String, content: T)(implicit m: ToEntityMarshallers[T], ec: ExecutionContext): HttpRequest = + apply(uri, Some(content)) + + def apply[T](uri: String, content: Option[T])(implicit m: ToEntityMarshallers[T], ec: ExecutionContext): HttpRequest = + apply(Uri(uri), content) + + def apply(uri: String, entity: HttpEntity.Regular): HttpRequest = + apply(Uri(uri), entity) + + def apply(uri: Uri): HttpRequest = + apply(uri, HttpEntity.Empty) + + def apply[T](uri: Uri, content: T)(implicit m: ToEntityMarshallers[T], ec: ExecutionContext): HttpRequest = + apply(uri, Some(content)) + + def apply[T](uri: Uri, content: Option[T])(implicit m: ToEntityMarshallers[T], timeout: Timeout = Timeout(1.second), ec: ExecutionContext): HttpRequest = + content match { + case None ⇒ apply(uri, HttpEntity.Empty) + case Some(value) ⇒ + val entity = Marshal(value).to[HttpEntity.Regular].await(timeout.duration) + apply(uri, entity) + } + + def apply(uri: Uri, entity: HttpEntity.Regular): HttpRequest = + HttpRequest(method, uri, Nil, entity) + } + + val Get = new RequestBuilder(GET) + val Post = new RequestBuilder(POST) + val Put = new RequestBuilder(PUT) + val Patch = new RequestBuilder(PATCH) + val Delete = new RequestBuilder(DELETE) + val Options = new RequestBuilder(OPTIONS) + val Head = new RequestBuilder(HEAD) + + // TODO: reactivate after HTTP message encoding has been ported + //def encode(encoder: Encoder, flow: FlowMaterializer): RequestTransformer = encoder.encode(_, flow) + + def addHeader(header: HttpHeader): RequestTransformer = _.mapHeaders(header +: _) + + def addHeader(headerName: String, headerValue: String): RequestTransformer = { + val rawHeader = RawHeader(headerName, headerValue) + addHeader(HeaderParser.parseHeader(rawHeader).left.flatMap(_ ⇒ Right(rawHeader)).right.get) + } + + def addHeaders(first: HttpHeader, more: HttpHeader*): RequestTransformer = _.mapHeaders(_ ++ (first +: more)) + + def mapHeaders(f: immutable.Seq[HttpHeader] ⇒ immutable.Seq[HttpHeader]): RequestTransformer = _.mapHeaders(f) + + def removeHeader(headerName: String): RequestTransformer = + _ mapHeaders (_ filterNot (_.name equalsIgnoreCase headerName)) + + def removeHeader[T <: HttpHeader: ClassTag]: RequestTransformer = + removeHeader(implicitly[ClassTag[T]].runtimeClass) + + def removeHeader(clazz: Class[_]): RequestTransformer = + _ mapHeaders (_ filterNot clazz.isInstance) + + def removeHeaders(names: String*): RequestTransformer = + _ mapHeaders (_ filterNot (header ⇒ names exists (_ equalsIgnoreCase header.name))) + + def addCredentials(credentials: HttpCredentials) = addHeader(headers.Authorization(credentials)) + + def logRequest(log: LoggingAdapter, level: Logging.LogLevel = Logging.DebugLevel) = logValue[HttpRequest](log, level) + + def logRequest(logFun: HttpRequest ⇒ Unit) = logValue[HttpRequest](logFun) + + implicit def header2AddHeader(header: HttpHeader): RequestTransformer = addHeader(header) +} + +object RequestBuilding extends RequestBuilding \ No newline at end of file diff --git a/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTest.scala b/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTest.scala new file mode 100644 index 0000000000..8c98675f42 --- /dev/null +++ b/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTest.scala @@ -0,0 +1,144 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.testkit + +import akka.http.util.Deferrable +import com.typesafe.config.{ ConfigFactory, Config } +import scala.collection.immutable +import scala.concurrent.duration._ +import scala.util.DynamicVariable +import scala.reflect.ClassTag +import org.scalatest.Suite +import akka.actor.ActorSystem +import akka.stream.FlowMaterializer +import akka.http.routing._ +import akka.http.unmarshalling._ +import akka.http.model._ +import headers.Host + +trait RouteTest extends RequestBuilding with RouteTestResultComponent { + this: TestFrameworkInterface ⇒ + + /** Override to supply a custom ActorSystem */ + protected def createActorSystem(): ActorSystem = + ActorSystem(actorSystemNameFrom(getClass), testConfig) + + def actorSystemNameFrom(clazz: Class[_]) = + clazz.getName + .replace('.', '-') + .replace('_', '-') + .filter(_ != '$') + + def testConfigSource: String = "" + def testConfig: Config = { + val source = testConfigSource + val config = if (source.isEmpty) ConfigFactory.empty() else ConfigFactory.parseString(source) + config.withFallback(ConfigFactory.load()) + } + implicit val system = createActorSystem() + implicit def executor = system.dispatcher + implicit val materializer = FlowMaterializer() + + def cleanUp(): Unit = system.shutdown() + + private val dynRR = new DynamicVariable[RouteTestResult](null) + private def result = + if (dynRR.value ne null) dynRR.value + else sys.error("This value is only available inside of a `check` construct!") + + def check[T](body: ⇒ T): RouteTestResult ⇒ T = result ⇒ dynRR.withValue(result.awaitResult)(body) + + def handled: Boolean = result.handled + def response: HttpResponse = result.response + def entity: HttpEntity = result.entity + def chunks: immutable.Seq[HttpEntity.ChunkStreamPart] = result.chunks + def entityAs[T: FromEntityUnmarshaller: ClassTag](implicit timeout: Duration = 1.second): T = { + def msg(e: Throwable) = s"Could not unmarshal entity to type '${implicitly[ClassTag[T]]}' for `entityAs` assertion: $e\n\nResponse was: $response" + Unmarshal(entity).to[T].recover[T] { case error ⇒ failTest(msg(error)) }.await(timeout) + } + def responseAs[T: FromResponseUnmarshaller: ClassTag](implicit timeout: Duration = 1.second): T = { + def msg(e: Throwable) = s"Could not unmarshal response to type '${implicitly[ClassTag[T]]}' for `responseAs` assertion: $e\n\nResponse was: $response" + Unmarshal(response).to[T].recover[T] { case error ⇒ failTest(msg(error)) }.await(timeout) + } + def contentType: ContentType = entity.contentType + def mediaType: MediaType = contentType.mediaType + def charset: HttpCharset = contentType.charset + def definedCharset: Option[HttpCharset] = contentType.definedCharset + def headers: immutable.Seq[HttpHeader] = response.headers + def header[T <: HttpHeader: ClassTag]: Option[T] = response.header[T] + def header(name: String): Option[HttpHeader] = response.headers.find(_.is(name.toLowerCase)) + def status: StatusCode = response.status + + def closingExtension: String = chunks.lastOption match { + case Some(HttpEntity.LastChunk(extension, _)) ⇒ extension + case _ ⇒ "" + } + def trailer: immutable.Seq[HttpHeader] = chunks.lastOption match { + case Some(HttpEntity.LastChunk(_, trailer)) ⇒ trailer + case _ ⇒ Nil + } + + def rejections: List[Rejection] = result.rejections + def rejection: Rejection = { + val r = rejections + if (r.size == 1) r.head else failTest("Expected a single rejection but got %s (%s)".format(r.size, r)) + } + + /** + * A dummy that can be used as `~> runRoute` to run the route but without blocking for the result. + * The result of the pipeline is the result that can later be checked with `check`. See the + * "separate running route from checking" example from ScalatestRouteTestSpec.scala. + */ + def runRoute: RouteTestResult ⇒ RouteTestResult = akka.http.util.identityFunc + + // there is already an implicit class WithTransformation in scope (inherited from akka.http.testkit.TransformerPipelineSupport) + // however, this one takes precedence + implicit class WithTransformation2(request: HttpRequest) { + def ~>[A, B](f: A ⇒ B)(implicit ta: TildeArrow[A, B]): ta.Out = ta(request, f) + } + + abstract class TildeArrow[A, B] { + type Out + def apply(request: HttpRequest, f: A ⇒ B): Out + } + + case class DefaultHostInfo(host: Host, securedConnection: Boolean) + object DefaultHostInfo { + implicit def defaultHost: DefaultHostInfo = DefaultHostInfo(Host("example.com"), securedConnection = false) + } + object TildeArrow { + implicit object InjectIntoRequestTransformer extends TildeArrow[HttpRequest, HttpRequest] { + type Out = HttpRequest + def apply(request: HttpRequest, f: HttpRequest ⇒ HttpRequest) = f(request) + } + implicit def injectIntoRoute(implicit timeout: RouteTestTimeout, setup: RoutingSetup, + defaultHostInfo: DefaultHostInfo) = + new TildeArrow[RequestContext, Deferrable[RouteResult]] { + type Out = RouteTestResult + def apply(request: HttpRequest, route: Route): Out = { + val routeTestResult = new RouteTestResult(timeout.duration)(setup.materializer) + val effectiveRequest = + request.withEffectiveUri( + securedConnection = defaultHostInfo.securedConnection, + defaultHostHeader = defaultHostInfo.host) + val ctx = new RequestContextImpl(effectiveRequest, setup.routingLog.requestLog(effectiveRequest)) + val sealedExceptionHandler = { + import setup._ + if (exceptionHandler.isDefault) exceptionHandler + else exceptionHandler orElse ExceptionHandler.default(settings)(setup.executionContext) + } + val semiSealedRoute = // sealed for exceptions but not for rejections + Directives.handleExceptions(sealedExceptionHandler) { route } + val deferrableRouteResult = semiSealedRoute(ctx) + deferrableRouteResult.foreach(routeTestResult.handleResult)(setup.executor) + routeTestResult + } + } + } +} + +trait ScalatestRouteTest extends RouteTest with TestFrameworkInterface.Scalatest { this: Suite ⇒ } + +//FIXME: trait Specs2RouteTest extends RouteTest with Specs2Interface \ No newline at end of file diff --git a/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTestResultComponent.scala b/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTestResultComponent.scala new file mode 100644 index 0000000000..a0cfc657c8 --- /dev/null +++ b/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTestResultComponent.scala @@ -0,0 +1,102 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.testkit + +import java.util.concurrent.CountDownLatch +import org.reactivestreams.Publisher +import scala.collection.immutable +import scala.concurrent.duration._ +import scala.concurrent.{ Await, ExecutionContext } +import akka.stream.FlowMaterializer +import akka.stream.scaladsl.Flow +import akka.http.model.HttpEntity.ChunkStreamPart +import akka.http.routing._ +import akka.http.model._ + +trait RouteTestResultComponent { + + def failTest(msg: String): Nothing + + /** + * A receptacle for the response or rejections created by a route. + */ + class RouteTestResult(timeout: FiniteDuration)(implicit fm: FlowMaterializer) { + private[this] var result: Option[Either[List[Rejection], HttpResponse]] = None + private[this] val latch = new CountDownLatch(1) + + def handled: Boolean = synchronized { result.isDefined && result.get.isRight } + + def rejections: List[Rejection] = synchronized { + result match { + case Some(Left(rejections)) ⇒ rejections + case Some(Right(response)) ⇒ failTest("Request was not rejected, response was " + response) + case None ⇒ failNeitherCompletedNorRejected() + } + } + + def response: HttpResponse = rawResponse.copy(entity = entity) + + /** Returns a "fresh" entity with a "fresh" unconsumed byte- or chunk stream (if not strict) */ + def entity: HttpEntity = entityRecreator() + + def chunks: immutable.Seq[ChunkStreamPart] = + entity match { + case HttpEntity.Chunked(_, chunks) ⇒ awaitAllElements[ChunkStreamPart](chunks) + case _ ⇒ Nil + } + + def ~>[T](f: RouteTestResult ⇒ T): T = f(this) + + private def rawResponse: HttpResponse = synchronized { + result match { + case Some(Right(response)) ⇒ response + case Some(Left(Nil)) ⇒ failTest("Request was rejected") + case Some(Left(rejection :: Nil)) ⇒ failTest("Request was rejected with rejection " + rejection) + case Some(Left(rejections)) ⇒ failTest("Request was rejected with rejections " + rejections) + case None ⇒ failNeitherCompletedNorRejected() + } + } + + private[testkit] def handleResult(rr: RouteResult)(implicit ec: ExecutionContext): Unit = + synchronized { + if (result.isEmpty) { + result = rr match { + case RouteResult.Complete(response) ⇒ Some(Right(response)) + case RouteResult.Rejected(rejections) ⇒ Some(Left(RejectionHandler.applyTransformations(rejections))) + case RouteResult.Failure(error) ⇒ sys.error("Route produced exception: " + error) + } + latch.countDown() + } else failTest("Route completed/rejected more than once") + } + + private[testkit] def awaitResult: this.type = { + latch.await(timeout.toMillis, MILLISECONDS) + this + } + + private[this] lazy val entityRecreator: () ⇒ HttpEntity = + rawResponse.entity match { + case s: HttpEntity.Strict ⇒ () ⇒ s + + case HttpEntity.Default(contentType, contentLength, data) ⇒ + val dataChunks = awaitAllElements(data); + { () ⇒ HttpEntity.Default(contentType, contentLength, Flow(dataChunks).toPublisher()) } + + case HttpEntity.CloseDelimited(contentType, data) ⇒ + val dataChunks = awaitAllElements(data); + { () ⇒ HttpEntity.CloseDelimited(contentType, Flow(dataChunks).toPublisher()) } + + case HttpEntity.Chunked(contentType, chunks) ⇒ + val dataChunks = awaitAllElements(chunks); + { () ⇒ HttpEntity.Chunked(contentType, Flow(dataChunks).toPublisher()) } + } + + private def failNeitherCompletedNorRejected(): Nothing = + failTest("Request was neither completed nor rejected within " + timeout) + + private def awaitAllElements[T](data: Publisher[T]): immutable.Seq[T] = + Await.result(Flow(data).grouped(Int.MaxValue).toFuture(), timeout) + } +} \ No newline at end of file diff --git a/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTestTimeout.scala b/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTestTimeout.scala new file mode 100644 index 0000000000..c87a8affa1 --- /dev/null +++ b/akka-http-testkit/src/main/scala/akka/http/testkit/RouteTestTimeout.scala @@ -0,0 +1,15 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.testkit + +import scala.concurrent.duration._ +import akka.actor.ActorSystem +import akka.testkit._ + +case class RouteTestTimeout(duration: FiniteDuration) + +object RouteTestTimeout { + implicit def default(implicit system: ActorSystem) = RouteTestTimeout(1.second dilated) +} diff --git a/akka-http-testkit/src/main/scala/akka/http/testkit/TestFrameworkInterface.scala b/akka-http-testkit/src/main/scala/akka/http/testkit/TestFrameworkInterface.scala new file mode 100644 index 0000000000..f13434641a --- /dev/null +++ b/akka-http-testkit/src/main/scala/akka/http/testkit/TestFrameworkInterface.scala @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.testkit + +import org.scalatest.exceptions.TestFailedException +import org.scalatest.{ BeforeAndAfterAll, Suite } + +trait TestFrameworkInterface { + + def cleanUp() + + def failTest(msg: String): Nothing +} + +object TestFrameworkInterface { + + trait Scalatest extends TestFrameworkInterface with BeforeAndAfterAll { + this: Suite ⇒ + + def failTest(msg: String) = throw new TestFailedException(msg, 11) + + abstract override protected def afterAll(): Unit = { + cleanUp() + super.afterAll() + } + } + +} diff --git a/akka-http-testkit/src/main/scala/akka/http/testkit/TransformerPipelineSupport.scala b/akka-http-testkit/src/main/scala/akka/http/testkit/TransformerPipelineSupport.scala new file mode 100644 index 0000000000..7c7531253d --- /dev/null +++ b/akka-http-testkit/src/main/scala/akka/http/testkit/TransformerPipelineSupport.scala @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.testkit + +import akka.event.{ Logging, LoggingAdapter } + +import scala.concurrent.{ Future, ExecutionContext } + +trait TransformerPipelineSupport { + + def logValue[T](log: LoggingAdapter, level: Logging.LogLevel = Logging.DebugLevel): T ⇒ T = + logValue { value ⇒ log.log(level, value.toString) } + + def logValue[T](logFun: T ⇒ Unit): T ⇒ T = { response ⇒ + logFun(response) + response + } + + implicit class WithTransformation[A](value: A) { + def ~>[B](f: A ⇒ B): B = f(value) + } + + implicit class WithTransformerConcatenation[A, B](f: A ⇒ B) extends (A ⇒ B) { + def apply(input: A) = f(input) + def ~>[AA, BB, R](g: AA ⇒ BB)(implicit aux: TransformerAux[A, B, AA, BB, R]) = + new WithTransformerConcatenation[A, R](aux(f, g)) + } +} + +object TransformerPipelineSupport extends TransformerPipelineSupport + +trait TransformerAux[A, B, AA, BB, R] { + def apply(f: A ⇒ B, g: AA ⇒ BB): A ⇒ R +} + +object TransformerAux { + implicit def aux1[A, B, C] = new TransformerAux[A, B, B, C, C] { + def apply(f: A ⇒ B, g: B ⇒ C): A ⇒ C = f andThen g + } + implicit def aux2[A, B, C](implicit ec: ExecutionContext) = + new TransformerAux[A, Future[B], B, C, Future[C]] { + def apply(f: A ⇒ Future[B], g: B ⇒ C): A ⇒ Future[C] = f(_).map(g) + } + implicit def aux3[A, B, C](implicit ec: ExecutionContext) = + new TransformerAux[A, Future[B], B, Future[C], Future[C]] { + def apply(f: A ⇒ Future[B], g: B ⇒ Future[C]): A ⇒ Future[C] = f(_).flatMap(g) + } +} \ No newline at end of file diff --git a/akka-http-testkit/src/test/resources/reference.conf b/akka-http-testkit/src/test/resources/reference.conf new file mode 100644 index 0000000000..85f3c7ab62 --- /dev/null +++ b/akka-http-testkit/src/test/resources/reference.conf @@ -0,0 +1,2 @@ +# override strange reference.conf setting in akka-stream test scope +akka.actor.default-mailbox.mailbox-type = akka.dispatch.UnboundedMailbox diff --git a/akka-http-testkit/src/test/scala/akka/http/testkit/ScalatestRouteTestSpec.scala b/akka-http-testkit/src/test/scala/akka/http/testkit/ScalatestRouteTestSpec.scala new file mode 100644 index 0000000000..c2d45ae295 --- /dev/null +++ b/akka-http-testkit/src/test/scala/akka/http/testkit/ScalatestRouteTestSpec.scala @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2009-2014 Typesafe Inc. + */ + +package akka.http.testkit + +import scala.concurrent.duration._ +import org.scalatest.FreeSpec +import org.scalatest.Matchers +import akka.testkit.TestProbe +import akka.util.Timeout +import akka.pattern.ask +import akka.http.model.headers.RawHeader +import akka.http.routing._ +import akka.http.model._ +import StatusCodes._ +import HttpMethods._ +import ScalaRoutingDSL._ + +class ScalatestRouteTestSpec extends FreeSpec with Matchers with ScalatestRouteTest { + + "The ScalatestRouteTest should support" - { + + "the most simple and direct route test" in { + Get() ~> complete(HttpResponse()) ~> { rr ⇒ rr.awaitResult; rr.response } shouldEqual HttpResponse() + } + + "a test using a directive and some checks" in { + val pinkHeader = RawHeader("Fancy", "pink") + Get() ~> addHeader(pinkHeader) ~> { + respondWithHeader(pinkHeader) { + complete("abc") + } + } ~> check { + status shouldEqual OK + entity shouldEqual HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc") + header("Fancy") shouldEqual Some(pinkHeader) + } + } + + "proper rejection collection" in { + Post("/abc", "content") ~> { + (get | put) { + complete("naah") + } + } ~> check { + rejections shouldEqual List(MethodRejection(GET), MethodRejection(PUT)) + } + } + + "separation of route execution from checking" in { + val pinkHeader = RawHeader("Fancy", "pink") + + case object Command + val service = TestProbe() + val handler = TestProbe() + implicit def serviceRef = service.ref + implicit val askTimeout: Timeout = 1.second + + val result = + Get() ~> pinkHeader ~> { + respondWithHeader(pinkHeader) { + complete(handler.ref.ask(Command).mapTo[String]) + } + } ~> runRoute + + handler.expectMsg(Command) + handler.reply("abc") + + check { + status shouldEqual OK + entity shouldEqual HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc") + header("Fancy") shouldEqual Some(pinkHeader) + }(result) + } + } + + // TODO: remove once RespondWithDirectives have been ported + def respondWithHeader(responseHeader: HttpHeader): Directive0 = + mapHttpResponseHeaders(responseHeader +: _) +}