Search code examples
jsonscalaziotapirsttp

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.


Solution

  • 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?