From e5d7a992b36fe663ba1ab951d3b793b57aa2757b Mon Sep 17 00:00:00 2001 From: Simon Popugaev Date: Sat, 11 Jun 2022 20:36:43 +0300 Subject: [PATCH] update zio to 2.0.0-RC6 replace all assert/assertM with assertTrue --- README.md | 48 +++--- build.sbt | 2 +- project/BuildHelper.scala | 2 +- project/plugins.sbt | 21 ++- project/project/sbt-updates.sbt | 2 +- project/sbt-updates.sbt | 2 +- .../akka/http/expose/ExposedRouteTest.scala | 33 ++-- .../test/akkahttp/AkkaZIOSpecDefault.scala | 10 +- .../scala/zio/test/akkahttp/RouteTest.scala | 126 ++------------- .../test/akkahttp/RouteTestEnvironment.scala | 2 +- .../zio/test/akkahttp/RouteTestResult.scala | 146 +++++++++++++++--- .../akkahttp/AkkaZIOSpecDefaultSpec.scala | 66 ++++---- .../akkahttp/RouteZIOSpecDefaultSpec.scala | 63 ++++---- 13 files changed, 254 insertions(+), 269 deletions(-) diff --git a/README.md b/README.md index ffa8710..d83008f 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ libraryDependencies += "info.senia" %% "zio-test-akka-http" % "x.x.x" The basic structure of a test built with the testkit is this (expression placeholder in all-caps): ``` -assertM(REQUEST ~> ROUTE) { - ASSERTIONS +(REQUEST ~> ROUTE).map { res => + assertTrue(res.some.path == value) } ``` @@ -38,11 +38,9 @@ object MySpec extends ZIOSpecDefault { def spec = suite("MySpec")( test("my test") { - assertM(Get() ~> complete(HttpResponse()))( - handled( - response(equalTo(HttpResponse())) - ) - ) + (Get() ~> complete(HttpResponse())).map { res => + assertTrue(res.handled.get.response == HttpResponse()) + } } ).provideShared(RouteTestEnvironment.environment) } @@ -61,11 +59,9 @@ object MySpec extends AkkaZIOSpecDefault { def spec = suite("MySpec")( test("my test") { - assertM(Get() ~> complete(HttpResponse()))( - handled( - response(equalTo(HttpResponse())) - ) - ) + (Get() ~> complete(HttpResponse())).map { res => + assertTrue(res.handled.get.response == HttpResponse()) + } } ) } @@ -83,11 +79,9 @@ import zio.test.Assertion._ import zio.test._ import zio.test.akkahttp.assertions._ -assertM(Get() ~> complete(HttpResponse()))( - handled( - response(equalTo(HttpResponse())) - ) -) +(Get() ~> complete(HttpResponse())).map { res => + assertTrue(res.handled.get.response == HttpResponse()) +} ``` Available request builders: `Get`,`Post`,`Put`,`Patch`,`Delete`,`Options`,`Head`. @@ -99,12 +93,12 @@ You can use any function `HttpRequest => HttpRequest` to modify request. There are several common request modifications provided: `addHeader`,`mapHeaders`,`removeHeader`,`addCredentials`: ```scala -assertM(Get() ~> addHeader("MyHeader", "value") ~> route)(???) +Get() ~> addHeader("MyHeader", "value") ~> route ``` You can also add header with `~>` method: ```scala -assertM(Get() ~> RawHeader("MyHeader", "value") ~> route)(???) +Get() ~> RawHeader("MyHeader", "value") ~> route ``` ## Assertions @@ -131,9 +125,9 @@ There are several assertions available: There should be an assertion for every [inspector from original Akka-HTTP Route TestKit](https://doc.akka.io/docs/akka-http/current/routing-dsl/testkit.html#table-of-inspectors). If you can't find an assertion for existing inspector please open an issue. -Note that assertions are lazy on response fetching - you can use assertions for status code and headers even if route returns an infinite body: +Note that assertions are eager on response fetching by default - you can't use assertions for status code and headers if route returns an infinite body. -Assertions are incompatible with `assert` - use `assertM` instead. +To avoid eager response body fetching use lazy `?~>` method instead of last `~>`: ```scala import akka.http.scaladsl.model.StatusCodes.OK @@ -156,12 +150,12 @@ val route = get { } } -assertM(Get() ~> route)( - handled( - status(equalTo(OK)) && - header("Fancy", isSome(equalTo(pinkHeader))), - ), -) +(Get() ?~> route).map { res => + assertTrue( + res.handled.get.status == OK, + res.handled.get.header("Fancy").get == pinkHeader, + ) +} ``` diff --git a/build.sbt b/build.sbt index 2ba3337..e66bba8 100644 --- a/build.sbt +++ b/build.sbt @@ -27,7 +27,7 @@ ThisBuild / publishTo := sonatypePublishToBundle.value addCommandAlias("fmt", "all scalafmtSbt scalafmt test:scalafmt") addCommandAlias("check", "all scalafmtSbtCheck scalafmtCheck test:scalafmtCheck") -lazy val zioVersion = "2.0.0-RC5" +lazy val zioVersion = "2.0.0-RC6" lazy val akkaVersion = "2.6.19" lazy val akkaHttpVersion = "10.2.9" diff --git a/project/BuildHelper.scala b/project/BuildHelper.scala index 5fd0e01..b1ec60e 100644 --- a/project/BuildHelper.scala +++ b/project/BuildHelper.scala @@ -72,7 +72,7 @@ object BuildHelper { val stdSettings = Seq( scalacOptions := stdOptions, - crossScalaVersions := Seq("2.13.8", "2.12.15", "3.1.2"), + crossScalaVersions := Seq("2.13.8", "2.12.16", "3.1.2"), ThisBuild / scalaVersion := crossScalaVersions.value.head, scalacOptions := stdOptions ++ extraOptions(scalaVersion.value), Test / parallelExecution := true, diff --git a/project/plugins.sbt b/project/plugins.sbt index 996eee6..56169be 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,12 +1,9 @@ -addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") -addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") -addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.16") -addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.7.0") -addSbtPlugin("ch.epfl.scala" % "sbt-bloop" % "1032048a") -addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.2") -addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1") -addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.13") -addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1") -addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.4.6") +addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") +addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.9.3") +addSbtPlugin("com.github.cb372" % "sbt-explicit-dependencies" % "0.2.16") +addSbtPlugin("com.dwijnand" % "sbt-dynver" % "4.1.1") +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.1.2") +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.13") +addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.10.0-RC1") +addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.5.10") diff --git a/project/project/sbt-updates.sbt b/project/project/sbt-updates.sbt index 400f09c..95f09fd 100644 --- a/project/project/sbt-updates.sbt +++ b/project/project/sbt-updates.sbt @@ -1 +1 @@ -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.2") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.3") diff --git a/project/sbt-updates.sbt b/project/sbt-updates.sbt index 400f09c..95f09fd 100644 --- a/project/sbt-updates.sbt +++ b/project/sbt-updates.sbt @@ -1 +1 @@ -addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.2") +addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.3") diff --git a/src/main/scala/akka/http/expose/ExposedRouteTest.scala b/src/main/scala/akka/http/expose/ExposedRouteTest.scala index 5bab356..6c4efa2 100644 --- a/src/main/scala/akka/http/expose/ExposedRouteTest.scala +++ b/src/main/scala/akka/http/expose/ExposedRouteTest.scala @@ -6,8 +6,6 @@ import akka.http.scaladsl.model.headers.`Sec-WebSocket-Protocol` import akka.http.scaladsl.server._ import akka.http.scaladsl.settings.{ParserSettings, RoutingSettings} import akka.stream.Materializer -import zio.test.Assertion -import zio.test.Assertion._ import zio.test.akkahttp.{RouteTest, RouteTestResult} import zio.{URIO, ZIO} @@ -16,24 +14,10 @@ import scala.concurrent.ExecutionContextExecutor trait ExposedRouteTest { this: RouteTest => - /** Asserts that the received response is a WebSocket upgrade response and the extracts the chosen subprotocol and - * passes it to the handler. - */ - def expectWebSocketUpgradeWithProtocol(assertion: Assertion[String]): Assertion[RouteTestResult.Completed] = - (isWebSocketUpgrade && header[`Sec-WebSocket-Protocol`]( - isSome( - hasField( - "protocols", - (_: `Sec-WebSocket-Protocol`).protocols, - hasSize[String](equalTo(1)) && hasFirst(assertion), - ), - ), - )) ?? "expectWebSocketUpgradeWithProtocol" - protected def executeRequest( request: HttpRequest, route: Route, - ): URIO[RouteTest.Environment with ActorSystem, RouteTestResult] = + ): URIO[RouteTest.Environment with ActorSystem, RouteTestResult.Lazy] = for { system <- ZIO.service[ActorSystem] config <- ZIO.service[RouteTest.Config] @@ -67,7 +51,7 @@ trait ExposedRouteTest { .fromFuture(_ => semiSealedRoute(ctx)) .orDie .flatMap { - case RouteResult.Complete(response) => RouteTestResult.Completed.make(response) + case RouteResult.Complete(response) => RouteTestResult.LazyCompleted.make(response) case RouteResult.Rejected(rejections) => ZIO.succeed(RouteTestResult.Rejected(rejections)) } .timeout(config.routeTestTimeout) @@ -75,3 +59,16 @@ trait ExposedRouteTest { } } yield res } + +object ExposedRouteTest { + + /** Check that the received response is a WebSocket upgrade response and extracts the chosen subprotocol. + */ + def webSocketUpgradeWithProtocol(result: RouteTestResult.Completed): Option[String] = + if (!result.isWebSocketUpgrade) None + else + result.header[`Sec-WebSocket-Protocol`].flatMap { h => + if (h.protocols.lengthCompare(1) == 0) h.protocols.headOption + else None + } +} diff --git a/src/main/scala/zio/test/akkahttp/AkkaZIOSpecDefault.scala b/src/main/scala/zio/test/akkahttp/AkkaZIOSpecDefault.scala index 176dd45..0fbafae 100644 --- a/src/main/scala/zio/test/akkahttp/AkkaZIOSpecDefault.scala +++ b/src/main/scala/zio/test/akkahttp/AkkaZIOSpecDefault.scala @@ -1,15 +1,13 @@ package zio.test.akkahttp import zio._ -import zio.internal.stacktracer.Tracer -import zio.test.{TestEnvironment, ZIOSpec, ZSpec} +import zio.test.{Spec, TestEnvironment, ZIOSpec} trait AkkaZIOSpecDefault extends ZIOSpec[RouteTestEnvironment.TestEnvironment with TestEnvironment] with RouteTest { - override val layer: ZLayer[ZIOAppArgs with Scope, Any, RouteTestEnvironment.TestEnvironment with TestEnvironment] = { - implicit val trace: zio.ZTraceElement = Tracer.newTrace + override val bootstrap + : ZLayer[ZIOAppArgs with Scope, Any, RouteTestEnvironment.TestEnvironment with TestEnvironment] = zio.ZEnv.live >>> TestEnvironment.live >+> RouteTestEnvironment.environment - } - def spec: ZSpec[RouteTestEnvironment.TestEnvironment with TestEnvironment with Scope, Any] + def spec: Spec[RouteTestEnvironment.TestEnvironment with TestEnvironment with Scope, Any] } diff --git a/src/main/scala/zio/test/akkahttp/RouteTest.scala b/src/main/scala/zio/test/akkahttp/RouteTest.scala index d669caa..2831711 100644 --- a/src/main/scala/zio/test/akkahttp/RouteTest.scala +++ b/src/main/scala/zio/test/akkahttp/RouteTest.scala @@ -3,122 +3,16 @@ package zio.test.akkahttp import akka.actor.ActorSystem import akka.http.expose.ExposedRouteTest import akka.http.scaladsl.model._ -import akka.http.scaladsl.model.headers.{Host, Upgrade} -import akka.http.scaladsl.server.{Rejection, RequestContext, RouteResult} -import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, FromResponseUnmarshaller, Unmarshal} +import akka.http.scaladsl.model.headers.Host +import akka.http.scaladsl.server.{RequestContext, RouteResult} import akka.stream.Materializer import zio._ -import zio.test.Assertion.Render.param -import zio.test.Assertion._ -import zio.test._ import zio.test.akkahttp.RouteTest.Environment -import scala.collection.immutable import scala.concurrent.Future -import scala.reflect.ClassTag trait RouteTest extends ExposedRouteTest with MarshallingTestUtils with RequestBuilding { - def handled(assertion: AssertionM[RouteTestResult.Completed]): AssertionM[RouteTestResult] = - AssertionM.assertionRecM("handled")(param(assertion))(assertion) { - case complete: RouteTestResult.Completed => ZIO.some(complete) - case _ => ZIO.none - } - - def response(assertion: AssertionM[HttpResponse]): AssertionM[RouteTestResult.Completed] = - AssertionM.assertionRecM("response")(param(assertion))(assertion) { - _.response.fold(_ => None, Some(_)) - } - - def responseEntity(assertion: AssertionM[HttpEntity]): AssertionM[RouteTestResult.Completed] = - AssertionM.assertionRecM("responseEntity")(param(assertion))(assertion) { - _.freshEntity.fold(_ => None, Some(_)) - } - - def chunks( - assertion: AssertionM[Option[immutable.Seq[HttpEntity.ChunkStreamPart]]], - ): AssertionM[RouteTestResult.Completed] = - AssertionM.assertionRecM("chunks")(param(assertion))(assertion) { - _.chunks.fold(_ => None, Some(_)) - } - - def entityAs[T : FromEntityUnmarshaller : ClassTag]( - assertion: AssertionM[Either[Throwable, T]], - ): AssertionM[RouteTestResult.Completed] = - AssertionM.assertionRecM(s"entityAs[${implicitly[ClassTag[T]]}]")(param(assertion))(assertion) { c => - implicit val mat: Materializer = c.materializer - c.freshEntity - .flatMap(entity => ZIO.fromFuture(implicit ec => Unmarshal(entity).to[T]).either) - .fold(_ => None, Some(_)) - } - - def responseAs[T : FromResponseUnmarshaller : ClassTag]( - assertion: AssertionM[Either[Throwable, T]], - ): AssertionM[RouteTestResult.Completed] = - AssertionM.assertionRecM(s"responseAs[${implicitly[ClassTag[T]]}]")(param(assertion))(assertion) { c => - implicit val mat: Materializer = c.materializer - c.response - .flatMap(response => ZIO.fromFuture(implicit ec => Unmarshal(response).to[T]).either) - .fold(_ => None, Some(_)) - } - - def contentType(assertion: Assertion[ContentType]): Assertion[HttpEntity] = - Assertion.assertionRec("contentType")(param(assertion))(assertion)(entity => Some(entity.contentType)) - - def mediaType(assertion: Assertion[MediaType]): Assertion[ContentType] = - Assertion.assertionRec("mediaType")(param(assertion))(assertion)(contentType => Some(contentType.mediaType)) - - def charset(assertion: Assertion[Option[HttpCharset]]): Assertion[ContentType] = - Assertion.assertionRec("charset")(param(assertion))(assertion)(contentType => Some(contentType.charsetOption)) - - def headers(assertion: Assertion[immutable.Seq[HttpHeader]]): Assertion[RouteTestResult.Completed] = - Assertion.assertionRec("headers")(param(assertion))(assertion)(c => Some(c.rawResponse.headers)) - - def header[T >: Null <: HttpHeader : ClassTag]( - assertion: Assertion[Option[T]], - ): Assertion[RouteTestResult.Completed] = - Assertion.assertionRec(s"header[{implicitly[ClassTag[T]]}]")(param(assertion))(assertion) { c => - Some(c.rawResponse.header[T]) - } - - def header(name: String, assertion: Assertion[Option[HttpHeader]]): Assertion[RouteTestResult.Completed] = - Assertion.assertionRec("header")(param(name), param(assertion))(assertion) { c => - Some(c.rawResponse.headers.find(_.is(name.toLowerCase))) - } - - def status(assertion: Assertion[StatusCode]): Assertion[RouteTestResult.Completed] = - Assertion.assertionRec("status")(param(assertion))(assertion)(c => Some(c.rawResponse.status)) - - def closingExtension(assertion: Assertion[Option[String]]): Assertion[immutable.Seq[HttpEntity.ChunkStreamPart]] = - Assertion.assertionRec("closingExtension")(param(assertion))(assertion) { - _.lastOption match { - case Some(HttpEntity.LastChunk(extension, _)) => Some(Some(extension)) - case _ => None - } - } - - def trailer(assertion: Assertion[immutable.Seq[HttpHeader]]): Assertion[immutable.Seq[HttpEntity.ChunkStreamPart]] = - Assertion.assertionRec("trailer")(param(assertion))(assertion) { - _.lastOption match { - case Some(HttpEntity.LastChunk(_, trailer)) => Some(trailer) - case _ => None - } - } - - def rejected(assertion: Assertion[immutable.Seq[Rejection]]): Assertion[RouteTestResult] = - Assertion.assertionRec("rejected")(param(assertion))(assertion) { - case RouteTestResult.Rejected(rejections) => Some(rejections) - case _ => None - } - - def rejection(assertion: Assertion[Rejection]): Assertion[RouteTestResult] = - rejected(hasSize[Rejection](equalTo(1)) && hasFirst(assertion)) ?? "rejection" - - val isWebSocketUpgrade: Assertion[RouteTestResult.Completed] = - (status(equalTo(StatusCodes.SwitchingProtocols)) && - header[Upgrade](isSome[Upgrade](hasField("hasWebSocket", _.hasWebSocket, equalTo(true))))) ?? - "isWebSocketUpgrade" - implicit class WithTransformation(request: HttpRequest) { def ~>(f: HttpRequest => HttpRequest): HttpRequest = f(request) def ~>(header: HttpHeader): HttpRequest = ~>(addHeader(header)) @@ -130,13 +24,22 @@ trait RouteTest extends ExposedRouteTest with MarshallingTestUtils with RequestB } implicit class WithRoute(request: HttpRequest) { - def ~>(route: RequestContext => Future[RouteResult]): URIO[Environment with ActorSystem, RouteTestResult] = + def ?~>(route: RequestContext => Future[RouteResult]): URIO[Environment with ActorSystem, RouteTestResult.Lazy] = executeRequest(request, route) + + def ~>(route: RequestContext => Future[RouteResult]): URIO[Environment with ActorSystem, RouteTestResult.Eager] = + executeRequest(request, route).flatMap(_.toEager.catchAll(_ => ZIO.succeed(RouteTestResult.Timeout))) } implicit class WithRouteM[R, E](request: ZIO[R, E, HttpRequest]) { - def ~>(route: RequestContext => Future[RouteResult]): ZIO[Environment with ActorSystem with R, E, RouteTestResult] = - request.flatMap(executeRequest(_, route)) + def ?~>( + route: RequestContext => Future[RouteResult], + ): ZIO[Environment with ActorSystem with R, E, RouteTestResult.Lazy] = request.flatMap(executeRequest(_, route)) + + def ~>( + route: RequestContext => Future[RouteResult], + ): ZIO[Environment with ActorSystem with R, E, RouteTestResult.Eager] = + request.flatMap(executeRequest(_, route)).flatMap(_.toEager.catchAll(_ => ZIO.succeed(RouteTestResult.Timeout))) } } @@ -147,7 +50,6 @@ object RouteTest { case class Config( timeout: Duration = 15.seconds, - unmarshalTimeout: Duration = 1.second, marshallingTimeout: Duration = Duration.Finite(1.second.toNanos), routeTestTimeout: Duration = 1.second, defaultHost: DefaultHostInfo = DefaultHostInfo(Host("example.com"), securedConnection = false)) diff --git a/src/main/scala/zio/test/akkahttp/RouteTestEnvironment.scala b/src/main/scala/zio/test/akkahttp/RouteTestEnvironment.scala index 2903098..2db5bd3 100644 --- a/src/main/scala/zio/test/akkahttp/RouteTestEnvironment.scala +++ b/src/main/scala/zio/test/akkahttp/RouteTestEnvironment.scala @@ -30,7 +30,7 @@ object RouteTestEnvironment { } lazy val testMaterializer: URLayer[ActorSystem, Materializer] = - ZLayer.fromFunction(SystemMaterializer(_).materializer) + ZLayer.fromFunction(SystemMaterializer(_: ActorSystem).materializer) type TestEnvironment = ActorSystem with Materializer with RouteTest.Config diff --git a/src/main/scala/zio/test/akkahttp/RouteTestResult.scala b/src/main/scala/zio/test/akkahttp/RouteTestResult.scala index 9cf1937..ece9441 100644 --- a/src/main/scala/zio/test/akkahttp/RouteTestResult.scala +++ b/src/main/scala/zio/test/akkahttp/RouteTestResult.scala @@ -1,34 +1,98 @@ package zio.test.akkahttp -import _root_.akka.stream.scaladsl.{Sink, Source} -import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.headers.Upgrade +import akka.http.scaladsl.model.{ + AttributeKey, ContentType, HttpCharset, HttpEntity, HttpHeader, HttpProtocol, HttpResponse, MediaType, ResponseEntity, + StatusCode, StatusCodes, +} import akka.http.scaladsl.server.Rejection +import akka.http.scaladsl.unmarshalling.{FromEntityUnmarshaller, FromResponseUnmarshaller, Unmarshal} import akka.stream.Materializer +import akka.stream.scaladsl.{Sink, Source} import zio._ import zio.test.akkahttp.RouteTest.Environment +import zio.test.akkahttp.RouteTestResult.Completed import scala.collection.immutable +import scala.reflect.ClassTag -sealed trait RouteTestResult +sealed trait RouteTestResult { + def rejected: Option[immutable.Seq[Rejection]] + def isTimeout: Boolean + def handled: Option[Completed] +} object RouteTestResult { case object TimeoutError type TimeoutError = TimeoutError.type - final case class Rejected(rejections: immutable.Seq[Rejection]) extends RouteTestResult - case object Timeout extends RouteTestResult - final class Completed( - private[akkahttp] val rawResponse: HttpResponse, - val materializer: Materializer, - val config: RouteTest.Config, - val response: IO[TimeoutError, HttpResponse], + sealed trait Lazy extends RouteTestResult { + def toEager: IO[TimeoutError, Eager] + def handled: Option[LazyCompleted] + } + sealed trait Eager extends RouteTestResult { + def handled: Option[EagerCompleted] + } + + final case class Rejected(rejections: immutable.Seq[Rejection]) extends Lazy with Eager { + def toEager: UIO[Rejected] = ZIO.succeed(this) + + def rejected: Option[immutable.Seq[Rejection]] = Some(rejections) + def isTimeout: Boolean = false + def handled: Option[Nothing] = None + } + + case object Timeout extends Lazy with Eager { + def toEager: UIO[Timeout.type] = ZIO.succeed(this) + + def rejected: Option[immutable.Seq[Rejection]] = None + def isTimeout: Boolean = true + def handled: Option[Nothing] = None + } + + sealed trait Completed { + protected[akkahttp] def rawResponse: HttpResponse + def status: StatusCode = rawResponse.status + val headers: immutable.Seq[HttpHeader] = rawResponse.headers + val attributes: Map[AttributeKey[_], _] = rawResponse.attributes + val protocol: HttpProtocol = rawResponse.protocol + def rejected: Option[Nothing] = None + def isTimeout: Boolean = false + + def header[T >: Null <: HttpHeader : ClassTag]: Option[T] = rawResponse.header[T] + def header(name: String): Option[HttpHeader] = rawResponse.headers.find(_.is(name.toLowerCase)) + + def isWebSocketUpgrade: Boolean = + status == + StatusCodes.SwitchingProtocols && header[Upgrade].exists(_.hasWebSocket) + } + + final class LazyCompleted( + protected[akkahttp] val rawResponse: HttpResponse, val freshEntity: IO[TimeoutError, ResponseEntity], - val chunks: IO[TimeoutError, Option[immutable.Seq[HttpEntity.ChunkStreamPart]]]) - extends RouteTestResult { - override def toString: String = s"Completed($rawResponse)" + val response: IO[TimeoutError, HttpResponse], + val chunks: IO[TimeoutError, Option[immutable.Seq[HttpEntity.ChunkStreamPart]]], + )(implicit materializer: Materializer) + extends Lazy + with Completed { + def handled: Option[LazyCompleted] = Some(this) + + def entityAs[T : FromEntityUnmarshaller : ClassTag]: IO[TimeoutError, Either[Throwable, T]] = + freshEntity.flatMap(entity => ZIO.fromFuture(implicit ec => Unmarshal(entity).to[T]).either) + + def responseAs[T : FromResponseUnmarshaller : ClassTag]: IO[TimeoutError, Either[Throwable, T]] = + response.flatMap(response => ZIO.fromFuture(implicit ec => Unmarshal(response).to[T]).either) + + def toEager: IO[TimeoutError, EagerCompleted] = + for { + runtime <- ZIO.runtime[Any] + entry <- freshEntity + resp <- response + ch <- chunks + } yield new EagerCompleted(entity = entry, response = resp, chunks = ch, runtime = runtime) } - object Completed { + object LazyCompleted { private def awaitAllElements[T](data: Source[T, _]) = for { @@ -63,18 +127,60 @@ object RouteTestResult { case _ => ZIO.none } - def make(response: HttpResponse): URIO[Environment, Completed] = + def make(response: HttpResponse): URIO[Environment, LazyCompleted] = for { environment <- ZIO.environment[Environment] freshEntityR <- freshEntityEff(response) freshEntity = freshEntityR.provideEnvironment(environment) - } yield new Completed( + } yield new LazyCompleted( response, - environment.get[Materializer], - environment.get[RouteTest.Config], - freshEntity.map(response.withEntity), freshEntity, + freshEntity.map(response.withEntity), freshEntity.flatMap(getChunks).provideEnvironment(environment), - ) + )(environment.get[Materializer]) + } + + final class EagerCompleted( + val entity: ResponseEntity, + val response: HttpResponse, + val chunks: Option[immutable.Seq[HttpEntity.ChunkStreamPart]], + runtime: Runtime[Any], + )(implicit materializer: Materializer) + extends Eager + with Completed { + protected[akkahttp] def rawResponse: HttpResponse = response + + def handled: Option[EagerCompleted] = Some(this) + + def entityAs[T : FromEntityUnmarshaller : ClassTag]: Either[Throwable, T] = + // The entry is already in memory. Run here is not so bad. + runtime.unsafeRun(ZIO.fromFuture(implicit ec => Unmarshal(entity).to[T]).either) + + def responseAs[T : FromResponseUnmarshaller : ClassTag]: Either[Throwable, T] = + // The response is already in memory. Run here is not so bad. + runtime.unsafeRun(ZIO.fromFuture(implicit ec => Unmarshal(response).to[T]).either) + + def contentType: ContentType = entity.contentType + + def mediaType: MediaType = contentType.mediaType + + def charset: Option[HttpCharset] = contentType.charsetOption + + def closingExtension: Option[String] = + chunks.flatMap { + _.lastOption match { + case Some(HttpEntity.LastChunk(extension, _)) => Some(extension) + case _ => None + } + } + + def trailer: Option[immutable.Seq[HttpHeader]] = + chunks.flatMap { + _.lastOption match { + case Some(HttpEntity.LastChunk(_, trailer)) => Some(trailer) + case _ => None + } + } } + } diff --git a/src/test/scala/zio/test/akkahttp/AkkaZIOSpecDefaultSpec.scala b/src/test/scala/zio/test/akkahttp/AkkaZIOSpecDefaultSpec.scala index a0064f4..f184932 100644 --- a/src/test/scala/zio/test/akkahttp/AkkaZIOSpecDefaultSpec.scala +++ b/src/test/scala/zio/test/akkahttp/AkkaZIOSpecDefaultSpec.scala @@ -12,7 +12,6 @@ import akka.stream.scaladsl.Source import akka.testkit.TestProbe import akka.util.{ByteString, Timeout} import zio.ZIO -import zio.test.Assertion._ import zio.test._ import scala.concurrent.duration.DurationInt @@ -21,27 +20,26 @@ object AkkaZIOSpecDefaultSpec extends AkkaZIOSpecDefault { def spec = suite("ZioRouteTestSpec")( test("the most simple and direct route test") { - assertM(Get() ~> complete(HttpResponse()))( - handled( - response(equalTo(HttpResponse())), - ), - ) + (Get() ~> complete(HttpResponse())).map { res => + assertTrue(res.handled.get.response == HttpResponse()) + } }, test("a test using a directive and some checks") { val pinkHeader = RawHeader("Fancy", "pink") + val result = Get() ~> addHeader(pinkHeader) ~> { respondWithHeader(pinkHeader) { complete("abc") } } - assertM(result)( - handled( - status(equalTo(OK)) && - responseEntity(equalTo(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc"))) && - header("Fancy", isSome(equalTo(pinkHeader))), - ), - ) + result.map { res => + assertTrue( + res.handled.get.status == OK, + res.handled.get.entity == HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc"), + res.handled.get.header("Fancy").get == pinkHeader, + ) + } }, test("proper rejection collection") { val result = Post("/abc", "content") ~> { @@ -49,20 +47,24 @@ object AkkaZIOSpecDefaultSpec extends AkkaZIOSpecDefault { complete("naah") } } - assertM(result)(rejected(equalTo(List(MethodRejection(GET), MethodRejection(PUT))))) + + result.map { res => + assertTrue(res.rejected.get == List(MethodRejection(GET), MethodRejection(PUT))) + } }, test("separation of route execution from checking") { val pinkHeader = RawHeader("Fancy", "pink") case object Command - val result = for { + for { system <- ZIO.service[ActorSystem] service = TestProbe()(system) handler = TestProbe()(system) resultFiber <- { implicit def serviceRef: ActorRef = service.ref - implicit val askTimeout: Timeout = 1.second + + implicit val askTimeout: Timeout = 1.second Get() ~> pinkHeader ~> { respondWithHeader(pinkHeader) { @@ -75,14 +77,10 @@ object AkkaZIOSpecDefaultSpec extends AkkaZIOSpecDefault { handler.reply("abc") } res <- resultFiber.join - } yield res - - assertM(result)( - handled( - status(equalTo(OK)) && - responseEntity(equalTo(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc"))) && - header("Fancy", isSome(equalTo(pinkHeader))), - ), + } yield assertTrue( + res.handled.get.status == OK, + res.handled.get.entity == HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc"), + res.handled.get.header("Fancy").get == pinkHeader, ) }, test("internal server error") { @@ -90,11 +88,9 @@ object AkkaZIOSpecDefaultSpec extends AkkaZIOSpecDefault { throw new RuntimeException("BOOM") } - assertM(Get() ~> route)( - handled( - status(equalTo(InternalServerError)), - ), - ) + (Get() ~> route).map { res => + assertTrue(res.handled.get.status == InternalServerError) + } }, test("infinite response") { val pinkHeader = RawHeader("Fancy", "pink") @@ -105,12 +101,12 @@ object AkkaZIOSpecDefaultSpec extends AkkaZIOSpecDefault { } } - assertM(Get() ~> route)( - handled( - status(equalTo(OK)) && - header("Fancy", isSome(equalTo(pinkHeader))), - ), - ) + (Get() ?~> route).map { res => + assertTrue( + res.handled.get.status == OK, + res.handled.get.header("Fancy").get == pinkHeader, + ) + } }, ) } diff --git a/src/test/scala/zio/test/akkahttp/RouteZIOSpecDefaultSpec.scala b/src/test/scala/zio/test/akkahttp/RouteZIOSpecDefaultSpec.scala index 19ff290..5e7ba37 100644 --- a/src/test/scala/zio/test/akkahttp/RouteZIOSpecDefaultSpec.scala +++ b/src/test/scala/zio/test/akkahttp/RouteZIOSpecDefaultSpec.scala @@ -12,7 +12,6 @@ import akka.stream.scaladsl.Source import akka.testkit.TestProbe import akka.util.{ByteString, Timeout} import zio.ZIO -import zio.test.Assertion._ import zio.test._ import zio.test.akkahttp.assertions._ @@ -22,27 +21,26 @@ object RouteZIOSpecDefaultSpec extends ZIOSpecDefault { def spec = suite("RouteZIOSpecDefaultSpec")( test("the most simple and direct route test") { - assertM(Get() ~> complete(HttpResponse()))( - handled( - response(equalTo(HttpResponse())), - ), - ) + (Get() ~> complete(HttpResponse())).map { res => + assertTrue(res.handled.get.response == HttpResponse()) + } }, test("a test using a directive and some checks") { val pinkHeader = RawHeader("Fancy", "pink") + val result = Get() ~> addHeader(pinkHeader) ~> { respondWithHeader(pinkHeader) { complete("abc") } } - assertM(result)( - handled( - status(equalTo(OK)) && - responseEntity(equalTo(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc"))) && - header("Fancy", isSome(equalTo(pinkHeader))), - ), - ) + result.map { res => + assertTrue( + res.handled.get.status == OK, + res.handled.get.entity == HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc"), + res.handled.get.header("Fancy").get == pinkHeader, + ) + } }, test("proper rejection collection") { val result = Post("/abc", "content") ~> { @@ -50,14 +48,17 @@ object RouteZIOSpecDefaultSpec extends ZIOSpecDefault { complete("naah") } } - assertM(result)(rejected(equalTo(List(MethodRejection(GET), MethodRejection(PUT))))) + + result.map { res => + assertTrue(res.rejected.get == List(MethodRejection(GET), MethodRejection(PUT))) + } }, test("separation of route execution from checking") { val pinkHeader = RawHeader("Fancy", "pink") case object Command - val result = for { + for { system <- ZIO.service[ActorSystem] service = TestProbe()(system) handler = TestProbe()(system) @@ -77,14 +78,10 @@ object RouteZIOSpecDefaultSpec extends ZIOSpecDefault { handler.reply("abc") } res <- resultFiber.join - } yield res - - assertM(result)( - handled( - status(equalTo(OK)) && - responseEntity(equalTo(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc"))) && - header("Fancy", isSome(equalTo(pinkHeader))), - ), + } yield assertTrue( + res.handled.get.status == OK, + res.handled.get.entity == HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc"), + res.handled.get.header("Fancy").get == pinkHeader, ) }, test("internal server error") { @@ -92,11 +89,9 @@ object RouteZIOSpecDefaultSpec extends ZIOSpecDefault { throw new RuntimeException("BOOM") } - assertM(Get() ~> route)( - handled( - status(equalTo(InternalServerError)), - ), - ) + (Get() ~> route).map { res => + assertTrue(res.handled.get.status == InternalServerError) + } }, test("infinite response") { val pinkHeader = RawHeader("Fancy", "pink") @@ -107,12 +102,12 @@ object RouteZIOSpecDefaultSpec extends ZIOSpecDefault { } } - assertM(Get() ~> route)( - handled( - status(equalTo(OK)) && - header("Fancy", isSome(equalTo(pinkHeader))), - ), - ) + (Get() ?~> route).map { res => + assertTrue( + res.handled.get.status == OK, + res.handled.get.header("Fancy").get == pinkHeader, + ) + } }, ).provideShared(RouteTestEnvironment.environment) }