Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update testkit to use common DSL (#2559) #2601

Merged
merged 1 commit into from
Jan 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading