diff options
author | Adam Warski <adam@warski.org> | 2018-05-21 14:28:29 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-05-21 14:28:29 +0200 |
commit | b7de29680d64c8465ba9b612cb9d903cbbc12291 (patch) | |
tree | 0d7a6b1c653393de8422e9704b9e68bb5cf7ed91 /core/src/test/scala/com/softwaremill/sttp/testing | |
parent | 588395d018c258eb74f60ad95bad706698bdf915 (diff) | |
parent | ccb1afe90e938fc2b8619dd960a1df1937f212be (diff) | |
download | sttp-master.tar.gz sttp-master.tar.bz2 sttp-master.zip |
Move backend tests into their projects
Diffstat (limited to 'core/src/test/scala/com/softwaremill/sttp/testing')
-rw-r--r-- | core/src/test/scala/com/softwaremill/sttp/testing/ConvertToFuture.scala (renamed from core/src/test/scala/com/softwaremill/sttp/testing/streaming/ConvertToFuture.scala) | 2 | ||||
-rw-r--r-- | core/src/test/scala/com/softwaremill/sttp/testing/CustomMatchers.scala | 22 | ||||
-rw-r--r-- | core/src/test/scala/com/softwaremill/sttp/testing/EvalScala.scala | 11 | ||||
-rw-r--r-- | core/src/test/scala/com/softwaremill/sttp/testing/ForceWrapped.scala | 30 | ||||
-rw-r--r-- | core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala | 524 | ||||
-rw-r--r-- | core/src/test/scala/com/softwaremill/sttp/testing/TestHttpServer.scala | 236 | ||||
-rw-r--r-- | core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala | 76 | ||||
-rw-r--r-- | core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala | 15 |
8 files changed, 900 insertions, 16 deletions
diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/ConvertToFuture.scala b/core/src/test/scala/com/softwaremill/sttp/testing/ConvertToFuture.scala index 9438890..25a7d8e 100644 --- a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/ConvertToFuture.scala +++ b/core/src/test/scala/com/softwaremill/sttp/testing/ConvertToFuture.scala @@ -1,4 +1,4 @@ -package com.softwaremill.sttp.testing.streaming +package com.softwaremill.sttp.testing import com.softwaremill.sttp.Id import scala.concurrent.Future diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/CustomMatchers.scala b/core/src/test/scala/com/softwaremill/sttp/testing/CustomMatchers.scala new file mode 100644 index 0000000..a6984c8 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/CustomMatchers.scala @@ -0,0 +1,22 @@ +package com.softwaremill.sttp.testing + +import java.nio.file.{Files, Paths} +import java.{io, util} + +import org.scalatest.matchers.{MatchResult, Matcher} + +object CustomMatchers { + class FileContentsMatch(file: java.io.File) extends Matcher[java.io.File] { + override def apply(left: io.File): MatchResult = { + val inBA = Files.readAllBytes(Paths.get(left.getAbsolutePath)) + val expectedBA = Files.readAllBytes(Paths.get(file.getAbsolutePath)) + MatchResult( + util.Arrays.equals(inBA, expectedBA), + "The files' contents are not the same", + "The files' contents are the same" + ) + } + } + + def haveSameContentAs(file: io.File) = new FileContentsMatch(file) +} diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/EvalScala.scala b/core/src/test/scala/com/softwaremill/sttp/testing/EvalScala.scala new file mode 100644 index 0000000..255755d --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/EvalScala.scala @@ -0,0 +1,11 @@ +package com.softwaremill.sttp.testing + +object EvalScala { + import scala.tools.reflect.ToolBox + + def apply(code: String): Any = { + val m = scala.reflect.runtime.currentMirror + val tb = m.mkToolBox() + tb.eval(tb.parse(code)) + } +} diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/ForceWrapped.scala b/core/src/test/scala/com/softwaremill/sttp/testing/ForceWrapped.scala new file mode 100644 index 0000000..f73833f --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/ForceWrapped.scala @@ -0,0 +1,30 @@ +package com.softwaremill.sttp.testing + +import org.scalatest.Suite +import org.scalatest.concurrent.{PatienceConfiguration, ScalaFutures} +import org.scalatest.exceptions.TestFailedException + +import scala.concurrent.Future +import scala.concurrent.duration._ +import scala.language.higherKinds + +trait ForceWrapped extends ScalaFutures with TestingPatience { this: Suite => + + implicit class ForceDecorator[R[_], T](wrapped: R[T]) { + def toFuture()(implicit ctf: ConvertToFuture[R]): Future[T] = + ctf.toFuture(wrapped) + + def force()(implicit ctf: ConvertToFuture[R]): T = { + try { + ctf.toFuture(wrapped).futureValue + } catch { + case e: TestFailedException if e.getCause != null => throw e.getCause + } + } + } +} + +trait TestingPatience extends PatienceConfiguration { + override implicit val patienceConfig: PatienceConfig = + PatienceConfig(timeout = 5.seconds, interval = 150.milliseconds) +} diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala b/core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala new file mode 100644 index 0000000..5598aa2 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala @@ -0,0 +1,524 @@ +package com.softwaremill.sttp.testing + +import java.io.{ByteArrayInputStream, IOException} +import java.nio.ByteBuffer +import java.nio.file.Paths +import java.time.{ZoneId, ZonedDateTime} + +import better.files._ +import com.softwaremill.sttp._ +import com.softwaremill.sttp.testing.CustomMatchers._ +import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} +import org.scalatest.{BeforeAndAfterAll, BeforeAndAfterEach, FreeSpec, Matchers, OptionValues} + +import scala.concurrent.duration._ +import scala.language.higherKinds + +trait HttpTest[R[_]] + extends FreeSpec + with Matchers + with ForceWrapped + with ScalaFutures + with OptionValues + with IntegrationPatience + with BeforeAndAfterEach + with BeforeAndAfterAll + with TestHttpServer { + + override def afterEach() { + val file = File(outPath) + if (file.exists) file.delete() + } + + private val textFile = new java.io.File(getClass.getResource("/textfile.txt").getFile) + private val binaryFile = new java.io.File(getClass.getResource("/binaryfile.jpg").getFile) + private val outPath = File.newTemporaryDirectory().path + private val textWithSpecialCharacters = "Żółć!" + + implicit val backend: SttpBackend[R, Nothing] + implicit val convertToFuture: ConvertToFuture[R] + + private def postEcho = sttp.post(uri"$endpoint/echo") + private val testBody = "this is the body" + private val testBodyBytes = testBody.getBytes("UTF-8") + private val expectedPostEchoResponse = "POST /echo this is the body" + + private val sttpIgnore = com.softwaremill.sttp.ignore + + "parse response" - { + "as string" in { + val response = postEcho.body(testBody).send().force() + response.unsafeBody should be(expectedPostEchoResponse) + } + + "as string with mapping using map" in { + val response = postEcho + .body(testBody) + .response(asString.map(_.length)) + .send() + .force() + response.unsafeBody should be(expectedPostEchoResponse.length) + } + + "as string with mapping using mapResponse" in { + val response = postEcho + .body(testBody) + .mapResponse(_.length) + .send() + .force() + response.unsafeBody should be(expectedPostEchoResponse.length) + } + + "as a byte array" in { + val response = + postEcho.body(testBody).response(asByteArray).send().force() + val fc = new String(response.unsafeBody, "UTF-8") + fc should be(expectedPostEchoResponse) + } + + "as parameters" in { + val params = List("a" -> "b", "c" -> "d", "e=" -> "&f") + val response = sttp + .post(uri"$endpoint/echo/form_params/as_params") + .body(params: _*) + .response(asParams) + .send() + .force() + response.unsafeBody.toList should be(params) + } + } + + "parameters" - { + "make a get request with parameters" in { + val response = sttp + .get(uri"$endpoint/echo?p2=v2&p1=v1") + .send() + .force() + + response.unsafeBody should be("GET /echo p1=v1 p2=v2") + } + } + + "body" - { + "post a string" in { + val response = postEcho.body(testBody).send().force() + response.unsafeBody should be(expectedPostEchoResponse) + } + + "post a byte array" in { + val response = + postEcho.body(testBodyBytes).send().force() + response.unsafeBody should be(expectedPostEchoResponse) + } + + "post an input stream" in { + val response = postEcho + .body(new ByteArrayInputStream(testBodyBytes)) + .send() + .force() + response.unsafeBody should be(expectedPostEchoResponse) + } + + "post a byte buffer" in { + val response = postEcho + .body(ByteBuffer.wrap(testBodyBytes)) + .send() + .force() + response.unsafeBody should be(expectedPostEchoResponse) + } + + "post a file" in { + val f = File.newTemporaryFile().write(testBody) + try { + val response = postEcho.body(f.toJava).send().force() + response.unsafeBody should be(expectedPostEchoResponse) + } finally f.delete() + } + + "post a path" in { + val f = File.newTemporaryFile().write(testBody) + try { + val response = + postEcho.body(f.toJava.toPath).send().force() + response.unsafeBody should be(expectedPostEchoResponse) + } finally f.delete() + } + + "post form data" in { + val response = sttp + .post(uri"$endpoint/echo/form_params/as_string") + .body("a" -> "b", "c" -> "d") + .send() + .force() + response.unsafeBody should be("a=b c=d") + } + + "post form data with special characters" in { + val response = sttp + .post(uri"$endpoint/echo/form_params/as_string") + .body("a=" -> "/b", "c:" -> "/d") + .send() + .force() + response.unsafeBody should be("a==/b c:=/d") + } + + "post without a body" in { + val response = postEcho.send().force() + response.unsafeBody should be("POST /echo") + } + } + + "headers" - { + def getHeaders = sttp.get(uri"$endpoint/set_headers") + + "read response headers" in { + val response = getHeaders.response(sttpIgnore).send().force() + response.headers should have length (6) + response.headers("Cache-Control").toSet should be(Set("no-cache", "max-age=1000")) + response.header("Server") should be('defined) + response.header("server") should be('defined) + response.header("Server").get should startWith("akka-http") + response.contentType should be(Some("text/plain; charset=UTF-8")) + response.contentLength should be(Some(2L)) + } + } + + "errors" - { + def getHeaders = sttp.post(uri"$endpoint/set_headers") + + "return 405 when method not allowed" in { + val response = getHeaders.response(sttpIgnore).send().force() + response.code should be(405) + response.isClientError should be(true) + response.body should be('left) + } + } + + "cookies" - { + "read response cookies" in { + val response = + sttp + .get(uri"$endpoint/set_cookies") + .response(sttpIgnore) + .send() + .force() + response.cookies should have length (3) + response.cookies.toSet should be( + Set( + Cookie("cookie1", "value1", secure = true, httpOnly = true, maxAge = Some(123L)), + Cookie("cookie2", "value2"), + Cookie("cookie3", "", domain = Some("xyz"), path = Some("a/b/c")) + )) + } + + "read response cookies with the expires attribute" in { + val response = sttp + .get(uri"$endpoint/set_cookies/with_expires") + .response(sttpIgnore) + .send() + .force() + response.cookies should have length (1) + val c = response.cookies(0) + + c.name should be("c") + c.value should be("v") + c.expires.map(_.toInstant.toEpochMilli) should be( + Some( + ZonedDateTime + .of(1997, 12, 8, 12, 49, 12, 0, ZoneId.of("GMT")) + .toInstant + .toEpochMilli + )) + } + } + + "auth" - { + def secureBasic = sttp.get(uri"$endpoint/secure_basic") + + "return a 401 when authorization fails" in { + val req = secureBasic + val resp = req.send().force() + resp.code should be(401) + resp.header("WWW-Authenticate") should be(Some("""Basic realm="test realm",charset=UTF-8""")) + } + + "perform basic authorization" in { + val req = secureBasic.auth.basic("adam", "1234") + val resp = req.send().force() + resp.code should be(200) + resp.unsafeBody should be("Hello, adam!") + } + } + + "compression" - { + def compress = sttp.get(uri"$endpoint/compress") + val decompressedBody = "I'm compressed!" + + "decompress using the default accept encoding header" in { + val req = compress + val resp = req.send().force() + resp.unsafeBody should be(decompressedBody) + } + + "decompress using gzip" in { + val req = + compress.header("Accept-Encoding", "gzip", replaceExisting = true) + val resp = req.send().force() + resp.unsafeBody should be(decompressedBody) + } + + "decompress using deflate" in { + val req = + compress.header("Accept-Encoding", "deflate", replaceExisting = true) + val resp = req.send().force() + resp.unsafeBody should be(decompressedBody) + } + + "work despite providing an unsupported encoding" in { + val req = + compress.header("Accept-Encoding", "br", replaceExisting = true) + val resp = req.send().force() + resp.unsafeBody should be(decompressedBody) + } + } + + "download file" - { + + "download a binary file using asFile" in { + val file = outPath.resolve("binaryfile.jpg").toFile + val req = + sttp.get(uri"$endpoint/download/binary").response(asFile(file)) + val resp = req.send().force() + + resp.unsafeBody shouldBe file + file should exist + file should haveSameContentAs(binaryFile) + } + + "download a text file using asFile" in { + val file = outPath.resolve("textfile.txt").toFile + val req = + sttp.get(uri"$endpoint/download/text").response(asFile(file)) + val resp = req.send().force() + + resp.unsafeBody shouldBe file + file should exist + file should haveSameContentAs(textFile) + } + + "download a binary file using asPath" in { + val path = outPath.resolve("binaryfile.jpg") + val req = + sttp.get(uri"$endpoint/download/binary").response(asPath(path)) + val resp = req.send().force() + + resp.unsafeBody shouldBe path + path.toFile should exist + path.toFile should haveSameContentAs(binaryFile) + } + + "download a text file using asPath" in { + val path = outPath.resolve("textfile.txt") + val req = + sttp.get(uri"$endpoint/download/text").response(asPath(path)) + val resp = req.send().force() + + resp.unsafeBody shouldBe path + path.toFile should exist + path.toFile should haveSameContentAs(textFile) + } + + "fail at trying to save file to a restricted location" in { + val path = Paths.get("/").resolve("textfile.txt") + val req = + sttp.get(uri"$endpoint/download/text").response(asPath(path)) + val caught = intercept[IOException] { + req.send().force() + } + + caught.getMessage shouldBe "Permission denied" + } + + "fail when file exists and overwrite flag is false" in { + val path = outPath.resolve("textfile.txt") + path.toFile.getParentFile.mkdirs() + path.toFile.createNewFile() + val req = + sttp.get(uri"$endpoint/download/text").response(asPath(path)) + + val caught = intercept[IOException] { + req.send().force() + } + + caught.getMessage shouldBe s"File ${path.toFile.getAbsolutePath} exists - overwriting prohibited" + + } + + "not fail when file exists and overwrite flag is true" in { + val path = outPath.resolve("textfile.txt") + path.toFile.getParentFile.mkdirs() + path.toFile.createNewFile() + val req = + sttp + .get(uri"$endpoint/download/text") + .response(asPath(path, overwrite = true)) + val resp = req.send().force() + + resp.unsafeBody shouldBe path + path.toFile should exist + path.toFile should haveSameContentAs(textFile) + } + } + + "multipart" - { + def mp = sttp.post(uri"$endpoint/multipart") + + "send a multipart message" in { + val req = mp.multipartBody(multipart("p1", "v1"), multipart("p2", "v2")) + val resp = req.send().force() + resp.unsafeBody should be("p1=v1, p2=v2") + } + + "send a multipart message with filenames" in { + val req = mp.multipartBody(multipart("p1", "v1").fileName("f1"), multipart("p2", "v2").fileName("f2")) + val resp = req.send().force() + resp.unsafeBody should be("p1=v1 (f1), p2=v2 (f2)") + } + + "send a multipart message with a file" in { + val f = File.newTemporaryFile().write(testBody) + try { + val req = + mp.multipartBody(multipart("p1", f.toJava), multipart("p2", "v2")) + val resp = req.send().force() + resp.unsafeBody should be(s"p1=$testBody (${f.name}), p2=v2") + } finally f.delete() + } + } + + "redirect" - { + def r1 = sttp.post(uri"$endpoint/redirect/r1") + def r2 = sttp.post(uri"$endpoint/redirect/r2") + def r3 = sttp.post(uri"$endpoint/redirect/r3") + val r4response = "819" + def loop = sttp.post(uri"$endpoint/redirect/loop") + + "not redirect when redirects shouldn't be followed (temporary)" in { + val resp = r1.followRedirects(false).send().force() + resp.code should be(307) + resp.body should be('left) + resp.history should be('empty) + } + + "not redirect when redirects shouldn't be followed (permanent)" in { + val resp = r2.followRedirects(false).send().force() + resp.code should be(308) + resp.body should be('left) + } + + "redirect when redirects should be followed" in { + val resp = r2.send().force() + resp.code should be(200) + resp.unsafeBody should be(r4response) + } + + "redirect twice when redirects should be followed" in { + val resp = r1.send().force() + resp.code should be(200) + resp.unsafeBody should be(r4response) + } + + "redirect when redirects should be followed, and the response is parsed" in { + val resp = r2.response(asString.map(_.toInt)).send().force() + resp.code should be(200) + resp.unsafeBody should be(r4response.toInt) + } + + "keep a single history entry of redirect responses" in { + val resp = r3.send().force() + resp.code should be(200) + resp.unsafeBody should be(r4response) + resp.history should have size (1) + resp.history(0).code should be(302) + } + + "keep whole history of redirect responses" in { + val resp = r1.send().force() + resp.code should be(200) + resp.unsafeBody should be(r4response) + resp.history should have size (3) + resp.history(0).code should be(307) + resp.history(1).code should be(308) + resp.history(2).code should be(302) + } + + "break redirect loops" in { + val resp = loop.send().force() + resp.code should be(0) + resp.history should have size (FollowRedirectsBackend.MaxRedirects) + } + + "break redirect loops after user-specified count" in { + val maxRedirects = 10 + val resp = loop.maxRedirects(maxRedirects).send().force() + resp.code should be(0) + resp.history should have size (maxRedirects) + } + + "not redirect when maxRedirects is less than or equal to 0" in { + val resp = loop.maxRedirects(-1).send().force() + resp.code should be(302) + resp.body should be('left) + resp.history should be('empty) + } + } + + "timeout" - { + "fail if read timeout is not big enough" in { + val request = sttp + .get(uri"$endpoint/timeout") + .readTimeout(200.milliseconds) + .response(asString) + + intercept[Throwable] { + request.send().force() + } + } + + "not fail if read timeout is big enough" in { + val request = sttp + .get(uri"$endpoint/timeout") + .readTimeout(5.seconds) + .response(asString) + + request.send().force().unsafeBody should be("Done") + } + } + + "empty response" - { + def postEmptyResponse = + sttp + .post(uri"$endpoint/empty_unauthorized_response") + .body("{}") + .contentType("application/json") + + "parse an empty error response as empty string" in { + val response = postEmptyResponse.send().force() + response.body should be(Left("")) + } + } + + "encoding" - { + "read response body encoded using ISO-8859-2, as specified in the header, overriding the default" in { + val request = sttp.get(uri"$endpoint/respond_with_iso_8859_2") + + request.send().force().unsafeBody should be(textWithSpecialCharacters) + } + } + + override protected def afterAll(): Unit = { + backend.close() + super.afterAll() + } + +} diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/TestHttpServer.scala b/core/src/test/scala/com/softwaremill/sttp/testing/TestHttpServer.scala new file mode 100644 index 0000000..11fc692 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/TestHttpServer.scala @@ -0,0 +1,236 @@ +package com.softwaremill.sttp.testing + +import akka.Done +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.coding.{Deflate, Gzip, NoCoding} +import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.headers.CacheDirectives._ +import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.server.Directives.{entity, path, _} +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.server.directives.Credentials +import akka.stream.ActorMaterializer +import akka.util.ByteString +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} + +import org.scalatest.BeforeAndAfterAll +import org.scalatest.Suite + +trait TestHttpServer extends BeforeAndAfterAll { this: Suite => + + private val server = new HttpServer(0) + protected var endpoint = "localhost:51823" + + override protected def beforeAll(): Unit = { + import scala.concurrent.ExecutionContext.Implicits.global + + super.beforeAll() + Await.result( + server.start().map { binding => + endpoint = s"localhost:${binding.localAddress.getPort}" + }, + 10.seconds + ) + } + + override protected def afterAll(): Unit = { + server.close() + super.afterAll() + } + +} + +object HttpServer { + + def main(args: Array[String]): Unit = { + val port = args.headOption.map(_.toInt).getOrElse(51823) + + Await.result(new HttpServer(port).start(), 10.seconds) + } +} + +private class HttpServer(port: Int) extends AutoCloseable { + + import scala.concurrent.ExecutionContext.Implicits.global + + private var server: Option[Future[Http.ServerBinding]] = None + + private implicit val actorSystem: ActorSystem = ActorSystem("sttp-test-server") + private implicit val materializer: ActorMaterializer = ActorMaterializer() + + private def paramsToString(m: Map[String, String]): String = + m.toList.sortBy(_._1).map(p => s"${p._1}=${p._2}").mkString(" ") + + private val textFile = new java.io.File(getClass.getResource("/textfile.txt").getFile) + private val binaryFile = new java.io.File(getClass.getResource("/binaryfile.jpg").getFile) + private val textWithSpecialCharacters = "Żółć!" + + val serverRoutes: Route = + pathPrefix("echo") { + pathPrefix("form_params") { + formFieldMap { params => + path("as_string") { + complete(paramsToString(params)) + } ~ + path("as_params") { + complete(FormData(params)) + } + } + } ~ get { + parameterMap { params => + complete(List("GET", "/echo", paramsToString(params)) + .filter(_.nonEmpty) + .mkString(" ")) + } + } ~ + post { + parameterMap { params => + entity(as[String]) { body: String => + complete(List("POST", "/echo", paramsToString(params), body) + .filter(_.nonEmpty) + .mkString(" ")) + } + } + } + } ~ pathPrefix("streaming") { + path("echo") { + post { + parameterMap { _ => + entity(as[String]) { body: String => + complete(body) + } + } + } + } + } ~ path("set_headers") { + get { + respondWithHeader(`Cache-Control`(`max-age`(1000L))) { + respondWithHeader(`Cache-Control`(`no-cache`)) { + complete("ok") + } + } + } + } ~ pathPrefix("set_cookies") { + path("with_expires") { + setCookie(HttpCookie("c", "v", expires = Some(DateTime(1997, 12, 8, 12, 49, 12)))) { + complete("ok") + } + } ~ get { + setCookie( + HttpCookie( + "cookie1", + "value1", + secure = true, + httpOnly = true, + maxAge = Some(123L) + ) + ) { + setCookie(HttpCookie("cookie2", "value2")) { + setCookie( + HttpCookie( + "cookie3", + "", + domain = Some("xyz"), + path = Some("a/b/c") + ) + ) { + complete("ok") + } + } + } + } + } ~ path("secure_basic") { + authenticateBasic("test realm", { + case c @ Credentials.Provided(un) if un == "adam" && c.verify("1234") => + Some(un) + case _ => None + }) { userName => + complete(s"Hello, $userName!") + } + } ~ path("compress") { + encodeResponseWith(Gzip, Deflate, NoCoding) { + complete("I'm compressed!") + } + } ~ pathPrefix("download") { + path("binary") { + getFromFile(binaryFile) + } ~ path("text") { + getFromFile(textFile) + } + } ~ pathPrefix("multipart") { + entity(as[akka.http.scaladsl.model.Multipart.FormData]) { fd => + complete { + fd.parts + .mapAsync(1) { p => + val fv = p.entity.dataBytes.runFold(ByteString())(_ ++ _) + fv.map(_.utf8String) + .map(v => p.name + "=" + v + p.filename.fold("")(fn => s" ($fn)")) + } + .runFold(Vector.empty[String])(_ :+ _) + .map(v => v.mkString(", ")) + } + } + } ~ pathPrefix("redirect") { + path("r1") { + redirect("/redirect/r2", StatusCodes.TemporaryRedirect) + } ~ + path("r2") { + redirect("/redirect/r3", StatusCodes.PermanentRedirect) + } ~ + path("r3") { + redirect("/redirect/r4", StatusCodes.Found) + } ~ + path("r4") { + complete("819") + } ~ + path("loop") { + redirect("/redirect/loop", StatusCodes.Found) + } + } ~ pathPrefix("timeout") { + complete { + akka.pattern.after(1.second, using = actorSystem.scheduler)( + Future.successful("Done") + ) + } + } ~ path("empty_unauthorized_response") { + post { + import akka.http.scaladsl.model._ + complete( + HttpResponse( + status = StatusCodes.Unauthorized, + headers = Nil, + entity = HttpEntity.Empty, + protocol = HttpProtocols.`HTTP/1.1` + )) + } + } ~ path("respond_with_iso_8859_2") { + get { ctx => + val entity = + HttpEntity(MediaTypes.`text/plain`.withCharset(HttpCharset.custom("ISO-8859-2")), textWithSpecialCharacters) + ctx.complete(HttpResponse(200, entity = entity)) + } + } + + def start(): Future[Http.ServerBinding] = { + unbindServer().flatMap { _ => + val server = Http().bindAndHandle(serverRoutes, "localhost", port) + this.server = Some(server) + server + } + } + + def close(): Unit = { + val unbind = unbindServer() + unbind.onComplete(_ => actorSystem.terminate()) + Await.result( + unbind, + 10.seconds + ) + } + + private def unbindServer(): Future[Done] = { + server.map(_.flatMap(_.unbind())).getOrElse(Future.successful(Done)) + } +} diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala new file mode 100644 index 0000000..40aaf82 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala @@ -0,0 +1,76 @@ +package com.softwaremill.sttp.testing.streaming + +import com.softwaremill.sttp._ +import com.softwaremill.sttp.testing.ForceWrapped +import org.scalatest.{AsyncFreeSpec, BeforeAndAfterAll, Matchers} +import scala.language.higherKinds + +import com.softwaremill.sttp.testing.ConvertToFuture +import com.softwaremill.sttp.testing.TestHttpServer + +trait StreamingTest[R[_], S] + extends AsyncFreeSpec + with Matchers + with ForceWrapped + with BeforeAndAfterAll + with TestHttpServer { + + private val body = "streaming test" + + implicit def backend: SttpBackend[R, S] + + implicit def convertToFuture: ConvertToFuture[R] + + def bodyProducer(body: String): S + + def bodyConsumer(stream: S): R[String] + + "stream request body" in { + sttp + .post(uri"$endpoint/streaming/echo") + .streamBody(bodyProducer(body)) + .send() + .toFuture() + .map { response => + response.unsafeBody shouldBe body + } + } + + "receive a stream" in { + sttp + .post(uri"$endpoint/streaming/echo") + .body(body) + .response(asStream[S]) + .send() + .toFuture() + .flatMap { response => + bodyConsumer(response.unsafeBody).toFuture() + } + .map { responseBody => + responseBody shouldBe body + } + } + + "receive a stream from an https site" in { + sttp + // of course, you should never rely on the internet being available + // in tests, but that's so much easier than setting up an https + // testing server + .get(uri"https://softwaremill.com") + .response(asStream[S]) + .send() + .toFuture() + .flatMap { response => + bodyConsumer(response.unsafeBody).toFuture() + } + .map { responseBody => + responseBody should include("</div>") + } + } + + override protected def afterAll(): Unit = { + backend.close() + super.afterAll() + } + +} diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala deleted file mode 100644 index 266c402..0000000 --- a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala +++ /dev/null @@ -1,15 +0,0 @@ -package com.softwaremill.sttp.testing.streaming - -import com.softwaremill.sttp.SttpBackend - -import scala.language.higherKinds - -trait TestStreamingBackend[R[_], S] { - implicit def backend: SttpBackend[R, S] - - implicit def convertToFuture: ConvertToFuture[R] - - def bodyProducer(body: String): S - - def bodyConsumer(stream: S): R[String] -} |