diff options
39 files changed, 1318 insertions, 968 deletions
diff --git a/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpClientHttpTest.scala b/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpClientHttpTest.scala new file mode 100644 index 0000000..fe72794 --- /dev/null +++ b/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpClientHttpTest.scala @@ -0,0 +1,12 @@ +package com.softwaremill.sttp.akkahttp + +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} + +import scala.concurrent.Future + +class AkkaHttpClientHttpTest extends HttpTest[Future] { + + override implicit val backend: SttpBackend[Future, Nothing] = AkkaHttpBackend() + override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future +} diff --git a/tests/src/test/scala/com/softwaremill/sttp/streaming/AkkaHttpStreamingTests.scala b/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTests.scala index 691df81..e8ab9d7 100644 --- a/tests/src/test/scala/com/softwaremill/sttp/streaming/AkkaHttpStreamingTests.scala +++ b/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTests.scala @@ -1,17 +1,28 @@ -package com.softwaremill.sttp.streaming +package com.softwaremill.sttp.akkahttp import akka.NotUsed import akka.actor.ActorSystem -import akka.stream.Materializer +import akka.stream.{ActorMaterializer, Materializer} import akka.stream.scaladsl.Source import akka.util.ByteString import com.softwaremill.sttp.SttpBackend -import com.softwaremill.sttp.akkahttp.AkkaHttpBackend -import com.softwaremill.sttp.testing.streaming.{ConvertToFuture, TestStreamingBackend} +import com.softwaremill.sttp.testing.ConvertToFuture +import com.softwaremill.sttp.testing.streaming.{StreamingTest, TestStreamingBackend} import scala.concurrent.Future -class AkkaHttpStreamingTests(actorSystem: ActorSystem)(implicit materializer: Materializer) +class AkkaHttpStreamingTest extends StreamingTest[Future, Source[ByteString, Any]] { + + private implicit val actorSystem: ActorSystem = ActorSystem("sttp-test") + private implicit val materializer: ActorMaterializer = ActorMaterializer() + + override val testStreamingBackend: TestStreamingBackend[Future, Source[ByteString, Any]] = + new AkkaHttpTestStreamingBackend(actorSystem) +} + +class AkkaHttpTestStreamingBackend( + actorSystem: ActorSystem +)(implicit materializer: Materializer) extends TestStreamingBackend[Future, Source[ByteString, Any]] { override implicit val backend: SttpBackend[Future, Source[ByteString, Any]] = diff --git a/async-http-client-backend/cats/src/test/scala/com/softwaremill/sttp/asynchttpclient/cats/AsyncHttpClientCatsHttpTest.scala b/async-http-client-backend/cats/src/test/scala/com/softwaremill/sttp/asynchttpclient/cats/AsyncHttpClientCatsHttpTest.scala new file mode 100644 index 0000000..5136914 --- /dev/null +++ b/async-http-client-backend/cats/src/test/scala/com/softwaremill/sttp/asynchttpclient/cats/AsyncHttpClientCatsHttpTest.scala @@ -0,0 +1,12 @@ +package com.softwaremill.sttp.asynchttpclient.cats + +import cats.effect.IO +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.impl.cats.convertCatsIOToFuture +import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} + +class AsyncHttpClientCatsHttpTest extends HttpTest[IO] { + + override implicit val backend: SttpBackend[IO, Nothing] = AsyncHttpClientCatsBackend() + override implicit val convertToFuture: ConvertToFuture[IO] = convertCatsIOToFuture +} diff --git a/async-http-client-backend/fs2/src/test/scala/com/softwaremill/sttp/asynchttpclient/fs2/AsyncHttpClientFs2HttpStreamingTest.scala b/async-http-client-backend/fs2/src/test/scala/com/softwaremill/sttp/asynchttpclient/fs2/AsyncHttpClientFs2HttpStreamingTest.scala new file mode 100644 index 0000000..565db5c --- /dev/null +++ b/async-http-client-backend/fs2/src/test/scala/com/softwaremill/sttp/asynchttpclient/fs2/AsyncHttpClientFs2HttpStreamingTest.scala @@ -0,0 +1,40 @@ +package com.softwaremill.sttp.asynchttpclient.fs2 + +import java.nio.ByteBuffer + +import cats.effect.IO +import cats.instances.string._ +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.testing.streaming.{StreamingTest, TestStreamingBackend} +import com.softwaremill.sttp.testing.ConvertToFuture +import fs2.{Chunk, Stream, text} +import scala.concurrent.Future + +class AsyncHttpClientFs2HttpStreamingTest extends StreamingTest[IO, Stream[IO, ByteBuffer]] { + + override val testStreamingBackend: TestStreamingBackend[IO, Stream[IO, ByteBuffer]] = + new AsyncHttpClientFs2TestStreamingBackend +} + +class AsyncHttpClientFs2TestStreamingBackend extends TestStreamingBackend[IO, Stream[IO, ByteBuffer]] { + + override implicit val backend: SttpBackend[IO, Stream[IO, ByteBuffer]] = + AsyncHttpClientFs2Backend[IO]() + + override implicit val convertToFuture: ConvertToFuture[IO] = + new ConvertToFuture[IO] { + override def toFuture[T](value: IO[T]): Future[T] = + value.unsafeToFuture() + } + + override def bodyProducer(body: String): Stream[IO, ByteBuffer] = + Stream.emits(body.getBytes("utf-8")).map(b => ByteBuffer.wrap(Array(b))) + + override def bodyConsumer(stream: Stream[IO, ByteBuffer]): IO[String] = + stream + .map(bb => Chunk.array(bb.array)) + .through(text.utf8DecodeC) + .compile + .foldMonoid + +} diff --git a/async-http-client-backend/fs2/src/test/scala/com/softwaremill/sttp/asynchttpclient/fs2/AsyncHttpClientFs2HttpTest.scala b/async-http-client-backend/fs2/src/test/scala/com/softwaremill/sttp/asynchttpclient/fs2/AsyncHttpClientFs2HttpTest.scala new file mode 100644 index 0000000..7772c23 --- /dev/null +++ b/async-http-client-backend/fs2/src/test/scala/com/softwaremill/sttp/asynchttpclient/fs2/AsyncHttpClientFs2HttpTest.scala @@ -0,0 +1,15 @@ +package com.softwaremill.sttp.asynchttpclient.fs2 + +import cats.effect.IO +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} + +import scala.concurrent.Future + +class AsyncHttpClientFs2HttpTest extends HttpTest[IO] { + + override implicit val backend: SttpBackend[IO, Nothing] = AsyncHttpClientFs2Backend() + override implicit val convertToFuture: ConvertToFuture[IO] = new ConvertToFuture[IO] { + override def toFuture[T](value: IO[T]): Future[T] = value.unsafeToFuture() + } +} diff --git a/async-http-client-backend/future/src/test/scala/com/softwaremill/sttp/asynchttpclient/future/AsyncHttpClientFutureHttpTest.scala b/async-http-client-backend/future/src/test/scala/com/softwaremill/sttp/asynchttpclient/future/AsyncHttpClientFutureHttpTest.scala new file mode 100644 index 0000000..58d8aa8 --- /dev/null +++ b/async-http-client-backend/future/src/test/scala/com/softwaremill/sttp/asynchttpclient/future/AsyncHttpClientFutureHttpTest.scala @@ -0,0 +1,12 @@ +package com.softwaremill.sttp.asynchttpclient.future + +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} + +import scala.concurrent.Future + +class AsyncHttpClientFutureHttpTest extends HttpTest[Future] { + + override implicit val backend: SttpBackend[Future, Nothing] = AsyncHttpClientFutureBackend() + override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future +} diff --git a/async-http-client-backend/monix/src/test/scala/com/softwaremill/sttp/asynchttpclient/monix/AsyncHttpClientMonixHttpTest.scala b/async-http-client-backend/monix/src/test/scala/com/softwaremill/sttp/asynchttpclient/monix/AsyncHttpClientMonixHttpTest.scala new file mode 100644 index 0000000..a08e738 --- /dev/null +++ b/async-http-client-backend/monix/src/test/scala/com/softwaremill/sttp/asynchttpclient/monix/AsyncHttpClientMonixHttpTest.scala @@ -0,0 +1,15 @@ +package com.softwaremill.sttp.asynchttpclient.monix + +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.impl.monix.convertMonixTaskToFuture +import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} +import monix.eval.Task + +class AsyncHttpClientMonixHttpTest extends HttpTest[Task] { + + import monix.execution.Scheduler.Implicits.global + + override implicit val backend: SttpBackend[Task, Nothing] = AsyncHttpClientMonixBackend() + override implicit val convertToFuture: ConvertToFuture[Task] = convertMonixTaskToFuture + +} diff --git a/async-http-client-backend/monix/src/test/scala/com/softwaremill/sttp/asynchttpclient/monix/AsyncHttpClientMonixStreamingTest.scala b/async-http-client-backend/monix/src/test/scala/com/softwaremill/sttp/asynchttpclient/monix/AsyncHttpClientMonixStreamingTest.scala new file mode 100644 index 0000000..34c15ff --- /dev/null +++ b/async-http-client-backend/monix/src/test/scala/com/softwaremill/sttp/asynchttpclient/monix/AsyncHttpClientMonixStreamingTest.scala @@ -0,0 +1,26 @@ +package com.softwaremill.sttp.asynchttpclient.monix + +import java.nio.ByteBuffer + +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.impl.monix.MonixTestStreamingBackend +import com.softwaremill.sttp.testing.streaming.{StreamingTest, TestStreamingBackend} +import monix.eval.Task +import monix.reactive.Observable + +class AsyncHttpClientMonixStreamingTest extends StreamingTest[Task, Observable[ByteBuffer]] { + + override val testStreamingBackend: TestStreamingBackend[Task, Observable[ByteBuffer]] = + new AsyncHttpClientMonixTestStreamingBackend +} + +class AsyncHttpClientMonixTestStreamingBackend extends MonixTestStreamingBackend[ByteBuffer] { + + import monix.execution.Scheduler.Implicits.global + + override def toByteArray(v: ByteBuffer): Array[Byte] = v.array() + override def fromByteArray(v: Array[Byte]): ByteBuffer = ByteBuffer.wrap(v) + + override implicit val backend: SttpBackend[Task, Observable[ByteBuffer]] = + AsyncHttpClientMonixBackend() +} diff --git a/async-http-client-backend/scalaz/src/test/scala/com/softwaremill/sttp/asynchttpclient/scalaz/AsyncHttpClientScalazHttpTest.scala b/async-http-client-backend/scalaz/src/test/scala/com/softwaremill/sttp/asynchttpclient/scalaz/AsyncHttpClientScalazHttpTest.scala new file mode 100644 index 0000000..67acc09 --- /dev/null +++ b/async-http-client-backend/scalaz/src/test/scala/com/softwaremill/sttp/asynchttpclient/scalaz/AsyncHttpClientScalazHttpTest.scala @@ -0,0 +1,13 @@ +package com.softwaremill.sttp.asynchttpclient.scalaz + +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.impl.scalaz.convertScalazTaskToFuture +import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} + +import scalaz.concurrent.Task + +class AsyncHttpClientScalazHttpTest extends HttpTest[Task] { + + override implicit val backend: SttpBackend[Task, Nothing] = AsyncHttpClientScalazBackend() + override implicit val convertToFuture: ConvertToFuture[Task] = convertScalazTaskToFuture +} @@ -34,9 +34,12 @@ val akkaStreams = "com.typesafe.akka" %% "akka-stream" % "2.5.12" val scalaTest = "org.scalatest" %% "scalatest" % "3.0.5" +val testServerPort = settingKey[Int]("Port to run the http test server on") +val startTestServer = taskKey[Unit]("Start a http server used by tests") + lazy val rootProject = (project in file(".")) .settings(commonSettings: _*) - .settings(publishArtifact := false, name := "sttp") + .settings(skip in publish := true, name := "sttp") .aggregate( core, cats, @@ -55,7 +58,8 @@ lazy val rootProject = (project in file(".")) json4s, braveBackend, prometheusBackend, - tests + tests, + testServer ) lazy val core: Project = (project in file("core")) @@ -63,9 +67,10 @@ lazy val core: Project = (project in file("core")) .settings( name := "core", libraryDependencies ++= Seq( - "org.scalacheck" %% "scalacheck" % "1.14.0" % "test", + "com.github.pathikrit" %% "better-files" % "3.4.0" % "test", scalaTest % "test" - ) + ), + publishArtifact in Test := true // allow implementations outside of this repo ) //----- implementations @@ -97,6 +102,7 @@ lazy val scalaz: Project = (project in file("implementations/scalaz")) //-- akka lazy val akkaHttpBackend: Project = (project in file("akka-http-backend")) .settings(commonSettings: _*) + .settings(testServerSettings: _*) .settings( name := "akka-http-backend", libraryDependencies ++= Seq( @@ -106,42 +112,45 @@ lazy val akkaHttpBackend: Project = (project in file("akka-http-backend")) akkaStreams % "provided" ) ) - .dependsOn(core) + .dependsOn(core % "compile->compile;test->test") //-- async http client lazy val asyncHttpClientBackend: Project = { (project in file("async-http-client-backend")) .settings(commonSettings: _*) + .settings(testServerSettings: _*) .settings( name := "async-http-client-backend", libraryDependencies ++= Seq( "org.asynchttpclient" % "async-http-client" % "2.4.7" ) ) - .dependsOn(core) + .dependsOn(core % "compile->compile;test->test") } def asyncHttpClientBackendProject(proj: String): Project = { Project(s"asyncHttpClientBackend${proj.capitalize}", file(s"async-http-client-backend/$proj")) .settings(commonSettings: _*) + .settings(testServerSettings: _*) .settings(name := s"async-http-client-backend-$proj") .dependsOn(asyncHttpClientBackend) } lazy val asyncHttpClientFutureBackend: Project = asyncHttpClientBackendProject("future") + .dependsOn(core % "compile->compile;test->test") lazy val asyncHttpClientScalazBackend: Project = asyncHttpClientBackendProject("scalaz") - .dependsOn(scalaz) + .dependsOn(scalaz % "compile->compile;test->test") lazy val asyncHttpClientMonixBackend: Project = asyncHttpClientBackendProject("monix") - .dependsOn(monix) + .dependsOn(monix % "compile->compile;test->test") lazy val asyncHttpClientCatsBackend: Project = asyncHttpClientBackendProject("cats") - .dependsOn(cats) + .dependsOn(cats % "compile->compile;test->test") lazy val asyncHttpClientFs2Backend: Project = asyncHttpClientBackendProject("fs2") @@ -150,28 +159,31 @@ lazy val asyncHttpClientFs2Backend: Project = "com.github.zainab-ali" %% "fs2-reactive-streams" % "0.5.1" ) ) + .dependsOn(core % "compile->compile;test->test") //-- okhttp lazy val okhttpBackend: Project = (project in file("okhttp-backend")) .settings(commonSettings: _*) + .settings(testServerSettings: _*) .settings( name := "okhttp-backend", libraryDependencies ++= Seq( "com.squareup.okhttp3" % "okhttp" % "3.10.0" ) ) - .dependsOn(core) + .dependsOn(core % "compile->compile;test->test") def okhttpBackendProject(proj: String): Project = { Project(s"okhttpBackend${proj.capitalize}", file(s"okhttp-backend/$proj")) .settings(commonSettings: _*) + .settings(testServerSettings: _*) .settings(name := s"okhttp-backend-$proj") .dependsOn(okhttpBackend) } lazy val okhttpMonixBackend: Project = okhttpBackendProject("monix") - .dependsOn(monix) + .dependsOn(monix % "compile->compile;test->test") lazy val circeVersion = "0.9.3" @@ -225,8 +237,9 @@ lazy val prometheusBackend: Project = (project in file("metrics/prometheus-backe lazy val tests: Project = (project in file("tests")) .settings(commonSettings: _*) + .settings(testServerSettings: _*) .settings( - publishArtifact := false, + skip in publish := true, name := "tests", libraryDependencies ++= Seq( akkaHttp, @@ -251,3 +264,26 @@ lazy val tests: Project = (project in file("tests")) asyncHttpClientFs2Backend, okhttpMonixBackend ) + +// https://stackoverflow.com/questions/25766797/how-do-i-start-a-server-before-running-a-test-suite-in-sbt +lazy val testServer: Project = project + .in(file("test-server")) + .settings(commonSettings: _*) + .settings( + name := "test-server", + libraryDependencies ++= Seq(akkaHttp, akkaStreams), + mainClass in reStart := Some("com.softwaremill.sttp.server.TestHttpServer"), + reStartArgs := Seq(s"${testServerPort.value}"), + testServerPort := 51823, + startTestServer := (reStart in Test).toTask("").value + ) + +// maybe use IntegrationTest instead of Test? +lazy val testServerSettings = Seq( + test in Test := (test in Test).dependsOn(startTestServer in testServer).value, + testOnly in Test := (testOnly in Test).dependsOn(startTestServer in testServer).evaluated, + testOptions in Test += Tests.Setup(() => { + val port = (testServerPort in testServer).value + PollingUtils.waitUntilServerAvailable(new URL(s"http://localhost:$port")) + }) +) diff --git a/core/src/main/scala/com/softwaremill/sttp/MonadError.scala b/core/src/main/scala/com/softwaremill/sttp/MonadError.scala index a783765..b5b1852 100644 --- a/core/src/main/scala/com/softwaremill/sttp/MonadError.scala +++ b/core/src/main/scala/com/softwaremill/sttp/MonadError.scala @@ -31,6 +31,15 @@ trait MonadAsyncError[R[_]] extends MonadError[R] { def async[T](register: (Either[Throwable, T] => Unit) => Unit): R[T] } +object syntax { + + implicit final class MonadErrorOps[R[_], A](val r: R[A]) extends AnyVal { + def map[B](f: A => B)(implicit ME: MonadError[R]): R[B] = ME.map(r)(f) + def flatMap[B](f: A => R[B])(implicit ME: MonadError[R]): R[B] = + ME.flatMap(r)(f) + } +} + object IdMonad extends MonadError[Id] { override def unit[T](t: T): Id[T] = t override def map[T, T2](fa: Id[T])(f: (T) => T2): Id[T2] = f(fa) 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/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..f1fa002 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala @@ -0,0 +1,526 @@ +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 { + + private val endpoint = "localhost:51823" + + override def afterEach() { + val file = File(outPath) + if (file.exists) file.delete() + } + + private val textFile = + new java.io.File("test-server/src/main/resources/textfile.txt") + private val binaryFile = + new java.io.File("test-server/src/main/resources/binaryfile.jpg") + private val outPath = File.newTemporaryDirectory().path + private val textWithSpecialCharacters = "Żółć!" + + implicit val backend: SttpBackend[R, Nothing] + implicit val convertToFuture: ConvertToFuture[R] + + private val 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" - { + val 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" - { + val 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" - { + val 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" - { + val 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" - { + val 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" - { + val r1 = sttp.post(uri"$endpoint/redirect/r1") + val r2 = sttp.post(uri"$endpoint/redirect/r2") + val r3 = sttp.post(uri"$endpoint/redirect/r3") + val r4response = "819" + val 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" - { + val 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/streaming/StreamingTest.scala b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala new file mode 100644 index 0000000..aa0af07 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala @@ -0,0 +1,65 @@ +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 + +trait StreamingTest[R[_], S] extends AsyncFreeSpec with Matchers with BeforeAndAfterAll with ForceWrapped { + + private val endpoint = "localhost:51823" + private val body = "streaming test" + + val testStreamingBackend: TestStreamingBackend[R, S] + import testStreamingBackend._ + + "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 = { + testStreamingBackend.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 index 266c402..3ea63a3 100644 --- a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala +++ b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala @@ -1,6 +1,7 @@ package com.softwaremill.sttp.testing.streaming import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.testing.ConvertToFuture import scala.language.higherKinds diff --git a/implementations/cats/src/test/scala/com/softwaremill/sttp/impl/cats/package.scala b/implementations/cats/src/test/scala/com/softwaremill/sttp/impl/cats/package.scala index abffc90..5f3db65 100644 --- a/implementations/cats/src/test/scala/com/softwaremill/sttp/impl/cats/package.scala +++ b/implementations/cats/src/test/scala/com/softwaremill/sttp/impl/cats/package.scala @@ -1,7 +1,7 @@ package com.softwaremill.sttp.impl import _root_.cats.effect.IO -import com.softwaremill.sttp.testing.streaming.ConvertToFuture +import com.softwaremill.sttp.testing.ConvertToFuture import scala.concurrent.Future diff --git a/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/MonixTestStreamingBackend.scala b/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/MonixTestStreamingBackend.scala index 3f84ec3..f1b262b 100644 --- a/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/MonixTestStreamingBackend.scala +++ b/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/MonixTestStreamingBackend.scala @@ -1,21 +1,27 @@ package com.softwaremill.sttp.impl.monix -import java.nio.ByteBuffer - -import com.softwaremill.sttp.testing.streaming.{ConvertToFuture, TestStreamingBackend} +import com.softwaremill.sttp.testing.ConvertToFuture +import com.softwaremill.sttp.testing.streaming.TestStreamingBackend import monix.eval.Task import monix.reactive.Observable -trait MonixTestStreamingBackend extends TestStreamingBackend[Task, Observable[ByteBuffer]] { +trait MonixTestStreamingBackend[T] extends TestStreamingBackend[Task, Observable[T]] { + + def toByteArray(v: T): Array[Byte] + def fromByteArray(v: Array[Byte]): T override implicit def convertToFuture: ConvertToFuture[Task] = convertMonixTaskToFuture - override def bodyProducer(body: String): Observable[ByteBuffer] = - Observable.fromIterable(body.getBytes("utf-8").map(b => ByteBuffer.wrap(Array(b)))) + override def bodyProducer(body: String): Observable[T] = + Observable + .fromIterable( + body.getBytes("utf-8") + ) + .map(v => fromByteArray(Array(v))) - override def bodyConsumer(stream: Observable[ByteBuffer]): Task[String] = + override def bodyConsumer(stream: Observable[T]): Task[String] = stream - .flatMap(bb => Observable.fromIterable(bb.array())) + .flatMap(v => Observable.fromIterable(toByteArray(v))) .toListL .map(bs => new String(bs.toArray, "utf8")) diff --git a/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/package.scala b/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/package.scala index f77aa93..02fef8b 100644 --- a/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/package.scala +++ b/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/package.scala @@ -3,7 +3,7 @@ package com.softwaremill.sttp.impl import scala.concurrent.Future import _root_.monix.eval.Task -import com.softwaremill.sttp.testing.streaming.ConvertToFuture +import com.softwaremill.sttp.testing.ConvertToFuture package object monix { diff --git a/implementations/scalaz/src/test/scala/com/softwaremill/sttp/impl/scalaz/package.scala b/implementations/scalaz/src/test/scala/com/softwaremill/sttp/impl/scalaz/package.scala index 8ac6446..27b4759 100644 --- a/implementations/scalaz/src/test/scala/com/softwaremill/sttp/impl/scalaz/package.scala +++ b/implementations/scalaz/src/test/scala/com/softwaremill/sttp/impl/scalaz/package.scala @@ -1,6 +1,6 @@ package com.softwaremill.sttp.impl -import com.softwaremill.sttp.testing.streaming.ConvertToFuture +import com.softwaremill.sttp.testing.ConvertToFuture import _root_.scalaz.concurrent.Task import _root_.scalaz.{-\/, \/-} diff --git a/okhttp-backend/monix/src/test/scala/com/softwaremill/sttp/okhttp/monix/OkHttpMonixHttpTest.scala b/okhttp-backend/monix/src/test/scala/com/softwaremill/sttp/okhttp/monix/OkHttpMonixHttpTest.scala new file mode 100644 index 0000000..4644bda --- /dev/null +++ b/okhttp-backend/monix/src/test/scala/com/softwaremill/sttp/okhttp/monix/OkHttpMonixHttpTest.scala @@ -0,0 +1,12 @@ +package com.softwaremill.sttp.okhttp.monix + +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.impl.monix.convertMonixTaskToFuture +import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} +import monix.eval.Task + +class OkHttpMonixHttpTest extends HttpTest[Task] { + + override implicit val backend: SttpBackend[Task, Nothing] = OkHttpMonixBackend() + override implicit val convertToFuture: ConvertToFuture[Task] = convertMonixTaskToFuture +} diff --git a/okhttp-backend/monix/src/test/scala/com/softwaremill/sttp/okhttp/monix/OkHttpMonixStreamingTest.scala b/okhttp-backend/monix/src/test/scala/com/softwaremill/sttp/okhttp/monix/OkHttpMonixStreamingTest.scala new file mode 100644 index 0000000..af9ea9c --- /dev/null +++ b/okhttp-backend/monix/src/test/scala/com/softwaremill/sttp/okhttp/monix/OkHttpMonixStreamingTest.scala @@ -0,0 +1,26 @@ +package com.softwaremill.sttp.okhttp.monix + +import java.nio.ByteBuffer + +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.impl.monix.MonixTestStreamingBackend +import com.softwaremill.sttp.testing.streaming.{StreamingTest, TestStreamingBackend} +import monix.eval.Task +import monix.reactive.Observable + +class OkHttpMonixStreamingTest extends StreamingTest[Task, Observable[ByteBuffer]] { + + override val testStreamingBackend: TestStreamingBackend[Task, Observable[ByteBuffer]] = + new OkHttpMonixTestStreamingBackend +} + +class OkHttpMonixTestStreamingBackend extends MonixTestStreamingBackend[ByteBuffer] { + + import monix.execution.Scheduler.Implicits.global + + override def toByteArray(v: ByteBuffer): Array[Byte] = v.array() + override def fromByteArray(v: Array[Byte]): ByteBuffer = ByteBuffer.wrap(v) + + override implicit val backend: SttpBackend[Task, Observable[ByteBuffer]] = + OkHttpMonixBackend() +} diff --git a/okhttp-backend/src/test/scala/com/softwaremill/sttp/okhttp/OkHttpFutureHttpTest.scala b/okhttp-backend/src/test/scala/com/softwaremill/sttp/okhttp/OkHttpFutureHttpTest.scala new file mode 100644 index 0000000..8ceda94 --- /dev/null +++ b/okhttp-backend/src/test/scala/com/softwaremill/sttp/okhttp/OkHttpFutureHttpTest.scala @@ -0,0 +1,12 @@ +package com.softwaremill.sttp.okhttp + +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} + +import scala.concurrent.Future + +class OkHttpFutureHttpTest extends HttpTest[Future] { + + override implicit val backend: SttpBackend[Future, Nothing] = OkHttpFutureBackend() + override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future +} diff --git a/okhttp-backend/src/test/scala/com/softwaremill/sttp/okhttp/OkHttpSyncHttpTest.scala b/okhttp-backend/src/test/scala/com/softwaremill/sttp/okhttp/OkHttpSyncHttpTest.scala new file mode 100644 index 0000000..bae87d7 --- /dev/null +++ b/okhttp-backend/src/test/scala/com/softwaremill/sttp/okhttp/OkHttpSyncHttpTest.scala @@ -0,0 +1,10 @@ +package com.softwaremill.sttp.okhttp + +import com.softwaremill.sttp.{Id, SttpBackend} +import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} + +class OkHttpSyncHttpTest extends HttpTest[Id] { + + override implicit val backend: SttpBackend[Id, Nothing] = OkHttpSyncBackend() + override implicit val convertToFuture: ConvertToFuture[Id] = ConvertToFuture.id +} diff --git a/project/PollingUtils.scala b/project/PollingUtils.scala new file mode 100644 index 0000000..34b92ac --- /dev/null +++ b/project/PollingUtils.scala @@ -0,0 +1,45 @@ +import java.io.FileNotFoundException +import java.net.{ConnectException, URL} + +import scala.concurrent.TimeoutException +import scala.concurrent.duration._ + +object PollingUtils { + + def waitUntilServerAvailable(url: URL): Unit = { + val connected = poll(5.seconds, 250.milliseconds)({ + urlConnectionAvailable(url) + }) + if (!connected) { + throw new TimeoutException(s"Failed to connect to $url") + } + } + + def poll(timeout: FiniteDuration, interval: FiniteDuration)(poll: => Boolean): Boolean = { + val start = System.nanoTime() + + def go(): Boolean = { + if (poll) { + true + } else if ((System.nanoTime() - start) > timeout.toNanos) { + false + } else { + Thread.sleep(interval.toMillis) + go() + } + } + go() + } + + def urlConnectionAvailable(url: URL): Boolean = { + try { + url.openConnection() + .getInputStream + .close() + true + } catch { + case _: ConnectException => false + case _: FileNotFoundException => true // on 404 + } + } +} diff --git a/project/plugins.sbt b/project/plugins.sbt index 1d83f24..b334460 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,5 @@ -addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15") +// using '-coursier' because of https://github.com/lucidsoftware/neo-sbt-scalafmt/issues/64 +addSbtPlugin("com.lucidchart" % "sbt-scalafmt-coursier" % "1.15") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0") @@ -6,4 +7,6 @@ addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.1.0") addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.7") -addSbtPlugin("com.updateimpact" % "updateimpact-sbt-plugin" % "2.1.3")
\ No newline at end of file +addSbtPlugin("com.updateimpact" % "updateimpact-sbt-plugin" % "2.1.3") + +addSbtPlugin("io.spray" % "sbt-revolver" % "0.9.1") diff --git a/test-server/src/main/resources/binaryfile.jpg b/test-server/src/main/resources/binaryfile.jpg Binary files differnew file mode 100644 index 0000000..b9f5c5a --- /dev/null +++ b/test-server/src/main/resources/binaryfile.jpg diff --git a/test-server/src/main/resources/textfile.txt b/test-server/src/main/resources/textfile.txt new file mode 100644 index 0000000..9904f90 --- /dev/null +++ b/test-server/src/main/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/test-server/src/main/scala/com/softwaremill/sttp/server/TestHttpServer.scala b/test-server/src/main/scala/com/softwaremill/sttp/server/TestHttpServer.scala new file mode 100644 index 0000000..906154d --- /dev/null +++ b/test-server/src/main/scala/com/softwaremill/sttp/server/TestHttpServer.scala @@ -0,0 +1,197 @@ +package com.softwaremill.sttp.server + +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} + +object TestHttpServer { + + def main(args: Array[String]): Unit = { + val port = args.headOption.map(_.toInt).getOrElse(51823) + + Await.result(new TestHttpServer(port).start(), 10.seconds) + } +} + +class TestHttpServer(port: Int) extends AutoCloseable { + + import scala.concurrent.ExecutionContext.Implicits.global + + 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("src/main/resources/textfile.txt") + private val binaryFile = new java.io.File("src/main/resources/binaryfile.jpg") + 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] = { + Http().bindAndHandle(serverRoutes, "localhost", port) + } + + def close(): Unit = { + Await.result( + actorSystem.terminate(), + 10.seconds + ) + } +} diff --git a/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala b/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala deleted file mode 100644 index 55e21b8..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala +++ /dev/null @@ -1,702 +0,0 @@ -package com.softwaremill.sttp - -import java.io.{ByteArrayInputStream, IOException} -import java.nio.ByteBuffer -import java.nio.file.Paths -import java.time.{ZoneId, ZonedDateTime} - -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._ -import akka.http.scaladsl.server.Route -import akka.http.scaladsl.server.directives.Credentials -import akka.util.ByteString -import better.files._ -import com.softwaremill.sttp.akkahttp.AkkaHttpBackend -import com.softwaremill.sttp.asynchttpclient.cats.AsyncHttpClientCatsBackend -import com.softwaremill.sttp.asynchttpclient.future.AsyncHttpClientFutureBackend -import com.softwaremill.sttp.asynchttpclient.monix.AsyncHttpClientMonixBackend -import com.softwaremill.sttp.asynchttpclient.scalaz.AsyncHttpClientScalazBackend -import com.softwaremill.sttp.impl.cats.convertCatsIOToFuture -import com.softwaremill.sttp.impl.monix.convertMonixTaskToFuture -import com.softwaremill.sttp.impl.scalaz.convertScalazTaskToFuture -import com.softwaremill.sttp.okhttp.monix.OkHttpMonixBackend -import com.softwaremill.sttp.okhttp.{OkHttpFutureBackend, OkHttpSyncBackend} -import com.softwaremill.sttp.testing.streaming.ConvertToFuture -import com.typesafe.scalalogging.StrictLogging -import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures} -import org.scalatest.{path => _, _} - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.Future -import scala.concurrent.duration._ -import scala.language.higherKinds - -class BasicTests - extends FlatSpec - with Matchers - with BeforeAndAfterAll - with ScalaFutures - with OptionValues - with StrictLogging - with IntegrationPatience - with TestHttpServer - with ForceWrapped - with BeforeAndAfterEach { - - override def afterEach() { - val file = File(outPath) - if (file.exists) file.delete() - } - - 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("tests/src/test/resources/textfile.txt") - private val binaryFile = - new java.io.File("tests/src/test/resources/binaryfile.jpg") - private val outPath = Paths.get("out") - private val textWithSpecialCharacters = "Żółć!" - - override 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(" ")) - } - } - } - } ~ 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)) - } - } - - override def port = 51823 - - var closeBackends: List[() => Unit] = Nil - - runTests("HttpURLConnection")(HttpURLConnectionBackend(), ConvertToFuture.id) - runTests("TryHttpURLConnection")(TryHttpURLConnectionBackend(), ConvertToFuture.scalaTry) - runTests("Akka HTTP")(AkkaHttpBackend.usingActorSystem(actorSystem), ConvertToFuture.future) - runTests("Async Http Client - Future")(AsyncHttpClientFutureBackend(), ConvertToFuture.future) - runTests("Async Http Client - Scalaz")(AsyncHttpClientScalazBackend(), convertScalazTaskToFuture) - runTests("Async Http Client - Monix")(AsyncHttpClientMonixBackend(), convertMonixTaskToFuture) - runTests("Async Http Client - Cats IO")(AsyncHttpClientCatsBackend[cats.effect.IO](), convertCatsIOToFuture) - runTests("OkHttpSyncClientHandler")(OkHttpSyncBackend(), ConvertToFuture.id) - runTests("OkHttpAsyncClientHandler - Future")(OkHttpFutureBackend(), ConvertToFuture.future) - runTests("OkHttpAsyncClientHandler - Monix")(OkHttpMonixBackend(), convertMonixTaskToFuture) - - def runTests[R[_]](name: String)(implicit - backend: SttpBackend[R, Nothing], - convertToFuture: ConvertToFuture[R]): Unit = { - - closeBackends = (() => backend.close()) :: closeBackends - - val postEcho = sttp.post(uri"$endpoint/echo") - val testBody = "this is the body" - val testBodyBytes = testBody.getBytes("UTF-8") - val expectedPostEchoResponse = "POST /echo this is the body" - - val sttpIgnore = com.softwaremill.sttp.ignore - - parseResponseTests() - parameterTests() - bodyTests() - headerTests() - errorsTests() - cookiesTests() - authTests() - compressionTests() - downloadFileTests() - multipartTests() - redirectTests() - timeoutTests() - emptyResponseTests() - encodingTests() - - def parseResponseTests(): Unit = { - name should "parse response as string" in { - val response = postEcho.body(testBody).send().force() - response.unsafeBody should be(expectedPostEchoResponse) - } - - name should "parse response as string with mapping using map" in { - val response = postEcho - .body(testBody) - .response(asString.map(_.length)) - .send() - .force() - response.unsafeBody should be(expectedPostEchoResponse.length) - } - - name should "parse response as string with mapping using mapResponse" in { - val response = postEcho - .body(testBody) - .mapResponse(_.length) - .send() - .force() - response.unsafeBody should be(expectedPostEchoResponse.length) - } - - name should "parse response 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) - } - - name should "parse response 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) - } - } - - def parameterTests(): Unit = { - name should "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") - } - } - - def bodyTests(): Unit = { - name should "post a string" in { - val response = postEcho.body(testBody).send().force() - response.unsafeBody should be(expectedPostEchoResponse) - } - - name should "post a byte array" in { - val response = - postEcho.body(testBodyBytes).send().force() - response.unsafeBody should be(expectedPostEchoResponse) - } - - name should "post an input stream" in { - val response = postEcho - .body(new ByteArrayInputStream(testBodyBytes)) - .send() - .force() - response.unsafeBody should be(expectedPostEchoResponse) - } - - name should "post a byte buffer" in { - val response = postEcho - .body(ByteBuffer.wrap(testBodyBytes)) - .send() - .force() - response.unsafeBody should be(expectedPostEchoResponse) - } - - name should "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() - } - - name should "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() - } - - name should "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") - } - - name should "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") - } - - name should "post without a body" in { - val response = postEcho.send().force() - response.unsafeBody should be("POST /echo") - } - } - - def headerTests(): Unit = { - val getHeaders = sttp.get(uri"$endpoint/set_headers") - - name should "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)) - } - } - - def errorsTests(): Unit = { - val getHeaders = sttp.post(uri"$endpoint/set_headers") - - name should "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) - } - } - - def cookiesTests(): Unit = { - name should "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")) - )) - } - - name should "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 - )) - } - } - - def authTests(): Unit = { - val secureBasic = sttp.get(uri"$endpoint/secure_basic") - - name should "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""")) - } - - name should "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!") - } - } - - def compressionTests(): Unit = { - val compress = sttp.get(uri"$endpoint/compress") - val decompressedBody = "I'm compressed!" - - name should "decompress using the default accept encoding header" in { - val req = compress - val resp = req.send().force() - resp.unsafeBody should be(decompressedBody) - } - - name should "decompress using gzip" in { - val req = - compress.header("Accept-Encoding", "gzip", replaceExisting = true) - val resp = req.send().force() - resp.unsafeBody should be(decompressedBody) - } - - name should "decompress using deflate" in { - val req = - compress.header("Accept-Encoding", "deflate", replaceExisting = true) - val resp = req.send().force() - resp.unsafeBody should be(decompressedBody) - } - - name should "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) - } - } - - def downloadFileTests(): Unit = { - import CustomMatchers._ - - name should "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) - } - - name should "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) - } - - name should "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) - } - - name should "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) - } - - name should "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" - } - - name should "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" - - } - - name should "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) - } - } - - def multipartTests(): Unit = { - val mp = sttp.post(uri"$endpoint/multipart") - - name should "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") - } - - name should "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)") - } - - name should "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() - } - } - - def redirectTests(): Unit = { - val r1 = sttp.post(uri"$endpoint/redirect/r1") - val r2 = sttp.post(uri"$endpoint/redirect/r2") - val r3 = sttp.post(uri"$endpoint/redirect/r3") - val r4response = "819" - val loop = sttp.post(uri"$endpoint/redirect/loop") - - name should "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) - } - - name should "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) - } - - name should "redirect when redirects should be followed" in { - val resp = r2.send().force() - resp.code should be(200) - resp.unsafeBody should be(r4response) - } - - name should "redirect twice when redirects should be followed" in { - val resp = r1.send().force() - resp.code should be(200) - resp.unsafeBody should be(r4response) - } - - name should "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) - } - - name should "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) - } - - name should "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) - } - - name should "break redirect loops" in { - val resp = loop.send().force() - resp.code should be(0) - resp.history should have size (FollowRedirectsBackend.MaxRedirects) - } - - name should "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) - } - - name should "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) - } - } - - def timeoutTests(): Unit = { - name should "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() - } - } - - name should "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") - } - } - - def emptyResponseTests(): Unit = { - val postEmptyResponse = sttp - .post(uri"$endpoint/empty_unauthorized_response") - .body("{}") - .contentType("application/json") - - name should "parse an empty error response as empty string" in { - val response = postEmptyResponse.send().force() - response.body should be(Left("")) - } - } - - def encodingTests(): Unit = { - name should "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 = { - closeBackends.foreach(_()) - super.afterAll() - } -} diff --git a/tests/src/test/scala/com/softwaremill/sttp/EvalScala.scala b/tests/src/test/scala/com/softwaremill/sttp/EvalScala.scala new file mode 100644 index 0000000..bac1537 --- /dev/null +++ b/tests/src/test/scala/com/softwaremill/sttp/EvalScala.scala @@ -0,0 +1,11 @@ +package com.softwaremill.sttp + +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/tests/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala b/tests/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala new file mode 100644 index 0000000..599913f --- /dev/null +++ b/tests/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala @@ -0,0 +1,9 @@ +package com.softwaremill.sttp + +import com.softwaremill.sttp.testing.{ConvertToFuture, 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/tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala b/tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala deleted file mode 100644 index b2b44a2..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala +++ /dev/null @@ -1,87 +0,0 @@ -package com.softwaremill.sttp - -import akka.http.scaladsl.server.Directives._ -import akka.http.scaladsl.server.Route -import com.softwaremill.sttp.streaming._ -import com.softwaremill.sttp.testing.streaming.TestStreamingBackend -import com.typesafe.scalalogging.StrictLogging -import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers} - -import scala.language.higherKinds - -class StreamingTests - extends FlatSpec - with Matchers - with BeforeAndAfterAll - with StrictLogging - with TestHttpServer - with ForceWrapped { - - override val serverRoutes: Route = - path("echo") { - post { - parameterMap { _ => - entity(as[String]) { body: String => - complete(body) - } - } - } - } - - override def port = 51824 - - val body = "streaming test" - - var closeBackends: List[() => Unit] = Nil - - runTests("Akka Http", new AkkaHttpStreamingTests(actorSystem)) - runTests("Monix Async Http Client", new AsyncHttpClientMonixStreamingTests) - runTests("Monix OkHttp", new OkHttpMonixStreamingTests) - runTests("fs2 Async Http Client", new AsyncHttpClientFs2StreamingTests) - - def runTests[R[_], S](name: String, testStreamingBackend: TestStreamingBackend[R, S]): Unit = { - import testStreamingBackend._ - - closeBackends = (() => backend.close()) :: closeBackends - - name should "stream request body" in { - val response = sttp - .post(uri"$endpoint/echo") - .streamBody(bodyProducer(body)) - .send() - .force() - - response.unsafeBody shouldBe body - } - - it should "receive a stream" in { - val response = sttp - .post(uri"$endpoint/echo") - .body(body) - .response(asStream[S]) - .send() - .force() - - bodyConsumer(response.unsafeBody).force() shouldBe body - } - - it should "receive a stream from an https site" in { - val response = 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() - .force() - - bodyConsumer(response.unsafeBody).force() should include("</div>") - } - } - - override protected def afterAll(): Unit = { - closeBackends.foreach(_()) - super.afterAll() - } - -} diff --git a/tests/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala b/tests/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala new file mode 100644 index 0000000..19b48fd --- /dev/null +++ b/tests/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala @@ -0,0 +1,11 @@ +package com.softwaremill.sttp + +import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} + +import scala.util.Try + +class TryHttpURLConnectionHttpTest extends HttpTest[Try] { + + override implicit val backend: SttpBackend[Try, Nothing] = TryHttpURLConnectionBackend() + override implicit val convertToFuture: ConvertToFuture[Try] = ConvertToFuture.scalaTry +} diff --git a/tests/src/test/scala/com/softwaremill/sttp/streaming/AsyncHttpClientFs2StreamingTests.scala b/tests/src/test/scala/com/softwaremill/sttp/streaming/AsyncHttpClientFs2StreamingTests.scala deleted file mode 100644 index 8959ccc..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/streaming/AsyncHttpClientFs2StreamingTests.scala +++ /dev/null @@ -1,30 +0,0 @@ -package com.softwaremill.sttp.streaming - -import java.nio.ByteBuffer - -import cats.effect._ -import cats.instances.string._ -import com.softwaremill.sttp.SttpBackend -import com.softwaremill.sttp.asynchttpclient.fs2.AsyncHttpClientFs2Backend -import com.softwaremill.sttp.impl.cats.convertCatsIOToFuture -import com.softwaremill.sttp.testing.streaming.{ConvertToFuture, TestStreamingBackend} -import fs2.{Chunk, Stream, text} - -class AsyncHttpClientFs2StreamingTests extends TestStreamingBackend[IO, Stream[IO, ByteBuffer]] { - - override implicit val backend: SttpBackend[IO, Stream[IO, ByteBuffer]] = - AsyncHttpClientFs2Backend[IO]() - - override implicit val convertToFuture: ConvertToFuture[IO] = convertCatsIOToFuture - - override def bodyProducer(body: String): Stream[IO, ByteBuffer] = - Stream.emits(body.getBytes("utf-8").map(b => ByteBuffer.wrap(Array(b)))) - - override def bodyConsumer(stream: Stream[IO, ByteBuffer]): IO[String] = - stream - .map(bb => Chunk.array(bb.array)) - .through(text.utf8DecodeC) - .compile - .foldMonoid - -} diff --git a/tests/src/test/scala/com/softwaremill/sttp/streaming/AsyncHttpClientMonixStreamingTests.scala b/tests/src/test/scala/com/softwaremill/sttp/streaming/AsyncHttpClientMonixStreamingTests.scala deleted file mode 100644 index faebf8b..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/streaming/AsyncHttpClientMonixStreamingTests.scala +++ /dev/null @@ -1,18 +0,0 @@ -package com.softwaremill.sttp.streaming - -import java.nio.ByteBuffer - -import com.softwaremill.sttp.SttpBackend -import com.softwaremill.sttp.asynchttpclient.monix.AsyncHttpClientMonixBackend -import com.softwaremill.sttp.impl.monix.MonixTestStreamingBackend -import monix.eval.Task -import monix.reactive.Observable - -class AsyncHttpClientMonixStreamingTests extends MonixTestStreamingBackend { - - import monix.execution.Scheduler.Implicits.global - - override implicit val backend: SttpBackend[Task, Observable[ByteBuffer]] = - AsyncHttpClientMonixBackend() - -} diff --git a/tests/src/test/scala/com/softwaremill/sttp/streaming/OkHttpMonixStreamingTests.scala b/tests/src/test/scala/com/softwaremill/sttp/streaming/OkHttpMonixStreamingTests.scala deleted file mode 100644 index 27a4517..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/streaming/OkHttpMonixStreamingTests.scala +++ /dev/null @@ -1,18 +0,0 @@ -package com.softwaremill.sttp.streaming - -import java.nio.ByteBuffer - -import com.softwaremill.sttp.SttpBackend -import com.softwaremill.sttp.impl.monix.MonixTestStreamingBackend -import com.softwaremill.sttp.okhttp.monix.OkHttpMonixBackend -import monix.eval.Task -import monix.reactive.Observable - -class OkHttpMonixStreamingTests extends MonixTestStreamingBackend { - - import monix.execution.Scheduler.Implicits.global - - override implicit val backend: SttpBackend[Task, Observable[ByteBuffer]] = - OkHttpMonixBackend() - -} diff --git a/tests/src/test/scala/com/softwaremill/sttp/testHelpers.scala b/tests/src/test/scala/com/softwaremill/sttp/testHelpers.scala deleted file mode 100644 index 12fe770..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/testHelpers.scala +++ /dev/null @@ -1,82 +0,0 @@ -package com.softwaremill.sttp - -import java.nio.file.{Files, Paths} -import java.{io, util} - -import akka.actor.ActorSystem -import akka.http.scaladsl.Http -import akka.http.scaladsl.server.Route -import akka.stream.ActorMaterializer -import org.scalatest.concurrent.{PatienceConfiguration, ScalaFutures} -import org.scalatest.exceptions.TestFailedException -import org.scalatest.matchers.{MatchResult, Matcher} -import org.scalatest.{BeforeAndAfterAll, Suite} - -import scala.concurrent.duration._ -import scala.language.higherKinds - -trait TestHttpServer extends BeforeAndAfterAll with ScalaFutures with TestingPatience { - this: Suite => - protected implicit val actorSystem: ActorSystem = ActorSystem("sttp-test") - import actorSystem.dispatcher - - protected implicit val materializer = ActorMaterializer() - protected val endpoint = uri"http://localhost:$port" - - override protected def beforeAll(): Unit = { - Http().bindAndHandle(serverRoutes, "localhost", port).futureValue - } - - override protected def afterAll(): Unit = { - actorSystem.terminate().futureValue - } - - def serverRoutes: Route - def port: Int -} - -trait ForceWrapped extends ScalaFutures with TestingPatience { this: Suite => - type ConvertToFuture[R[_]] = - com.softwaremill.sttp.testing.streaming.ConvertToFuture[R] - - implicit class ForceDecorator[R[_], T](wrapped: R[T]) { - def force()(implicit ctf: ConvertToFuture[R]): T = { - try { - ctf.toFuture(wrapped).futureValue - } catch { - case e: TestFailedException if e.getCause != null => throw e.getCause - } - } - } -} - -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)) - } -} - -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) -} - -trait TestingPatience extends PatienceConfiguration { - override implicit val patienceConfig: PatienceConfig = - PatienceConfig(timeout = 5.seconds, interval = 150.milliseconds) -} |