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()
}
}