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