Skip to content

Commit

Permalink
issue-1438: add more test cases
Browse files Browse the repository at this point in the history
  • Loading branch information
GaryAghedo committed Aug 6, 2024
1 parent 0f9e5f1 commit 4830859
Show file tree
Hide file tree
Showing 6 changed files with 208 additions and 35 deletions.
2 changes: 1 addition & 1 deletion modules/core/src/smithy4s/http/HttpContractError.scala
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ object HttpPayloadError {
}

sealed trait MetadataError extends HttpContractError {
import MetadataError.*
import MetadataError._

override def getMessage(): String = this match {
case NotFound(field, location) =>
Expand Down
25 changes: 14 additions & 11 deletions modules/core/src/smithy4s/http/HttpResponse.scala
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,12 @@ object HttpResponse {
response.statusCode,
response.headers,
bodyBlob.toUTF8String,
Some(FailedDecodeAttempt(
discriminator = discriminator,
contractError = error
))
Some(
FailedDecodeAttempt(
discriminator = discriminator,
contractError = error
)
)
)
)
case otherError => F.raiseError(otherError)
Expand All @@ -263,14 +265,15 @@ object HttpResponse {
body = bodyBlob.toUTF8String,
failedDecodeAttempt = Some(
FailedDecodeAttempt(
discriminator = HttpDiscriminator.StatusCode(response.statusCode),
contractError = HttpPayloadError(
path = smithy4s.codecs.PayloadPath(List()),
expected = "JSON",
message = "Unknown error due to unrecognised discriminator"
discriminator =
HttpDiscriminator.StatusCode(response.statusCode),
contractError = HttpPayloadError(
path = smithy4s.codecs.PayloadPath(List()),
expected = "object",
message =
"Unknown error due to unrecognised discriminator"
)
)
)

)
)
)
Expand Down
23 changes: 22 additions & 1 deletion modules/http4s/test/src/smithy4s/http4s/Http4sPizzaSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ package smithy4s.http4s
import cats.effect.IO
import cats.effect.Resource
import org.http4s.Uri
import org.http4s.client.Client
import org.http4s.Response
import org.http4s.implicits._
import org.http4s.client.Client
import smithy4s.example.PizzaAdminService

object Http4sPizzaSpec extends smithy4s.tests.PizzaSpec {
Expand All @@ -40,4 +41,24 @@ object Http4sPizzaSpec extends smithy4s.tests.PizzaSpec {

}

def runServerWithClient(
pizzaService: PizzaAdminService[IO],
clientResponse: Response[IO]
): Resource[IO, Res] = {
SimpleRestJsonBuilder
.routes(
SimpleRestJsonBuilder(PizzaAdminService)
.client[IO](Client(_ => Resource.pure(clientResponse)))
.make
.toTry
.get
)
.resource
.map { httpRoutes =>
val client = Client.fromHttpApp(httpRoutes.orNotFound)
val uri = Uri.unsafeFromString("http://localhost")
(client, uri)
}
}

}
14 changes: 13 additions & 1 deletion modules/tests/src/smithy4s/tests/PizzaAdminServiceImpl.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package smithy4s.tests
import cats.effect._
import cats.effect.std.UUIDGen
import cats.implicits._
import org.http4s.Response
import smithy4s.Timestamp
import smithy4s.example._
import smithy4s.tests.PizzaAdminServiceImpl._
Expand All @@ -27,7 +28,18 @@ import java.util.UUID

object PizzaAdminServiceImpl {
case class Item(food: Food, price: Float, addedAt: Timestamp)
case class State(restaurants: Map[String, Restaurant])
case class State(
restaurants: Map[String, Restaurant],
responses: Map[String, Response[IO]] = Map.empty
) {
def prepResponse(key: String, response: Response[IO]): State =
copy(responses = responses.updated(key, response))

def getResponse(key: String): IO[Response[IO]] =
responses
.get(key)
.liftTo[IO](new Throwable(s"Response not found for key: $key"))
}
case class Restaurant(menu: Map[UUID, Item])

case object Boom extends Throwable with scala.util.control.NoStackTrace
Expand Down
46 changes: 26 additions & 20 deletions modules/tests/src/smithy4s/tests/PizzaClientSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,16 @@ abstract class PizzaClientSpec extends IOSuite {
407,
Map("Content-Length" -> "42", "Content-Type" -> "application/json"),
"""{"message":"generic client error message"}""",
Some(FailedDecodeAttempt(
discriminator = HttpDiscriminator.StatusCode(407),
contractError = HttpPayloadError(
path = smithy4s.codecs.PayloadPath(List()),
expected = "JSON",
message = "Unknown error due to unrecognised discriminator"
Some(
FailedDecodeAttempt(
discriminator = HttpDiscriminator.StatusCode(407),
contractError = HttpPayloadError(
path = smithy4s.codecs.PayloadPath(List()),
expected = "object",
message = "Unknown error due to unrecognised discriminator"
)
)
))
)
)
)

Expand All @@ -141,25 +143,29 @@ abstract class PizzaClientSpec extends IOSuite {
.withEntity("goodbye world"),
rawErrorResponse(
500,
Map("Content-Length" -> "13", "Content-Type" -> "text/plain; charset=UTF-8"),
Map(
"Content-Length" -> "13",
"Content-Type" -> "text/plain; charset=UTF-8"
),
"goodbye world",
Some(FailedDecodeAttempt(
discriminator = HttpDiscriminator.StatusCode(500),
contractError = HttpPayloadError(
path = smithy4s.codecs.PayloadPath(List()),
expected = "JSON",
message = "Unknown error due to unrecognised discriminator"
Some(
FailedDecodeAttempt(
discriminator = HttpDiscriminator.StatusCode(500),
contractError = HttpPayloadError(
path = smithy4s.codecs.PayloadPath(List()),
expected = "object",
message = "Unknown error due to unrecognised discriminator"
)
)
))
)
)
)


private def rawErrorResponse(
code: Int,
headers: Map[String, String],
body: String,
failedDecodeAttempt: Option[FailedDecodeAttempt]
code: Int,
headers: Map[String, String],
body: String,
failedDecodeAttempt: Option[FailedDecodeAttempt]
): RawErrorResponse =
RawErrorResponse(
code,
Expand Down
133 changes: 132 additions & 1 deletion modules/tests/src/smithy4s/tests/PizzaSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -19,17 +19,22 @@ package smithy4s.tests
import cats.data.NonEmptyList
import cats.effect._
import cats.syntax.all._

import io.circe._
import org.http4s.Request
import org.http4s._
import org.http4s.Uri
import org.http4s.circe._
import org.http4s.client.Client
import org.typelevel.ci.CIString
import org.http4s.client.dsl.Http4sClientDsl
import org.http4s.dsl.Http4sDsl
import org.http4s.Response
import smithy4s.http.HttpPayloadError
import smithy4s.example.PizzaAdminService
import smithy4s.http.CaseInsensitive
import smithy4s.http.HttpContractError
import smithy4s.http.HttpDiscriminator
import smithy4s.http.RawErrorResponse
import weaver._
import cats.Show
import org.http4s.EntityDecoder
Expand All @@ -44,6 +49,11 @@ abstract class PizzaSpec
errorAdapter: PartialFunction[Throwable, Throwable]
): Resource[IO, Res]

def runServerWithClient(
pizzaService: PizzaAdminService[IO],
clientResponse: Response[IO]
): Resource[IO, Res]

val pizzaItem = Json.obj(
"pizza" -> Json.obj(
"name" -> Json.fromString("margharita"),
Expand Down Expand Up @@ -463,6 +473,127 @@ abstract class PizzaSpec
}
}

routerTest("Negative: handle error without discriminator") {
(client, uri, log) =>
val badResponse = org.http4s.Response[IO](
status = Status.InternalServerError,
body = fs2.Stream.emits("malformed body".getBytes),
headers = Headers(
Header.Raw(CIString("Content-Length"), "14"),
Header.Raw(CIString("Content-Type"), "text/plain")
)
)

for {
stateRef <- IO.ref(PizzaAdminServiceImpl.State(Map.empty))
impl = new PizzaAdminServiceImpl(stateRef)
response <- runServerWithClient(
impl,
badResponse
).use { case (client, uri) =>
val request = POST(
menuItem,
uri / "restaurant" / "boom" / "menu" / "item"
)
client
.run(request)
.use { response =>
IO.pure(response.status.code)
}
.attempt
}

} yield {
val httpPayloadError = HttpPayloadError(
path = smithy4s.codecs.PayloadPath(List()),
expected = "object",
message = "Unknown error due to unrecognised discriminator"
)

val expectHeaders = Map(
CaseInsensitive("Content-Type") -> List("text/plain"),
CaseInsensitive("Content-Length") -> List("14")
)
response match {
case Left(
RawErrorResponse(code, headers, body, Some(failedDecodeAttempt))
) =>
expect(code == 500) &&
expect(headers == expectHeaders) &&
expect(body.contains("malformed body")) &&
expect(
failedDecodeAttempt.discriminator == HttpDiscriminator
.StatusCode(500)
) &&
expect(failedDecodeAttempt.contractError == httpPayloadError)
case _ =>
failure("Expected RawErrorResponse with status 500")
}
}
}

routerTest(
"Negative: handle decoder exception for known error with discriminator"
) { (client, uri, log) =>
val badResponse = org.http4s.Response[IO](
status = Status.InternalServerError,
body = fs2.Stream.emits("malformed body".getBytes),
headers = Headers(
Header.Raw(CIString("Content-Length"), "14"),
Header.Raw(CIString("Content-Type"), "text/plain"),
Header.Raw(CIString("X-Error-Type"), "GenericServerError")
)
)

for {
stateRef <- IO.ref(PizzaAdminServiceImpl.State(Map.empty))
impl = new PizzaAdminServiceImpl(stateRef)
response <- runServerWithClient(
impl,
badResponse
).use { case (client, uri) =>
val request = POST(
menuItem,
uri / "restaurant" / "boom" / "menu" / "item"
)
client
.run(request)
.use { response =>
IO.pure(response.status.code)
}
.attempt

}

} yield {
val expectHeaders = Map(
CaseInsensitive("Content-Length") -> List("14"),
CaseInsensitive("Content-Type") -> List("text/plain"),
CaseInsensitive("X-Error-Type") -> List("GenericServerError")
)
response match {
case Left(
RawErrorResponse(code, headers, body, Some(failedDecodeAttempt))
) =>
expect(code == 500) &&
expect(headers == expectHeaders) &&
expect(body.contains("malformed body")) &&
expect(
failedDecodeAttempt.discriminator == HttpDiscriminator.NameOnly(
"GenericServerError"
)
) &&
expect(
failedDecodeAttempt.contractError.getMessage.contains(
"Expected JSON object, offset: 0x00000000, buf:"
)
)
case _ =>
failure("Expected RawErrorResponse with status 500")
}
}
}

type Res = (Client[IO], Uri)
def sharedResource: Resource[IO, (Client[IO], Uri)] = for {
stateRef <- Resource.eval(
Expand Down

0 comments on commit 4830859

Please sign in to comment.