/* * 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