diff --git a/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/SttpMockServerClient.scala b/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/SttpMockServerClient.scala index 48aee0bdcc..1c90e789a4 100644 --- a/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/SttpMockServerClient.scala +++ b/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/SttpMockServerClient.scala @@ -11,6 +11,7 @@ import sttp.tapir.{CodecFormat, DecodeResult, Endpoint, RawBodyType, WebSocketBo import sttp.tapir.server.mockserver.impl.JsonCodecs._ import cats.syntax.either._ +import java.util.Base64 import java.nio.charset.Charset import io.circe.parser._ import sttp.tapir.capabilities.NoStreams @@ -160,7 +161,10 @@ object SttpMockServerClient { private def toExpectationBody(outputValues: OutputValues[Any]): Option[ExpectationBodyDefinition] = { for { - body <- outputValues.body.map(_.apply(Headers(outputValues.headers)).toString) + body <- outputValues.body.map(_.apply(Headers(outputValues.headers))).map { + case a: Array[Byte] => Base64.getEncoder.encodeToString(a) + case b => b.toString + } if body.nonEmpty contentTypeRaw <- outputValues.headers.find(_.name == HeaderNames.ContentType) contentType <- MediaType.parse(contentTypeRaw.value).toOption @@ -174,6 +178,8 @@ object SttpMockServerClient { json = decode[JsonObject](body).valueOr(throw _), // todo: probably it should not throw if tapir interprets correctly matchType = ExpectationBodyDefinition.JsonMatchType.Strict ) + case MediaType.ApplicationOctetStream => ExpectationBodyDefinition.BinaryBodyDefinition(body, MediaType.ApplicationOctetStream) + // Should Image Media Types also be binary? case other => ExpectationBodyDefinition.PlainBodyDefinition(body, other) } } diff --git a/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/impl/JsonCodecs.scala b/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/impl/JsonCodecs.scala index 7d12904578..37fb33762c 100644 --- a/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/impl/JsonCodecs.scala +++ b/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/impl/JsonCodecs.scala @@ -40,6 +40,9 @@ private[mockserver] object JsonCodecs { private implicit val plainBodyDefnEncoder: Encoder.AsObject[ExpectationBodyDefinition.PlainBodyDefinition] = deriveEncoder[ExpectationBodyDefinition.PlainBodyDefinition].mapJsonObject(_.add("type", ExpectationBodyDefinition.PlainType.asJson)) + private implicit val binaryBodyDefnEncoder: Encoder.AsObject[ExpectationBodyDefinition.BinaryBodyDefinition] = + deriveEncoder[ExpectationBodyDefinition.BinaryBodyDefinition].mapJsonObject(_.add("type", ExpectationBodyDefinition.BinaryType.asJson)) + private implicit val jsonBodyDefnEncoder: Encoder.AsObject[ExpectationBodyDefinition.JsonBodyDefinition] = deriveEncoder[ExpectationBodyDefinition.JsonBodyDefinition].mapJsonObject( _.add("type", ExpectationBodyDefinition.JsonType.asJson) @@ -47,9 +50,10 @@ private[mockserver] object JsonCodecs { private implicit val expectationBodyDefinitionEncoder: Encoder[ExpectationBodyDefinition] = Encoder[Json].contramap[ExpectationBodyDefinition] { - case plainDefn: ExpectationBodyDefinition.PlainBodyDefinition => plainBodyDefnEncoder(plainDefn) - case jsonDefn: ExpectationBodyDefinition.JsonBodyDefinition => jsonBodyDefnEncoder(jsonDefn) - case rawJson: ExpectationBodyDefinition.RawJson => rawJson.underlying.asJson + case plainDefn: ExpectationBodyDefinition.PlainBodyDefinition => plainBodyDefnEncoder(plainDefn) + case binaryDefn: ExpectationBodyDefinition.BinaryBodyDefinition => binaryBodyDefnEncoder(binaryDefn) + case jsonDefn: ExpectationBodyDefinition.JsonBodyDefinition => jsonBodyDefnEncoder(jsonDefn) + case rawJson: ExpectationBodyDefinition.RawJson => rawJson.underlying.asJson } private implicit val plainBodyDefnDecoder: Decoder[ExpectationBodyDefinition.PlainBodyDefinition] = @@ -58,14 +62,18 @@ private[mockserver] object JsonCodecs { private implicit val jsonBodyDefnDecoder: Decoder[ExpectationBodyDefinition.JsonBodyDefinition] = deriveDecoder[ExpectationBodyDefinition.JsonBodyDefinition] + private implicit val binaryBodyDefnDecoder: Decoder[ExpectationBodyDefinition.BinaryBodyDefinition] = + deriveDecoder[ExpectationBodyDefinition.BinaryBodyDefinition] + private implicit val expectationBodyDefinitionDecoder: Decoder[ExpectationBodyDefinition] = { Decoder[JsonObject] .emapTry { json => json("type") .flatMap(_.asString) .map { - case ExpectationBodyDefinition.PlainType => plainBodyDefnDecoder.decodeJson(json.asJson) - case ExpectationBodyDefinition.JsonType => jsonBodyDefnDecoder.decodeJson(json.asJson) + case ExpectationBodyDefinition.PlainType => plainBodyDefnDecoder.decodeJson(json.asJson) + case ExpectationBodyDefinition.JsonType => jsonBodyDefnDecoder.decodeJson(json.asJson) + case ExpectationBodyDefinition.BinaryType => binaryBodyDefnDecoder.decodeJson(json.asJson) case other => Left( DecodingFailure( diff --git a/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/model.scala b/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/model.scala index 9bae583f32..4b4f7aecea 100644 --- a/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/model.scala +++ b/server/sttp-mock-server/src/main/scala/sttp/tapir/server/mockserver/model.scala @@ -36,11 +36,13 @@ sealed trait ExpectationBodyDefinition extends Product with Serializable object ExpectationBodyDefinition { private[mockserver] val PlainType: String = "STRING" private[mockserver] val JsonType: String = "JSON" + private[mockserver] val BinaryType: String = "BINARY" - private[mockserver] val KnownTypes: List[String] = List(PlainType, JsonType) + private[mockserver] val KnownTypes: List[String] = List(PlainType, JsonType, BinaryType) private[mockserver] val KnownTypesString: String = KnownTypes.mkString("[", ", ", "]") case class PlainBodyDefinition(string: String, contentType: MediaType) extends ExpectationBodyDefinition + case class BinaryBodyDefinition(base64Bytes: String, contentType: MediaType) extends ExpectationBodyDefinition case class JsonBodyDefinition(json: JsonObject, matchType: JsonMatchType) extends ExpectationBodyDefinition // NOTE: for some reasons mock-server just returns the JSON body in httpResponse field... case class RawJson(underlying: JsonObject) extends ExpectationBodyDefinition diff --git a/server/sttp-mock-server/src/test/scala/sttp/tapir/server/mockserver/SttpMockServerClientSpec.scala b/server/sttp-mock-server/src/test/scala/sttp/tapir/server/mockserver/SttpMockServerClientSpec.scala index 166b847110..3187726343 100644 --- a/server/sttp-mock-server/src/test/scala/sttp/tapir/server/mockserver/SttpMockServerClientSpec.scala +++ b/server/sttp-mock-server/src/test/scala/sttp/tapir/server/mockserver/SttpMockServerClientSpec.scala @@ -46,6 +46,12 @@ class SttpMockServerClientSpec extends AnyFlatSpec with Matchers with BeforeAndA .errorOut(circe.jsonBody[ApiError]) .out(circe.jsonBody[PersonView]) + private val binaryEndpoint = endpoint + .in("api" / "v1" / "image") + .get + .errorOut(circe.jsonBody[ApiError]) + .out(rawBinaryBody(RawBodyType.ByteArrayBody)) + private val queryParameterEndpoint = endpoint .in("api" / "v1" / "person") .in(query[String]("name").and(query[Int]("age")).mapTo[CreatePersonCommand]) @@ -137,6 +143,32 @@ class SttpMockServerClientSpec extends AnyFlatSpec with Matchers with BeforeAndA actual shouldEqual Success(Value(Right(sampleOut))) } + it should "create binary payload expectation correctly" in { + val sampleOut = "This is some test text that should have some length".toCharArray.map(_.toByte) + + val actual = for { + _ <- mockServerClient + .whenInputMatches(binaryEndpoint)((), ()) + .thenSuccess(sampleOut) + + resp <- SttpClientInterpreter() + .toRequest(binaryEndpoint, baseUri = Some(baseUri)) + .apply(()) + .send(backend) + + _ <- mockServerClient + .verifyRequest(binaryEndpoint, VerificationTimes.exactlyOnce)((), ()) + } yield resp.body + + val s = actual match { + case Success(Value(Right(v))) => v + case _ => fail(s"Response doesnt have correct structure: ${actual}") + } + // Tests failed when unwrapping in shouldEqual - ScalaTest fails to compare wrapped byteArrays + + s shouldEqual sampleOut + } + it should "create error json expectation correctly" in { val sampleIn = CreatePersonCommand(name = "John", age = -1) val sampleErrorOut = ApiError(code = 1, message = "Invalid age")