Bender Rodriguez
Bender Rodriguez

Reputation: 97

sttp/tapir - testing endpoint with JSON body

I have some tapir endpoints defined as part of a zio-http server. Everything works for real, including those POST endpoints with JSON bodies.

However, I've been unable to get a unit test using SttpBackendStub & TapirStubInterpreter to work for endpoints with a JSON body - if I remove the body from the endpoint and test request, it works fine. With the body in place, I get a 404, with no other error info.

The endpoint is defined like:

  case class Payload(one: String, two: String)

  @endpointInput("accounts/{id}/test-request")
  final case class RequestInput(
      @path
      id: String,
      @header("Source-Event-Timestamp")
      sourceEventTimestamp: LocalDateTime,
      @header("Accept")
      accept: String,
      @jsonbody
      payload: Payload
  )  

  val input = EndpointInput.derived[RequestInput]
  baseEndpoint.post.in(input)

I also experimented with using

.in(jsonBody[Input])

along with corresponding zio-json encoder & decoder, which surprisingly results in a different failure - a 400 with the message "Invalid value for: body."

Here is the relevant test code snippet (this is a ZIO spec):

        stub = TapirStubInterpreter(SttpBackendStub(new RIOMonadAsyncError[Any]))
          .whenServerEndpoint(endpoint)
          .thenRunLogic()
          .backend()
        body =
          """
            {
              "one": "",
              "two": ""
            }
          """.stripMargin
        response <- basicRequest
          .contentType("application/json")
          .body(body)
          .post(uri"http://test.com/test-request")
          .send(stub)

I've tried every permutation of calls/methods I can think of and just can't get the test to work for an endpoint with a body - again, bear in mind the real server does work with the exact same JSON body input.

Upvotes: 1

Views: 795

Answers (1)

adamw
adamw

Reputation: 8636

I think there's something missing from your example. I tried reproducing the problem, using the following code:

import sttp.tapir.EndpointIO.annotations.jsonbody
import sttp.tapir.ztapir._
import sttp.client3._
import sttp.client3.impl.zio.RIOMonadAsyncError
import sttp.client3.testing.SttpBackendStub
import sttp.tapir.{EndpointInput, Schema}
import sttp.tapir.server.stub.TapirStubInterpreter
import sttp.tapir.json.zio._
import zio.{Console, ZIO, ZIOAppDefault}
import zio.json.{DeriveJsonDecoder, DeriveJsonEncoder, JsonDecoder, JsonEncoder}

object TestWithJsonBodyUsingZioJson extends ZIOAppDefault {
  case class Payload(one: String, two: String)
  implicit val encoder: JsonEncoder[Payload] = DeriveJsonEncoder.gen[Payload]
  implicit val decoder: JsonDecoder[Payload] = DeriveJsonDecoder.gen[Payload]
  implicit val schema: Schema[Payload] = Schema.derived[Payload]

  case class RequestInput(@jsonbody payload: Payload)

  val input = EndpointInput.derived[RequestInput]
  val myEndpoint = endpoint.post.in(input).out(stringBody).zServerLogic(r => ZIO.succeed(s"Got request: $r"))

  val stub = TapirStubInterpreter(SttpBackendStub(new RIOMonadAsyncError[Any]))
    .whenServerEndpoint(myEndpoint)
    .thenRunLogic()
    .backend()
  val body = """
              {
                "one": "",
                "two": ""
              }
            """.stripMargin
  val response = basicRequest
    .contentType("application/json")
    .body(body)
    .post(uri"http://test.com/test-request")
    .send(stub)

  override def run = response.flatMap { r =>
    Console.printLine(r.toString())
  }
}

And I'm getting the expected result:

Response(Right(Got request: RequestInput(Payload(,))),200,,Vector(Content-Type: text/plain; charset=UTF-8),List(),RequestMetadata(POST,http://test.com/test-request,Vector(Accept-Encoding: gzip, deflate, Content-Type: application/json, Content-Length: 98)))

Maybe you can try to create a reproducible example?

Upvotes: 1

Related Questions