Skip to content

Commit

Permalink
Update testkit to use common DSL (#2559) (#2601)
Browse files Browse the repository at this point in the history
  • Loading branch information
987Nabil committed Jan 7, 2024
1 parent d297871 commit 12f6b99
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 151 deletions.
88 changes: 48 additions & 40 deletions zio-http-cli/src/test/scala/zio/http/endpoint/cli/CliSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,9 @@ import zio.test._

import zio.schema._

import zio.http._
import zio.http.codec._
import zio.http.endpoint._
import zio.http.endpoint.cli.CliRepr._
import zio.http.{Request, _}

/**
* Test suite for Http CliApp. It tests:
Expand Down Expand Up @@ -47,46 +46,55 @@ object CliSpec extends ZIOSpecDefault {
val testClient: ZLayer[Any, Nothing, TestClient & Client] =
ZLayer.scopedEnvironment {
for {
behavior <- Ref.make[PartialFunction[Request, ZIO[Any, Response, Response]]](PartialFunction.empty)
behavior <- Ref.make[Routes[Any, Response]](Routes.empty)
socketBehavior <- Ref.make[WebSocketApp[Any]](WebSocketApp(Handler.unit))
driver = TestClient(behavior, socketBehavior)
_ <- driver.addHandler {
case Request(_, Method.GET, URL(path, _, _, _), _, _, _) if path.encode == "/fromURL" =>
ZIO.succeed(Response.text("342.76"))
case Request(_, Method.GET, _, headers, body, _)
if headers.headOption.map(_.renderedValue) == Some("fromURL") =>
ZIO.succeed(Response(Status.Ok, headers, body))
case Request(_, Method.GET, _, _, body, _) =>
for {
text <- body.asMultipartForm
.map(_.formData)
.map(_.map(_.stringValue.toString()))
.map(_.toString())
.mapError(e => Response.error(Status.BadRequest, e.getMessage()))
} yield if (text == "Chunk(Some(342.76))") Response.text("received 1") else Response.text(text)
case Request(_, Method.POST, _, _, body, _) =>
for {
text <- body.asMultipartForm
.map(_.formData)
.map(_.map(_.stringValue.toString()))
.map(_.toString())
.mapError(e => Response.error(Status.BadRequest, e.getMessage()))
response <-
if (text == """Chunk(Some(342.76),Some("sample"))""") ZIO.succeed("received 2")
else ZIO.succeed(text)
} yield Response.text(response)
case Request(_, Method.PUT, _, _, body, _) =>
for {
text <- body.asMultipartForm
.map(_.formData)
.map(_.map(_.stringValue.toString()))
.map(_.toString())
.mapError(e => Response.error(Status.BadRequest, e.getMessage()))
response <-
if (text == "Chunk(Some(342))") ZIO.succeed("received 3")
else ZIO.succeed(text)
} yield Response.text(response)
case _ => ZIO.succeed(Response.text("not received"))
_ <- driver.addRoutes {
Routes(
Method.GET / "fromURL" -> handler(Response.text("342.76")),
Method.GET / trailing -> handler { (_: Path, request: Request) =>
val headers = request.headers
val body = request.body
if (headers.headOption.map(_.renderedValue).contains("fromURL"))
ZIO.succeed(Response(Status.Ok, headers, body))
else {
for {
text <- body.asMultipartForm
.map(_.formData)
.map(_.map(_.stringValue.toString()))
.map(_.toString())
.mapError(e => Response.error(Status.BadRequest, e.getMessage))
} yield if (text == "Chunk(Some(342.76))") Response.text("received 1") else Response.text(text)
}
},
Method.POST / trailing -> handler { (req: Request) =>
val body = req.body
for {
text <- body.asMultipartForm
.map(_.formData)
.map(_.map(_.stringValue.toString()))
.map(_.toString())
.mapError(e => Response.error(Status.BadRequest, e.getMessage))
response <-
if (text == """Chunk(Some(342.76),Some("sample"))""") ZIO.succeed("received 2")
else ZIO.succeed(text)
} yield Response.text(response)
},
Method.PUT / trailing -> handler { (req: Request) =>
val body = req.body
for {
text <- body.asMultipartForm
.map(_.formData)
.map(_.map(_.stringValue.toString()))
.map(_.toString())
.mapError(e => Response.error(Status.BadRequest, e.getMessage))
response <-
if (text == "Chunk(Some(342))") ZIO.succeed("received 3")
else ZIO.succeed(text)
} yield Response.text(response)
},
Method.ANY / trailing -> handler(Response.text("not received")),
)
}
} yield ZEnvironment[TestClient, Client](driver, ZClient.fromDriver(driver))
}
Expand Down
127 changes: 80 additions & 47 deletions zio-http-testkit/src/main/scala/zio/http/TestClient.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package zio.http

import zio._

import zio.http.ChannelEvent.{Unregistered, UserEvent}
import zio.http.{Headers, Method, Scheme, Status, Version}

/**
* Enables tests that use a client without needing a live Server
*
Expand All @@ -13,7 +10,7 @@ import zio.http.{Headers, Method, Scheme, Status, Version}
* Server
*/
final case class TestClient(
behavior: Ref[PartialFunction[Request, ZIO[Any, Response, Response]]],
behavior: Ref[Routes[Any, Response]],
serverSocketBehavior: Ref[WebSocketApp[Any]],
) extends ZClient.Driver[Any, Throwable] {

Expand All @@ -33,45 +30,66 @@ final case class TestClient(
expectedRequest: Request,
response: Response,
): ZIO[Any, Nothing, Unit] = {
val handler = new PartialFunction[Request, ZIO[Any, Response, Response]] {

def isDefinedAt(realRequest: Request): Boolean = {
// The way that the Client breaks apart and re-assembles the request prevents a straightforward
// expectedRequest == realRequest
val defined = expectedRequest.url.relative == realRequest.url &&
expectedRequest.method == realRequest.method &&
expectedRequest.headers.toSet.forall(expectedHeader => realRequest.headers.toSet.contains(expectedHeader))

defined
}

def apply(request: Request): ZIO[Any, Response, Response] =
if (!isDefinedAt(request))
throw new MatchError(s"TestClient received unexpected request: $request (expected: $expectedRequest)")
else ZIO.succeed(response)

def isDefinedAt(realRequest: Request): Boolean = {
// The way that the Client breaks apart and re-assembles the request prevents a straightforward
// expectedRequest == realRequest
expectedRequest.url.relative == realRequest.url &&
expectedRequest.method == realRequest.method &&
expectedRequest.headers.toSet.forall(expectedHeader => realRequest.headers.toSet.contains(expectedHeader))
}
addHandler(handler)
addRoute(RoutePattern(expectedRequest.method, expectedRequest.path) -> handler { (realRequest: Request) =>
if (!isDefinedAt(realRequest))
throw new MatchError(s"TestClient received unexpected request: $realRequest (expected: $expectedRequest)")
else response
})
}

/**
* Adds a flexible handler for requests that are submitted by test cases
* @param handler
* New behavior to be added to the TestClient
* Adds a route definition to handle requests that are submitted by test cases
* @param route
* New route to be added to the TestClient
* @tparam R
* Environment of the new handler's effect.
* Environment of the new route
*
* @example
* {{{
* TestClient.addHandler{case request if request.method == Method.GET => ZIO.succeed(Response.ok)}
* TestClient.addRoute { Method.ANY / trailing -> handler(Response.ok) }
* }}}
*/
def addHandler[R](
handler: PartialFunction[Request, ZIO[R, Response, Response]],
def addRoute[R](
route: Route[R, Response],
): ZIO[R, Nothing, Unit] =
for {
r <- ZIO.environment[R]
newBehavior = handler.andThen(_.provideEnvironment(r))
_ <- behavior.update(_.orElse(newBehavior))
provided = route.provideEnvironment(r)
_ <- behavior.update(_ :+ provided)
} yield ()

/**
* Adds routes to handle requests that are submitted by test cases
* @param routes
* New routes to be added to the TestClient
* @tparam R
* Environment of the new route
*
* @example
* {{{
* TestClient.addRoutes {
* Routes(
* Method.GET / trailing -> handler { Response.text("fallback") },
* Method.GET / "hello" / "world" -> handler { Response.text("Hey there!") },
* )
* }
* }}}
*/
def addRoutes[R](
routes: Routes[R, Response],
): ZIO[R, Nothing, Unit] =
for {
r <- ZIO.environment[R]
provided = routes.provideEnvironment(r)
_ <- behavior.update(_ ++ provided)
} yield ()

def headers: Headers = Headers.empty
Expand All @@ -93,12 +111,8 @@ final case class TestClient(
sslConfig: Option[zio.http.ClientSSLConfig],
proxy: Option[Proxy],
)(implicit trace: Trace): ZIO[Any, Throwable, Response] = {
val notFound: PartialFunction[Request, ZIO[Any, Response, Response]] = { case _: Request =>
ZIO.succeed(Response.notFound)
}

for {
currentBehavior <- behavior.get.map(_.orElse(notFound))
currentBehavior <- behavior.get.map(_ :+ Method.ANY / trailing -> handler(Response.notFound))
request = Request(
body = body,
headers = headers,
Expand All @@ -107,9 +121,6 @@ final case class TestClient(
version = version,
remoteAddress = None,
)
_ <- ZIO.when(!currentBehavior.isDefinedAt(request)) {
ZIO.fail(new Throwable(s"TestClient does not have a handler for $request"))
}
response <- currentBehavior(request).merge
} yield response
}
Expand Down Expand Up @@ -166,21 +177,43 @@ object TestClient {
ZIO.serviceWithZIO[TestClient](_.addRequestResponse(request, response))

/**
* Adds a flexible handler for requests that are submitted by test cases
* @param handler
* New behavior to be added to the TestClient
* Adds a route definition to handle requests that are submitted by test cases
* @param route
* New route to be added to the TestClient
* @tparam R
* Environment of the new route
*
* @example
* {{{
* TestClient.addRoute { Method.ANY / trailing -> handler(Response.ok) }
* }}}
*/
def addRoute[R](
route: Route[R, Response],
): ZIO[R with TestClient, Nothing, Unit] =
ZIO.serviceWithZIO[TestClient](_.addRoute(route))

/**
* Adds routes to handle requests that are submitted by test cases
* @param routes
* New routes to be added to the TestClient
* @tparam R
* Environment of the new handler's effect.
* Environment of the new route
*
* @example
* {{{
* TestClient.addHandler{case request if request.method == Method.GET => ZIO.succeed(Response.ok)}
* TestClient.addRoutes {
* Routes(
* Method.GET / trailing -> handler { Response.text("fallback") },
* Method.GET / "hello" / "world" -> handler { Response.text("Hey there!") },
* )
* }
* }}}
*/
def addHandler[R](
handler: PartialFunction[Request, ZIO[R, Response, Response]],
def addRoutes[R](
routes: Routes[R, Response],
): ZIO[R with TestClient, Nothing, Unit] =
ZIO.serviceWithZIO[TestClient](_.addHandler(handler))
ZIO.serviceWithZIO[TestClient](_.addRoutes(routes))

def installSocketApp(
app: WebSocketApp[Any],
Expand All @@ -190,7 +223,7 @@ object TestClient {
val layer: ZLayer[Any, Nothing, TestClient & Client] =
ZLayer.scopedEnvironment {
for {
behavior <- Ref.make[PartialFunction[Request, ZIO[Any, Response, Response]]](PartialFunction.empty)
behavior <- Ref.make[Routes[Any, Response]](Routes.empty)
socketBehavior <- Ref.make[WebSocketApp[Any]](WebSocketApp.unit)
driver = TestClient(behavior, socketBehavior)
} yield ZEnvironment[TestClient, Client](driver, ZClient.fromDriver(driver))
Expand Down
Loading

0 comments on commit 12f6b99

Please sign in to comment.