Skip to content

Commit

Permalink
Merge pull request #154 from senia-psm/zio_rc6
Browse files Browse the repository at this point in the history
update zio to 2.0.0-RC6
  • Loading branch information
senia-psm authored Jun 11, 2022
2 parents 21c246d + e5d7a99 commit ed3e8ac
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 269 deletions.
48 changes: 21 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
```

Expand All @@ -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)
}
Expand All @@ -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())
}
}
)
}
Expand All @@ -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`.
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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,
)
}
```


Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
2 changes: 1 addition & 1 deletion project/BuildHelper.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 9 additions & 12 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
@@ -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")
2 changes: 1 addition & 1 deletion project/project/sbt-updates.sbt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.2")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.3")
2 changes: 1 addition & 1 deletion project/sbt-updates.sbt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.2")
addSbtPlugin("com.timushev.sbt" % "sbt-updates" % "0.6.3")
33 changes: 15 additions & 18 deletions src/main/scala/akka/http/expose/ExposedRouteTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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}

Expand All @@ -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]
Expand Down Expand Up @@ -67,11 +51,24 @@ 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)
.map(_.getOrElse(RouteTestResult.Timeout))
}
} 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
}
}
10 changes: 4 additions & 6 deletions src/main/scala/zio/test/akkahttp/AkkaZIOSpecDefault.scala
Original file line number Diff line number Diff line change
@@ -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]
}
126 changes: 14 additions & 112 deletions src/main/scala/zio/test/akkahttp/RouteTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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)))
}
}

Expand All @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading

0 comments on commit ed3e8ac

Please sign in to comment.