aboutsummaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
Diffstat (limited to 'core/src')
-rw-r--r--core/src/test/resources/binaryfile.jpgbin0 -> 42010 bytes
-rw-r--r--core/src/test/resources/textfile.txt100
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala10
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala37
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala12
-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.scala22
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/testing/EvalScala.scala11
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/testing/ForceWrapped.scala30
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala524
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/testing/TestHttpServer.scala236
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala76
-rw-r--r--core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala15
13 files changed, 1059 insertions, 16 deletions
diff --git a/core/src/test/resources/binaryfile.jpg b/core/src/test/resources/binaryfile.jpg
new file mode 100644
index 0000000..b9f5c5a
--- /dev/null
+++ b/core/src/test/resources/binaryfile.jpg
Binary files differ
diff --git a/core/src/test/resources/textfile.txt b/core/src/test/resources/textfile.txt
new file mode 100644
index 0000000..9904f90
--- /dev/null
+++ b/core/src/test/resources/textfile.txt
@@ -0,0 +1,100 @@
+- Lorem ipsum dolor sit amet
+- Vivamus sem ipsum.
+- Ut molestie.
+- Donec.
+- Fusce non porta.
+- Nulla ac metus. Morbi mattis.
+- Etiam varius.
+- Nulla.
+- Phasellus id mollis.
+- Suspendisse at.
+- Quisque nec leo velit.
+- Fusce.
+- Maecenas nec tristique senectus et.
+- Integer vestibulum lorem fermentum.
+- Vestibulum consectetuer dolor.
+- Lorem ipsum in.
+- Fusce condimentum auctor scelerisque, wisi.
+- Quisque.
+- Curae.
+- Curae, Nullam.
+- Curae, Integer.
+- Vestibulum dignissim massa. Donec.
+- Pellentesque sed sem.
+- Vivamus est.
+- Maecenas elit sed est.
+- Quisque sed tellus.
+- Cum sociis natoque penatibus et.
+- Fusce aliquam.
+- Donec.
+- Sed elementum, sapien accumsan odio.
+- Nam mattis, magna lectus, tincidunt.
+- Pellentesque scelerisque a, sodales.
+- Sed sed condimentum.
+- Curae, In nonummy. Phasellus adipiscing.
+- Vestibulum quis diam mollis.
+- Sed eros. Duis ipsum.
+- Aenean pellentesque at, mollis tempus.
+- Cras ornare facilisis sodales. Aenean.
+- Cum sociis natoque penatibus.
+- Donec id nulla.
+- Quisque ut sapien.
+- Phasellus purus. Proin ultricies.
+- Aliquam auctor neque. Nunc.
+- Nam nunc fringilla non, vehicula.
+- Morbi molestie, felis ut lobortis.
+- Nulla quis.
+- In.
+- Phasellus laoreet urna.
+- Lorem ipsum.
+- Phasellus.
+- Class aptent taciti sociosqu ad.
+- Sed lacinia.
+- Pellentesque dapibus diam. Duis.
+- Suspendisse est. Curabitur.
+- Fusce condimentum justo.
+- Aenean congue quis, faucibus.
+- Ut pharetra leo. Donec.
+- Fusce.
+- Donec porta.
+- Pellentesque orci.
+- Sed.
+- Quisque rutrum, wisi vulputate wisi.
+- Cum sociis.
+- Cras.
+- Sed eros. Curabitur.
+- Proin in velit wisi, tempor.
+- Quisque eu.
+- Proin.
+- Nam pellentesque sed, imperdiet aliquam.
+- Mauris euismod. Sed euismod orci.
+- Etiam.
+- Donec.
+- Fusce wisi a metus. Proin.
+- Phasellus quis.
+- Donec non imperdiet.
+- Aenean vel bibendum a, laoreet.
+- Fusce non enim. Phasellus vulputate.
+- Donec urna elit, sit.
+- Pellentesque habitant morbi.
+- Nulla ante. Curabitur elit. Donec.
+- Cum sociis natoque penatibus.
+- Maecenas eget leo at.
+- Cum sociis natoque penatibus et.
+- Vivamus lacus.
+- Integer.
+- Curae.
+- Maecenas rhoncus. Morbi.
+- Aenean posuere.
+- Duis.
+- Suspendisse a odio fermentum libero.
+- Nam enim. Fusce enim. In.
+- Maecenas.
+- Lorem ipsum primis.
+- Curabitur ac turpis semper sed.
+- Quisque condimentum. Donec sit.
+- Integer convallis non, posuere.
+- Etiam vulputate, odio.
+- Proin id lorem. Donec quis.
+- Curae, Sed nec augue.
+- Aliquam ut turpis. \ No newline at end of file
diff --git a/core/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala b/core/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala
new file mode 100644
index 0000000..807209a
--- /dev/null
+++ b/core/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala
@@ -0,0 +1,10 @@
+package com.softwaremill.sttp
+
+import com.softwaremill.sttp.testing.ConvertToFuture
+import com.softwaremill.sttp.testing.HttpTest
+
+class HttpURLConnectionHttpTest extends HttpTest[Id] {
+
+ override implicit val backend: SttpBackend[Id, Nothing] = HttpURLConnectionBackend()
+ override implicit val convertToFuture: ConvertToFuture[Id] = ConvertToFuture.id
+}
diff --git a/core/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala b/core/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala
new file mode 100644
index 0000000..4336a0c
--- /dev/null
+++ b/core/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala
@@ -0,0 +1,37 @@
+package com.softwaremill.sttp
+
+import com.softwaremill.sttp.testing.EvalScala
+import org.scalatest.{FlatSpec, Matchers}
+
+import scala.tools.reflect.ToolBoxError
+
+class IllTypedTests extends FlatSpec with Matchers {
+ "compilation" should "fail when trying to stream using the default backend" in {
+ val thrown = intercept[ToolBoxError] {
+ EvalScala("""
+ import com.softwaremill.sttp._
+
+ class MyStream[T]()
+
+ implicit val sttpBackend = HttpURLConnectionBackend()
+ sttp.get(uri"http://example.com").response(asStream[MyStream[Byte]]).send()
+ """)
+ }
+
+ thrown.getMessage should include(
+ "could not find implicit value for parameter backend: com.softwaremill.sttp.SttpBackend[R,MyStream[Byte]]"
+ )
+ }
+
+ "compilation" should "fail when trying to send a request without giving an URL" in {
+ val thrown = intercept[ToolBoxError] {
+ EvalScala("""
+ import com.softwaremill.sttp._
+ implicit val sttpBackend = HttpURLConnectionBackend()
+ sttp.send()
+ """)
+ }
+
+ thrown.getMessage should include("This is a partial request, the method & url are not specified")
+ }
+}
diff --git a/core/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala b/core/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala
new file mode 100644
index 0000000..aad1669
--- /dev/null
+++ b/core/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala
@@ -0,0 +1,12 @@
+package com.softwaremill.sttp
+
+import scala.util.Try
+
+import com.softwaremill.sttp.testing.ConvertToFuture
+import com.softwaremill.sttp.testing.HttpTest
+
+class TryHttpURLConnectionHttpTest extends HttpTest[Try] {
+
+ override implicit val backend: SttpBackend[Try, Nothing] = TryHttpURLConnectionBackend()
+ override implicit val convertToFuture: ConvertToFuture[Try] = ConvertToFuture.scalaTry
+}
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]
-}