From 92e10991df0d168d1972d4618fcc7e02e2e0a0fa Mon Sep 17 00:00:00 2001 From: Sam Guymer Date: Thu, 17 May 2018 20:11:22 +1000 Subject: Move backend tests into their projects Instead of having a single project which tests all backends, each backend now implements a http test trait along with a streaming test trait if it supports streaming. The test http server has been moved into its own project and is started automatically before running a backends test. This allows each backend to be tested without the possibility of dependency eviction from another backend or the test http server. It also has the side effect of parallelizing the tests providing a speed up when run with multiple cores. --- .../sttp/akkahttp/AkkaHttpClientHttpTest.scala | 12 + .../sttp/akkahttp/AkkaHttpStreamingTests.scala | 40 ++ .../cats/AsyncHttpClientCatsHttpTest.scala | 12 + .../fs2/AsyncHttpClientFs2HttpStreamingTest.scala | 40 ++ .../fs2/AsyncHttpClientFs2HttpTest.scala | 15 + .../future/AsyncHttpClientFutureHttpTest.scala | 12 + .../monix/AsyncHttpClientMonixHttpTest.scala | 15 + .../monix/AsyncHttpClientMonixStreamingTest.scala | 26 + .../scalaz/AsyncHttpClientScalazHttpTest.scala | 13 + build.sbt | 60 +- .../scala/com/softwaremill/sttp/MonadError.scala | 9 + .../sttp/testing/ConvertToFuture.scala | 26 + .../softwaremill/sttp/testing/CustomMatchers.scala | 22 + .../softwaremill/sttp/testing/ForceWrapped.scala | 30 + .../com/softwaremill/sttp/testing/HttpTest.scala | 526 +++++++++++++++ .../sttp/testing/streaming/ConvertToFuture.scala | 26 - .../sttp/testing/streaming/StreamingTest.scala | 65 ++ .../testing/streaming/TestStreamingBackend.scala | 1 + .../com/softwaremill/sttp/impl/cats/package.scala | 2 +- .../impl/monix/MonixTestStreamingBackend.scala | 22 +- .../com/softwaremill/sttp/impl/monix/package.scala | 2 +- .../softwaremill/sttp/impl/scalaz/package.scala | 2 +- .../sttp/okhttp/monix/OkHttpMonixHttpTest.scala | 12 + .../okhttp/monix/OkHttpMonixStreamingTest.scala | 26 + .../sttp/okhttp/OkHttpFutureHttpTest.scala | 12 + .../sttp/okhttp/OkHttpSyncHttpTest.scala | 10 + project/PollingUtils.scala | 45 ++ project/plugins.sbt | 7 +- test-server/src/main/resources/binaryfile.jpg | Bin 0 -> 42010 bytes test-server/src/main/resources/textfile.txt | 100 +++ .../softwaremill/sttp/server/TestHttpServer.scala | 197 ++++++ .../scala/com/softwaremill/sttp/BasicTests.scala | 702 --------------------- .../scala/com/softwaremill/sttp/EvalScala.scala | 11 + .../sttp/HttpURLConnectionHttpTest.scala | 9 + .../com/softwaremill/sttp/StreamingTests.scala | 87 --- .../sttp/TryHttpURLConnectionHttpTest.scala | 11 + .../sttp/streaming/AkkaHttpStreamingTests.scala | 29 - .../AsyncHttpClientFs2StreamingTests.scala | 30 - .../AsyncHttpClientMonixStreamingTests.scala | 18 - .../sttp/streaming/OkHttpMonixStreamingTests.scala | 18 - .../scala/com/softwaremill/sttp/testHelpers.scala | 82 --- 41 files changed, 1367 insertions(+), 1017 deletions(-) create mode 100644 akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpClientHttpTest.scala create mode 100644 akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTests.scala create mode 100644 async-http-client-backend/cats/src/test/scala/com/softwaremill/sttp/asynchttpclient/cats/AsyncHttpClientCatsHttpTest.scala create mode 100644 async-http-client-backend/fs2/src/test/scala/com/softwaremill/sttp/asynchttpclient/fs2/AsyncHttpClientFs2HttpStreamingTest.scala create mode 100644 async-http-client-backend/fs2/src/test/scala/com/softwaremill/sttp/asynchttpclient/fs2/AsyncHttpClientFs2HttpTest.scala create mode 100644 async-http-client-backend/future/src/test/scala/com/softwaremill/sttp/asynchttpclient/future/AsyncHttpClientFutureHttpTest.scala create mode 100644 async-http-client-backend/monix/src/test/scala/com/softwaremill/sttp/asynchttpclient/monix/AsyncHttpClientMonixHttpTest.scala create mode 100644 async-http-client-backend/monix/src/test/scala/com/softwaremill/sttp/asynchttpclient/monix/AsyncHttpClientMonixStreamingTest.scala create mode 100644 async-http-client-backend/scalaz/src/test/scala/com/softwaremill/sttp/asynchttpclient/scalaz/AsyncHttpClientScalazHttpTest.scala create mode 100644 core/src/test/scala/com/softwaremill/sttp/testing/ConvertToFuture.scala create mode 100644 core/src/test/scala/com/softwaremill/sttp/testing/CustomMatchers.scala create mode 100644 core/src/test/scala/com/softwaremill/sttp/testing/ForceWrapped.scala create mode 100644 core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala delete mode 100644 core/src/test/scala/com/softwaremill/sttp/testing/streaming/ConvertToFuture.scala create mode 100644 core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala create mode 100644 okhttp-backend/monix/src/test/scala/com/softwaremill/sttp/okhttp/monix/OkHttpMonixHttpTest.scala create mode 100644 okhttp-backend/monix/src/test/scala/com/softwaremill/sttp/okhttp/monix/OkHttpMonixStreamingTest.scala create mode 100644 okhttp-backend/src/test/scala/com/softwaremill/sttp/okhttp/OkHttpFutureHttpTest.scala create mode 100644 okhttp-backend/src/test/scala/com/softwaremill/sttp/okhttp/OkHttpSyncHttpTest.scala create mode 100644 project/PollingUtils.scala create mode 100644 test-server/src/main/resources/binaryfile.jpg create mode 100644 test-server/src/main/resources/textfile.txt create mode 100644 test-server/src/main/scala/com/softwaremill/sttp/server/TestHttpServer.scala delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala create mode 100644 tests/src/test/scala/com/softwaremill/sttp/EvalScala.scala create mode 100644 tests/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala create mode 100644 tests/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/streaming/AkkaHttpStreamingTests.scala delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/streaming/AsyncHttpClientFs2StreamingTests.scala delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/streaming/AsyncHttpClientMonixStreamingTests.scala delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/streaming/OkHttpMonixStreamingTests.scala delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/testHelpers.scala 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/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTests.scala b/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTests.scala new file mode 100644 index 0000000..e8ab9d7 --- /dev/null +++ b/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTests.scala @@ -0,0 +1,40 @@ +package com.softwaremill.sttp.akkahttp + +import akka.NotUsed +import akka.actor.ActorSystem +import akka.stream.{ActorMaterializer, Materializer} +import akka.stream.scaladsl.Source +import akka.util.ByteString +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.testing.ConvertToFuture +import com.softwaremill.sttp.testing.streaming.{StreamingTest, TestStreamingBackend} + +import scala.concurrent.Future + +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]] = + AkkaHttpBackend.usingActorSystem(actorSystem) + + override implicit val convertToFuture: ConvertToFuture[Future] = + ConvertToFuture.future + + override def bodyProducer(body: String): Source[ByteString, NotUsed] = + Source.single(ByteString(body)) + + override def bodyConsumer(stream: Source[ByteString, Any]): Future[String] = + stream.map(_.utf8String).runReduce(_ + _) + +} 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 +} diff --git a/build.sbt b/build.sbt index 1f510e1..461a88e 100644 --- a/build.sbt +++ b/build.sbt @@ -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/ConvertToFuture.scala b/core/src/test/scala/com/softwaremill/sttp/testing/ConvertToFuture.scala new file mode 100644 index 0000000..25a7d8e --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/ConvertToFuture.scala @@ -0,0 +1,26 @@ +package com.softwaremill.sttp.testing + +import com.softwaremill.sttp.Id +import scala.concurrent.Future +import scala.language.higherKinds +import scala.util.Try + +trait ConvertToFuture[R[_]] { + def toFuture[T](value: R[T]): Future[T] +} + +object ConvertToFuture { + + val id: ConvertToFuture[Id] = new ConvertToFuture[Id] { + override def toFuture[T](value: Id[T]): Future[T] = + Future.successful(value) + } + + val future: ConvertToFuture[Future] = new ConvertToFuture[Future] { + override def toFuture[T](value: Future[T]): Future[T] = value + } + + val scalaTry: ConvertToFuture[Try] = new ConvertToFuture[Try] { + override def toFuture[T](value: Try[T]): Future[T] = Future.fromTry(value) + } +} 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/ConvertToFuture.scala b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/ConvertToFuture.scala deleted file mode 100644 index 9438890..0000000 --- a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/ConvertToFuture.scala +++ /dev/null @@ -1,26 +0,0 @@ -package com.softwaremill.sttp.testing.streaming - -import com.softwaremill.sttp.Id -import scala.concurrent.Future -import scala.language.higherKinds -import scala.util.Try - -trait ConvertToFuture[R[_]] { - def toFuture[T](value: R[T]): Future[T] -} - -object ConvertToFuture { - - val id: ConvertToFuture[Id] = new ConvertToFuture[Id] { - override def toFuture[T](value: Id[T]): Future[T] = - Future.successful(value) - } - - val future: ConvertToFuture[Future] = new ConvertToFuture[Future] { - override def toFuture[T](value: Future[T]): Future[T] = value - } - - val scalaTry: ConvertToFuture[Try] = new ConvertToFuture[Try] { - override def toFuture[T](value: Try[T]): Future[T] = Future.fromTry(value) - } -} 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("") + } + } + + 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 new file mode 100644 index 0000000..b9f5c5a Binary files /dev/null and b/test-server/src/main/resources/binaryfile.jpg differ 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("") - } - } - - 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/AkkaHttpStreamingTests.scala b/tests/src/test/scala/com/softwaremill/sttp/streaming/AkkaHttpStreamingTests.scala deleted file mode 100644 index 691df81..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/streaming/AkkaHttpStreamingTests.scala +++ /dev/null @@ -1,29 +0,0 @@ -package com.softwaremill.sttp.streaming - -import akka.NotUsed -import akka.actor.ActorSystem -import akka.stream.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 scala.concurrent.Future - -class AkkaHttpStreamingTests(actorSystem: ActorSystem)(implicit materializer: Materializer) - extends TestStreamingBackend[Future, Source[ByteString, Any]] { - - override implicit val backend: SttpBackend[Future, Source[ByteString, Any]] = - AkkaHttpBackend.usingActorSystem(actorSystem) - - override implicit val convertToFuture: ConvertToFuture[Future] = - ConvertToFuture.future - - override def bodyProducer(body: String): Source[ByteString, NotUsed] = - Source.single(ByteString(body)) - - override def bodyConsumer(stream: Source[ByteString, Any]): Future[String] = - stream.map(_.utf8String).runReduce(_ + _) - -} 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) -} -- cgit v1.2.3 From bcb94e252a96c78b1db29aebe47b18bfd337e764 Mon Sep 17 00:00:00 2001 From: Sam Guymer Date: Sun, 20 May 2018 21:02:37 +1000 Subject: Code review updates Remove tests sub project, all tests are now in core. Remove TestStreamingBackend, StreamingTest now has the required abstract methods. --- .../sttp/akkahttp/AkkaHttpStreamingTest.scala | 30 +++++++ .../sttp/akkahttp/AkkaHttpStreamingTests.scala | 40 --------- .../fs2/AsyncHttpClientFs2HttpStreamingTest.scala | 11 +-- .../monix/AsyncHttpClientMonixStreamingTest.scala | 19 +--- build.sbt | 37 ++------ .../scala/com/softwaremill/sttp/MonadError.scala | 9 -- .../sttp/HttpURLConnectionHttpTest.scala | 10 +++ .../com/softwaremill/sttp/IllTypedTests.scala | 37 ++++++++ .../sttp/TryHttpURLConnectionHttpTest.scala | 12 +++ .../com/softwaremill/sttp/testing/EvalScala.scala | 11 +++ .../sttp/testing/streaming/StreamingTest.scala | 14 ++- .../testing/streaming/TestStreamingBackend.scala | 16 ---- docs/backends/custom.rst | 29 +++++- .../sttp/impl/monix/MonixStreamingTest.scala | 26 ++++++ .../impl/monix/MonixTestStreamingBackend.scala | 28 ------ .../okhttp/monix/OkHttpMonixStreamingTest.scala | 19 +--- tests/src/test/resources/binaryfile.jpg | Bin 42010 -> 0 bytes tests/src/test/resources/logback.xml | 12 --- tests/src/test/resources/textfile.txt | 100 --------------------- .../scala/com/softwaremill/sttp/EvalScala.scala | 11 --- .../sttp/HttpURLConnectionHttpTest.scala | 9 -- .../com/softwaremill/sttp/IllTypedTests.scala | 34 ------- .../sttp/TryHttpURLConnectionHttpTest.scala | 11 --- 23 files changed, 175 insertions(+), 350 deletions(-) create mode 100644 akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTest.scala delete mode 100644 akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTests.scala create mode 100644 core/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala create mode 100644 core/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala create mode 100644 core/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala create mode 100644 core/src/test/scala/com/softwaremill/sttp/testing/EvalScala.scala delete mode 100644 core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala create mode 100644 implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/MonixStreamingTest.scala delete mode 100644 implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/MonixTestStreamingBackend.scala delete mode 100644 tests/src/test/resources/binaryfile.jpg delete mode 100644 tests/src/test/resources/logback.xml delete mode 100644 tests/src/test/resources/textfile.txt delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/EvalScala.scala delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala delete mode 100644 tests/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala diff --git a/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTest.scala b/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTest.scala new file mode 100644 index 0000000..494ecd2 --- /dev/null +++ b/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTest.scala @@ -0,0 +1,30 @@ +package com.softwaremill.sttp.akkahttp + +import akka.NotUsed +import akka.actor.ActorSystem +import akka.stream.ActorMaterializer +import akka.stream.scaladsl.Source +import akka.util.ByteString +import com.softwaremill.sttp.SttpBackend +import com.softwaremill.sttp.testing.ConvertToFuture +import com.softwaremill.sttp.testing.streaming.StreamingTest + +import scala.concurrent.Future + +class AkkaHttpStreamingTest extends StreamingTest[Future, Source[ByteString, Any]] { + + private implicit val actorSystem: ActorSystem = ActorSystem("sttp-test") + private implicit val materializer: ActorMaterializer = ActorMaterializer() + + override implicit val backend: SttpBackend[Future, Source[ByteString, Any]] = + AkkaHttpBackend.usingActorSystem(actorSystem) + + override implicit val convertToFuture: ConvertToFuture[Future] = + ConvertToFuture.future + + override def bodyProducer(body: String): Source[ByteString, NotUsed] = + Source.single(ByteString(body)) + + override def bodyConsumer(stream: Source[ByteString, Any]): Future[String] = + stream.map(_.utf8String).runReduce(_ + _) +} diff --git a/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTests.scala b/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTests.scala deleted file mode 100644 index e8ab9d7..0000000 --- a/akka-http-backend/src/test/scala/com/softwaremill/sttp/akkahttp/AkkaHttpStreamingTests.scala +++ /dev/null @@ -1,40 +0,0 @@ -package com.softwaremill.sttp.akkahttp - -import akka.NotUsed -import akka.actor.ActorSystem -import akka.stream.{ActorMaterializer, Materializer} -import akka.stream.scaladsl.Source -import akka.util.ByteString -import com.softwaremill.sttp.SttpBackend -import com.softwaremill.sttp.testing.ConvertToFuture -import com.softwaremill.sttp.testing.streaming.{StreamingTest, TestStreamingBackend} - -import scala.concurrent.Future - -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]] = - AkkaHttpBackend.usingActorSystem(actorSystem) - - override implicit val convertToFuture: ConvertToFuture[Future] = - ConvertToFuture.future - - override def bodyProducer(body: String): Source[ByteString, NotUsed] = - Source.single(ByteString(body)) - - override def bodyConsumer(stream: Source[ByteString, Any]): Future[String] = - stream.map(_.utf8String).runReduce(_ + _) - -} 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 index 565db5c..56bb18b 100644 --- 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 @@ -5,21 +5,14 @@ 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.streaming.StreamingTest 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 backend: SttpBackend[IO, Stream[IO, ByteBuffer]] = AsyncHttpClientFs2Backend[IO]() override implicit val convertToFuture: ConvertToFuture[IO] = new ConvertToFuture[IO] { 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 index 34c15ff..8e93074 100644 --- 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 @@ -3,24 +3,11 @@ 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 com.softwaremill.sttp.impl.monix.MonixStreamingTest import monix.eval.Task import monix.reactive.Observable -class AsyncHttpClientMonixStreamingTest extends StreamingTest[Task, Observable[ByteBuffer]] { +class AsyncHttpClientMonixStreamingTest extends MonixStreamingTest { - 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() + override implicit val backend: SttpBackend[Task, Observable[ByteBuffer]] = AsyncHttpClientMonixBackend() } diff --git a/build.sbt b/build.sbt index 461a88e..6bcaf1a 100644 --- a/build.sbt +++ b/build.sbt @@ -58,16 +58,17 @@ lazy val rootProject = (project in file(".")) json4s, braveBackend, prometheusBackend, - tests, testServer ) lazy val core: Project = (project in file("core")) .settings(commonSettings: _*) + .settings(testServerSettings: _*) .settings( name := "core", libraryDependencies ++= Seq( "com.github.pathikrit" %% "better-files" % "3.4.0" % "test", + "org.scala-lang" % "scala-compiler" % scalaVersion.value % "test", scalaTest % "test" ), publishArtifact in Test := true // allow implementations outside of this repo @@ -78,6 +79,7 @@ lazy val cats: Project = (project in file("implementations/cats")) .settings(commonSettings: _*) .settings( name := "cats", + publishArtifact in Test := true, libraryDependencies ++= Seq("org.typelevel" %% "cats-effect" % "1.0.0-RC") ) .dependsOn(core % "compile->compile;test->test") @@ -86,6 +88,7 @@ lazy val monix: Project = (project in file("implementations/monix")) .settings(commonSettings: _*) .settings( name := "monix", + publishArtifact in Test := true, libraryDependencies ++= Seq("io.monix" %% "monix" % "3.0.0-RC1") ) .dependsOn(core % "compile->compile;test->test") @@ -94,6 +97,7 @@ lazy val scalaz: Project = (project in file("implementations/scalaz")) .settings(commonSettings: _*) .settings( name := "scalaz", + publishArtifact in Test := true, libraryDependencies ++= Seq("org.scalaz" %% "scalaz-concurrent" % "7.2.22") ) .dependsOn(core % "compile->compile;test->test") @@ -235,36 +239,6 @@ lazy val prometheusBackend: Project = (project in file("metrics/prometheus-backe ) .dependsOn(core) -lazy val tests: Project = (project in file("tests")) - .settings(commonSettings: _*) - .settings(testServerSettings: _*) - .settings( - skip in publish := true, - name := "tests", - libraryDependencies ++= Seq( - akkaHttp, - akkaStreams, - scalaTest, - "com.typesafe.scala-logging" %% "scala-logging" % "3.9.0", - "com.github.pathikrit" %% "better-files" % "3.4.0", - "ch.qos.logback" % "logback-classic" % "1.2.3", - "org.scala-lang" % "scala-compiler" % scalaVersion.value - ).map(_ % "test") - ) - .dependsOn( - core % "compile->compile;test->test", - cats % "compile->compile;test->test", - monix % "compile->compile;test->test", - scalaz % "compile->compile;test->test", - akkaHttpBackend, - asyncHttpClientFutureBackend, - asyncHttpClientScalazBackend, - asyncHttpClientMonixBackend, - asyncHttpClientCatsBackend, - 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")) @@ -278,7 +252,6 @@ lazy val testServer: Project = project 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, diff --git a/core/src/main/scala/com/softwaremill/sttp/MonadError.scala b/core/src/main/scala/com/softwaremill/sttp/MonadError.scala index b5b1852..a783765 100644 --- a/core/src/main/scala/com/softwaremill/sttp/MonadError.scala +++ b/core/src/main/scala/com/softwaremill/sttp/MonadError.scala @@ -31,15 +31,6 @@ 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/HttpURLConnectionHttpTest.scala b/core/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala new file mode 100644 index 0000000..807209a --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala @@ -0,0 +1,10 @@ +package com.softwaremill.sttp + +import com.softwaremill.sttp.testing.ConvertToFuture +import com.softwaremill.sttp.testing.HttpTest + +class HttpURLConnectionHttpTest extends HttpTest[Id] { + + override implicit val backend: SttpBackend[Id, Nothing] = HttpURLConnectionBackend() + override implicit val convertToFuture: ConvertToFuture[Id] = ConvertToFuture.id +} diff --git a/core/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala b/core/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala new file mode 100644 index 0000000..4336a0c --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala @@ -0,0 +1,37 @@ +package com.softwaremill.sttp + +import com.softwaremill.sttp.testing.EvalScala +import org.scalatest.{FlatSpec, Matchers} + +import scala.tools.reflect.ToolBoxError + +class IllTypedTests extends FlatSpec with Matchers { + "compilation" should "fail when trying to stream using the default backend" in { + val thrown = intercept[ToolBoxError] { + EvalScala(""" + import com.softwaremill.sttp._ + + class MyStream[T]() + + implicit val sttpBackend = HttpURLConnectionBackend() + sttp.get(uri"http://example.com").response(asStream[MyStream[Byte]]).send() + """) + } + + thrown.getMessage should include( + "could not find implicit value for parameter backend: com.softwaremill.sttp.SttpBackend[R,MyStream[Byte]]" + ) + } + + "compilation" should "fail when trying to send a request without giving an URL" in { + val thrown = intercept[ToolBoxError] { + EvalScala(""" + import com.softwaremill.sttp._ + implicit val sttpBackend = HttpURLConnectionBackend() + sttp.send() + """) + } + + thrown.getMessage should include("This is a partial request, the method & url are not specified") + } +} diff --git a/core/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala b/core/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala new file mode 100644 index 0000000..aad1669 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala @@ -0,0 +1,12 @@ +package com.softwaremill.sttp + +import scala.util.Try + +import com.softwaremill.sttp.testing.ConvertToFuture +import com.softwaremill.sttp.testing.HttpTest + +class TryHttpURLConnectionHttpTest extends HttpTest[Try] { + + override implicit val backend: SttpBackend[Try, Nothing] = TryHttpURLConnectionBackend() + override implicit val convertToFuture: ConvertToFuture[Try] = ConvertToFuture.scalaTry +} diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/EvalScala.scala b/core/src/test/scala/com/softwaremill/sttp/testing/EvalScala.scala new file mode 100644 index 0000000..255755d --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/EvalScala.scala @@ -0,0 +1,11 @@ +package com.softwaremill.sttp.testing + +object EvalScala { + import scala.tools.reflect.ToolBox + + def apply(code: String): Any = { + val m = scala.reflect.runtime.currentMirror + val tb = m.mkToolBox() + tb.eval(tb.parse(code)) + } +} diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala index aa0af07..27a6eda 100644 --- a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala +++ b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala @@ -3,16 +3,22 @@ package com.softwaremill.sttp.testing.streaming import com.softwaremill.sttp._ import com.softwaremill.sttp.testing.ForceWrapped import org.scalatest.{AsyncFreeSpec, BeforeAndAfterAll, Matchers} - import scala.language.higherKinds +import com.softwaremill.sttp.testing.ConvertToFuture + 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._ + implicit def backend: SttpBackend[R, S] + + implicit def convertToFuture: ConvertToFuture[R] + + def bodyProducer(body: String): S + + def bodyConsumer(stream: S): R[String] "stream request body" in { sttp @@ -58,7 +64,7 @@ trait StreamingTest[R[_], S] extends AsyncFreeSpec with Matchers with BeforeAndA } override protected def afterAll(): Unit = { - testStreamingBackend.backend.close() + backend.close() super.afterAll() } diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala deleted file mode 100644 index 3ea63a3..0000000 --- a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/TestStreamingBackend.scala +++ /dev/null @@ -1,16 +0,0 @@ -package com.softwaremill.sttp.testing.streaming - -import com.softwaremill.sttp.SttpBackend -import com.softwaremill.sttp.testing.ConvertToFuture - -import scala.language.higherKinds - -trait TestStreamingBackend[R[_], S] { - implicit def backend: SttpBackend[R, S] - - implicit def convertToFuture: ConvertToFuture[R] - - def bodyProducer(body: String): S - - def bodyConsumer(stream: S): R[String] -} diff --git a/docs/backends/custom.rst b/docs/backends/custom.rst index 17571e1..86bfb9d 100644 --- a/docs/backends/custom.rst +++ b/docs/backends/custom.rst @@ -3,10 +3,10 @@ Custom backends, logging, metrics ================================= -It is also entirely possible to write custom backends (if doing so, please consider contributing!) or wrap an existing one. One can even write completely generic wrappers for any delegate backend, as each backend comes equipped with a monad for the response type. This brings the possibility to ``map`` and ``flatMap`` over responses. +It is also entirely possible to write custom backends (if doing so, please consider contributing!) or wrap an existing one. One can even write completely generic wrappers for any delegate backend, as each backend comes equipped with a monad for the response type. This brings the possibility to ``map`` and ``flatMap`` over responses. Possible use-cases for wrapper-backend include: - + * logging * capturing metrics * request signing (transforming the request before sending it to the delegate) @@ -60,7 +60,7 @@ Below is an example on how to implement a backend wrapper, which sends metrics f class MetricWrapper[S](delegate: SttpBackend[Future, S], metrics: MetricsServer) extends SttpBackend[Future, S] { - + override def send[T](request: Request[T, S]): Future[Response[T]] = { val start = System.currentTimeMillis() @@ -138,3 +138,26 @@ In some cases it's possible to implement a generic retry mechanism; such a mecha } Note that some backends also have built-in retry mechanisms, e.g. `akka-http `_ or `OkHttp `_ (see the builder's ``retryOnConnectionFailure`` method). + +Example new backend +-------------------------------- + +Implementing a new backend is made easy as the tests are published in the ``core`` jar file under the ``tests`` classifier. Simply add the follow dependencies to your ``build.sbt``:: + + "com.softwaremill.sttp" %% "core" % sttpVersion % "test" classifier "tests", + "com.github.pathikrit" %% "better-files" % "3.4.0" % "test", + "org.scalatest" %% "scalatest" % "3.0.5" % "test" + +Implement your backend and extend the ``HttpTest`` class:: + + import com.softwaremill.sttp.SttpBackend + import com.softwaremill.sttp.testing.{ConvertToFuture, HttpTest} + + class MyCustomBackendHttpTest extends HttpTest[Future] { + + override implicit val convertToFuture: ConvertToFuture[Future] = ConvertToFuture.future + override implicit lazy val backend: SttpBackend[Future, Nothing] = new MyCustomBackend() + + } + +You can find a more detailed example in the `sttp-vertx `_ repository. diff --git a/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/MonixStreamingTest.scala b/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/MonixStreamingTest.scala new file mode 100644 index 0000000..d00c056 --- /dev/null +++ b/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/MonixStreamingTest.scala @@ -0,0 +1,26 @@ +package com.softwaremill.sttp.impl.monix + +import java.nio.ByteBuffer + +import com.softwaremill.sttp.testing.ConvertToFuture +import com.softwaremill.sttp.testing.streaming.StreamingTest +import monix.eval.Task +import monix.reactive.Observable + +abstract class MonixStreamingTest extends StreamingTest[Task, Observable[ByteBuffer]] { + + override implicit val convertToFuture: ConvertToFuture[Task] = convertMonixTaskToFuture + + override def bodyProducer(body: String): Observable[ByteBuffer] = + Observable + .fromIterable( + body.getBytes("utf-8") + ) + .map(v => ByteBuffer.wrap(Array(v))) + + override def bodyConsumer(stream: Observable[ByteBuffer]): Task[String] = + stream + .flatMap(v => Observable.fromIterable(v.array())) + .toListL + .map(bs => new String(bs.toArray, "utf8")) +} 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 deleted file mode 100644 index f1b262b..0000000 --- a/implementations/monix/src/test/scala/com/softwaremill/sttp/impl/monix/MonixTestStreamingBackend.scala +++ /dev/null @@ -1,28 +0,0 @@ -package com.softwaremill.sttp.impl.monix - -import com.softwaremill.sttp.testing.ConvertToFuture -import com.softwaremill.sttp.testing.streaming.TestStreamingBackend -import monix.eval.Task -import monix.reactive.Observable - -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[T] = - Observable - .fromIterable( - body.getBytes("utf-8") - ) - .map(v => fromByteArray(Array(v))) - - override def bodyConsumer(stream: Observable[T]): Task[String] = - stream - .flatMap(v => Observable.fromIterable(toByteArray(v))) - .toListL - .map(bs => new String(bs.toArray, "utf8")) - -} 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 index af9ea9c..dd08214 100644 --- 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 @@ -3,24 +3,11 @@ 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 com.softwaremill.sttp.impl.monix.MonixStreamingTest import monix.eval.Task import monix.reactive.Observable -class OkHttpMonixStreamingTest extends StreamingTest[Task, Observable[ByteBuffer]] { +class OkHttpMonixStreamingTest extends MonixStreamingTest { - 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() + override implicit val backend: SttpBackend[Task, Observable[ByteBuffer]] = OkHttpMonixBackend() } diff --git a/tests/src/test/resources/binaryfile.jpg b/tests/src/test/resources/binaryfile.jpg deleted file mode 100644 index b9f5c5a..0000000 Binary files a/tests/src/test/resources/binaryfile.jpg and /dev/null differ diff --git a/tests/src/test/resources/logback.xml b/tests/src/test/resources/logback.xml deleted file mode 100644 index 1470b51..0000000 --- a/tests/src/test/resources/logback.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - - - diff --git a/tests/src/test/resources/textfile.txt b/tests/src/test/resources/textfile.txt deleted file mode 100644 index 9904f90..0000000 --- a/tests/src/test/resources/textfile.txt +++ /dev/null @@ -1,100 +0,0 @@ -- 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/tests/src/test/scala/com/softwaremill/sttp/EvalScala.scala b/tests/src/test/scala/com/softwaremill/sttp/EvalScala.scala deleted file mode 100644 index bac1537..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/EvalScala.scala +++ /dev/null @@ -1,11 +0,0 @@ -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 deleted file mode 100644 index 599913f..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/HttpURLConnectionHttpTest.scala +++ /dev/null @@ -1,9 +0,0 @@ -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/IllTypedTests.scala b/tests/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala deleted file mode 100644 index 881c60e..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/IllTypedTests.scala +++ /dev/null @@ -1,34 +0,0 @@ -package com.softwaremill.sttp - -import org.scalatest.{FlatSpec, Matchers} - -import scala.tools.reflect.ToolBoxError - -class IllTypedTests extends FlatSpec with Matchers { - "compilation" should "fail when trying to stream using the default backend" in { - val thrown = intercept[ToolBoxError] { - EvalScala(""" - import com.softwaremill.sttp._ - import akka.stream.scaladsl.Source - import akka.util.ByteString - implicit val sttpBackend = HttpURLConnectionBackend() - sttp.get(uri"http://example.com").response(asStream[Source[ByteString, Any]]).send() - """) - } - - thrown.getMessage should include( - "could not find implicit value for parameter backend: com.softwaremill.sttp.SttpBackend[R,akka.stream.scaladsl.Source[akka.util.ByteString,Any]]") - } - - "compilation" should "fail when trying to send a request without giving an URL" in { - val thrown = intercept[ToolBoxError] { - EvalScala(""" - import com.softwaremill.sttp._ - implicit val sttpBackend = HttpURLConnectionBackend() - sttp.send() - """) - } - - thrown.getMessage should include("This is a partial request, the method & url are not specified") - } -} diff --git a/tests/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala b/tests/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala deleted file mode 100644 index 19b48fd..0000000 --- a/tests/src/test/scala/com/softwaremill/sttp/TryHttpURLConnectionHttpTest.scala +++ /dev/null @@ -1,11 +0,0 @@ -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 -} -- cgit v1.2.3 From ccb1afe90e938fc2b8619dd960a1df1937f212be Mon Sep 17 00:00:00 2001 From: Sam Guymer Date: Mon, 21 May 2018 20:54:10 +1000 Subject: Code review updates Start the test server before each backend test --- build.sbt | 36 +--- core/src/test/resources/binaryfile.jpg | Bin 0 -> 42010 bytes core/src/test/resources/textfile.txt | 100 +++++++++ .../com/softwaremill/sttp/testing/HttpTest.scala | 40 ++-- .../softwaremill/sttp/testing/TestHttpServer.scala | 236 +++++++++++++++++++++ .../sttp/testing/streaming/StreamingTest.scala | 9 +- docs/backends/custom.rst | 2 + project/PollingUtils.scala | 45 ---- project/plugins.sbt | 5 +- test-server/src/main/resources/binaryfile.jpg | Bin 42010 -> 0 bytes test-server/src/main/resources/textfile.txt | 100 --------- .../softwaremill/sttp/server/TestHttpServer.scala | 197 ----------------- 12 files changed, 368 insertions(+), 402 deletions(-) create mode 100644 core/src/test/resources/binaryfile.jpg create mode 100644 core/src/test/resources/textfile.txt create mode 100644 core/src/test/scala/com/softwaremill/sttp/testing/TestHttpServer.scala delete mode 100644 project/PollingUtils.scala delete mode 100644 test-server/src/main/resources/binaryfile.jpg delete mode 100644 test-server/src/main/resources/textfile.txt delete mode 100644 test-server/src/main/scala/com/softwaremill/sttp/server/TestHttpServer.scala diff --git a/build.sbt b/build.sbt index 6bcaf1a..2acfbf6 100644 --- a/build.sbt +++ b/build.sbt @@ -34,9 +34,6 @@ 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(skip in publish := true, name := "sttp") @@ -57,18 +54,18 @@ lazy val rootProject = (project in file(".")) circe, json4s, braveBackend, - prometheusBackend, - testServer + prometheusBackend ) lazy val core: Project = (project in file("core")) .settings(commonSettings: _*) - .settings(testServerSettings: _*) .settings( name := "core", libraryDependencies ++= Seq( "com.github.pathikrit" %% "better-files" % "3.4.0" % "test", "org.scala-lang" % "scala-compiler" % scalaVersion.value % "test", + akkaHttp % "test", + akkaStreams % "test", scalaTest % "test" ), publishArtifact in Test := true // allow implementations outside of this repo @@ -106,7 +103,6 @@ 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( @@ -122,7 +118,6 @@ lazy val akkaHttpBackend: Project = (project in file("akka-http-backend")) lazy val asyncHttpClientBackend: Project = { (project in file("async-http-client-backend")) .settings(commonSettings: _*) - .settings(testServerSettings: _*) .settings( name := "async-http-client-backend", libraryDependencies ++= Seq( @@ -135,7 +130,6 @@ lazy val asyncHttpClientBackend: Project = { 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) } @@ -168,7 +162,6 @@ lazy val asyncHttpClientFs2Backend: Project = //-- okhttp lazy val okhttpBackend: Project = (project in file("okhttp-backend")) .settings(commonSettings: _*) - .settings(testServerSettings: _*) .settings( name := "okhttp-backend", libraryDependencies ++= Seq( @@ -180,7 +173,6 @@ lazy val okhttpBackend: Project = (project in file("okhttp-backend")) 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) } @@ -238,25 +230,3 @@ lazy val prometheusBackend: Project = (project in file("metrics/prometheus-backe ) ) .dependsOn(core) - -// 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 - ) - -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/test/resources/binaryfile.jpg b/core/src/test/resources/binaryfile.jpg new file mode 100644 index 0000000..b9f5c5a Binary files /dev/null and b/core/src/test/resources/binaryfile.jpg differ diff --git a/core/src/test/resources/textfile.txt b/core/src/test/resources/textfile.txt new file mode 100644 index 0000000..9904f90 --- /dev/null +++ b/core/src/test/resources/textfile.txt @@ -0,0 +1,100 @@ +- Lorem ipsum dolor sit amet +- Vivamus sem ipsum. +- Ut molestie. +- Donec. +- Fusce non porta. +- Nulla ac metus. Morbi mattis. +- Etiam varius. +- Nulla. +- Phasellus id mollis. +- Suspendisse at. +- Quisque nec leo velit. +- Fusce. +- Maecenas nec tristique senectus et. +- Integer vestibulum lorem fermentum. +- Vestibulum consectetuer dolor. +- Lorem ipsum in. +- Fusce condimentum auctor scelerisque, wisi. +- Quisque. +- Curae. +- Curae, Nullam. +- Curae, Integer. +- Vestibulum dignissim massa. Donec. +- Pellentesque sed sem. +- Vivamus est. +- Maecenas elit sed est. +- Quisque sed tellus. +- Cum sociis natoque penatibus et. +- Fusce aliquam. +- Donec. +- Sed elementum, sapien accumsan odio. +- Nam mattis, magna lectus, tincidunt. +- Pellentesque scelerisque a, sodales. +- Sed sed condimentum. +- Curae, In nonummy. Phasellus adipiscing. +- Vestibulum quis diam mollis. +- Sed eros. Duis ipsum. +- Aenean pellentesque at, mollis tempus. +- Cras ornare facilisis sodales. Aenean. +- Cum sociis natoque penatibus. +- Donec id nulla. +- Quisque ut sapien. +- Phasellus purus. Proin ultricies. +- Aliquam auctor neque. Nunc. +- Nam nunc fringilla non, vehicula. +- Morbi molestie, felis ut lobortis. +- Nulla quis. +- In. +- Phasellus laoreet urna. +- Lorem ipsum. +- Phasellus. +- Class aptent taciti sociosqu ad. +- Sed lacinia. +- Pellentesque dapibus diam. Duis. +- Suspendisse est. Curabitur. +- Fusce condimentum justo. +- Aenean congue quis, faucibus. +- Ut pharetra leo. Donec. +- Fusce. +- Donec porta. +- Pellentesque orci. +- Sed. +- Quisque rutrum, wisi vulputate wisi. +- Cum sociis. +- Cras. +- Sed eros. Curabitur. +- Proin in velit wisi, tempor. +- Quisque eu. +- Proin. +- Nam pellentesque sed, imperdiet aliquam. +- Mauris euismod. Sed euismod orci. +- Etiam. +- Donec. +- Fusce wisi a metus. Proin. +- Phasellus quis. +- Donec non imperdiet. +- Aenean vel bibendum a, laoreet. +- Fusce non enim. Phasellus vulputate. +- Donec urna elit, sit. +- Pellentesque habitant morbi. +- Nulla ante. Curabitur elit. Donec. +- Cum sociis natoque penatibus. +- Maecenas eget leo at. +- Cum sociis natoque penatibus et. +- Vivamus lacus. +- Integer. +- Curae. +- Maecenas rhoncus. Morbi. +- Aenean posuere. +- Duis. +- Suspendisse a odio fermentum libero. +- Nam enim. Fusce enim. In. +- Maecenas. +- Lorem ipsum primis. +- Curabitur ac turpis semper sed. +- Quisque condimentum. Donec sit. +- Integer convallis non, posuere. +- Etiam vulputate, odio. +- Proin id lorem. Donec quis. +- Curae, Sed nec augue. +- Aliquam ut turpis. \ No newline at end of file diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala b/core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala index f1fa002..5598aa2 100644 --- a/core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala +++ b/core/src/test/scala/com/softwaremill/sttp/testing/HttpTest.scala @@ -22,26 +22,23 @@ trait HttpTest[R[_]] with OptionValues with IntegrationPatience with BeforeAndAfterEach - with BeforeAndAfterAll { - - private val endpoint = "localhost:51823" + with BeforeAndAfterAll + with TestHttpServer { 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 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 val postEcho = sttp.post(uri"$endpoint/echo") + 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" @@ -172,7 +169,7 @@ trait HttpTest[R[_]] } "headers" - { - val getHeaders = sttp.get(uri"$endpoint/set_headers") + def getHeaders = sttp.get(uri"$endpoint/set_headers") "read response headers" in { val response = getHeaders.response(sttpIgnore).send().force() @@ -187,7 +184,7 @@ trait HttpTest[R[_]] } "errors" - { - val getHeaders = sttp.post(uri"$endpoint/set_headers") + def getHeaders = sttp.post(uri"$endpoint/set_headers") "return 405 when method not allowed" in { val response = getHeaders.response(sttpIgnore).send().force() @@ -236,7 +233,7 @@ trait HttpTest[R[_]] } "auth" - { - val secureBasic = sttp.get(uri"$endpoint/secure_basic") + def secureBasic = sttp.get(uri"$endpoint/secure_basic") "return a 401 when authorization fails" in { val req = secureBasic @@ -254,7 +251,7 @@ trait HttpTest[R[_]] } "compression" - { - val compress = sttp.get(uri"$endpoint/compress") + def compress = sttp.get(uri"$endpoint/compress") val decompressedBody = "I'm compressed!" "decompress using the default accept encoding header" in { @@ -374,7 +371,7 @@ trait HttpTest[R[_]] } "multipart" - { - val mp = sttp.post(uri"$endpoint/multipart") + def mp = sttp.post(uri"$endpoint/multipart") "send a multipart message" in { val req = mp.multipartBody(multipart("p1", "v1"), multipart("p2", "v2")) @@ -400,11 +397,11 @@ trait HttpTest[R[_]] } "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") + 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" - val loop = sttp.post(uri"$endpoint/redirect/loop") + 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() @@ -499,10 +496,11 @@ trait HttpTest[R[_]] } "empty response" - { - val postEmptyResponse = sttp - .post(uri"$endpoint/empty_unauthorized_response") - .body("{}") - .contentType("application/json") + 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() diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/TestHttpServer.scala b/core/src/test/scala/com/softwaremill/sttp/testing/TestHttpServer.scala new file mode 100644 index 0000000..11fc692 --- /dev/null +++ b/core/src/test/scala/com/softwaremill/sttp/testing/TestHttpServer.scala @@ -0,0 +1,236 @@ +package com.softwaremill.sttp.testing + +import akka.Done +import akka.actor.ActorSystem +import akka.http.scaladsl.Http +import akka.http.scaladsl.coding.{Deflate, Gzip, NoCoding} +import akka.http.scaladsl.model._ +import akka.http.scaladsl.model.headers.CacheDirectives._ +import akka.http.scaladsl.model.headers._ +import akka.http.scaladsl.server.Directives.{entity, path, _} +import akka.http.scaladsl.server.Route +import akka.http.scaladsl.server.directives.Credentials +import akka.stream.ActorMaterializer +import akka.util.ByteString +import scala.concurrent.duration._ +import scala.concurrent.{Await, Future} + +import org.scalatest.BeforeAndAfterAll +import org.scalatest.Suite + +trait TestHttpServer extends BeforeAndAfterAll { this: Suite => + + private val server = new HttpServer(0) + protected var endpoint = "localhost:51823" + + override protected def beforeAll(): Unit = { + import scala.concurrent.ExecutionContext.Implicits.global + + super.beforeAll() + Await.result( + server.start().map { binding => + endpoint = s"localhost:${binding.localAddress.getPort}" + }, + 10.seconds + ) + } + + override protected def afterAll(): Unit = { + server.close() + super.afterAll() + } + +} + +object HttpServer { + + def main(args: Array[String]): Unit = { + val port = args.headOption.map(_.toInt).getOrElse(51823) + + Await.result(new HttpServer(port).start(), 10.seconds) + } +} + +private class HttpServer(port: Int) extends AutoCloseable { + + import scala.concurrent.ExecutionContext.Implicits.global + + private var server: Option[Future[Http.ServerBinding]] = None + + private implicit val actorSystem: ActorSystem = ActorSystem("sttp-test-server") + private implicit val materializer: ActorMaterializer = ActorMaterializer() + + private def paramsToString(m: Map[String, String]): String = + m.toList.sortBy(_._1).map(p => s"${p._1}=${p._2}").mkString(" ") + + private val textFile = new java.io.File(getClass.getResource("/textfile.txt").getFile) + private val binaryFile = new java.io.File(getClass.getResource("/binaryfile.jpg").getFile) + private val textWithSpecialCharacters = "Żółć!" + + val serverRoutes: Route = + pathPrefix("echo") { + pathPrefix("form_params") { + formFieldMap { params => + path("as_string") { + complete(paramsToString(params)) + } ~ + path("as_params") { + complete(FormData(params)) + } + } + } ~ get { + parameterMap { params => + complete(List("GET", "/echo", paramsToString(params)) + .filter(_.nonEmpty) + .mkString(" ")) + } + } ~ + post { + parameterMap { params => + entity(as[String]) { body: String => + complete(List("POST", "/echo", paramsToString(params), body) + .filter(_.nonEmpty) + .mkString(" ")) + } + } + } + } ~ pathPrefix("streaming") { + path("echo") { + post { + parameterMap { _ => + entity(as[String]) { body: String => + complete(body) + } + } + } + } + } ~ path("set_headers") { + get { + respondWithHeader(`Cache-Control`(`max-age`(1000L))) { + respondWithHeader(`Cache-Control`(`no-cache`)) { + complete("ok") + } + } + } + } ~ pathPrefix("set_cookies") { + path("with_expires") { + setCookie(HttpCookie("c", "v", expires = Some(DateTime(1997, 12, 8, 12, 49, 12)))) { + complete("ok") + } + } ~ get { + setCookie( + HttpCookie( + "cookie1", + "value1", + secure = true, + httpOnly = true, + maxAge = Some(123L) + ) + ) { + setCookie(HttpCookie("cookie2", "value2")) { + setCookie( + HttpCookie( + "cookie3", + "", + domain = Some("xyz"), + path = Some("a/b/c") + ) + ) { + complete("ok") + } + } + } + } + } ~ path("secure_basic") { + authenticateBasic("test realm", { + case c @ Credentials.Provided(un) if un == "adam" && c.verify("1234") => + Some(un) + case _ => None + }) { userName => + complete(s"Hello, $userName!") + } + } ~ path("compress") { + encodeResponseWith(Gzip, Deflate, NoCoding) { + complete("I'm compressed!") + } + } ~ pathPrefix("download") { + path("binary") { + getFromFile(binaryFile) + } ~ path("text") { + getFromFile(textFile) + } + } ~ pathPrefix("multipart") { + entity(as[akka.http.scaladsl.model.Multipart.FormData]) { fd => + complete { + fd.parts + .mapAsync(1) { p => + val fv = p.entity.dataBytes.runFold(ByteString())(_ ++ _) + fv.map(_.utf8String) + .map(v => p.name + "=" + v + p.filename.fold("")(fn => s" ($fn)")) + } + .runFold(Vector.empty[String])(_ :+ _) + .map(v => v.mkString(", ")) + } + } + } ~ pathPrefix("redirect") { + path("r1") { + redirect("/redirect/r2", StatusCodes.TemporaryRedirect) + } ~ + path("r2") { + redirect("/redirect/r3", StatusCodes.PermanentRedirect) + } ~ + path("r3") { + redirect("/redirect/r4", StatusCodes.Found) + } ~ + path("r4") { + complete("819") + } ~ + path("loop") { + redirect("/redirect/loop", StatusCodes.Found) + } + } ~ pathPrefix("timeout") { + complete { + akka.pattern.after(1.second, using = actorSystem.scheduler)( + Future.successful("Done") + ) + } + } ~ path("empty_unauthorized_response") { + post { + import akka.http.scaladsl.model._ + complete( + HttpResponse( + status = StatusCodes.Unauthorized, + headers = Nil, + entity = HttpEntity.Empty, + protocol = HttpProtocols.`HTTP/1.1` + )) + } + } ~ path("respond_with_iso_8859_2") { + get { ctx => + val entity = + HttpEntity(MediaTypes.`text/plain`.withCharset(HttpCharset.custom("ISO-8859-2")), textWithSpecialCharacters) + ctx.complete(HttpResponse(200, entity = entity)) + } + } + + def start(): Future[Http.ServerBinding] = { + unbindServer().flatMap { _ => + val server = Http().bindAndHandle(serverRoutes, "localhost", port) + this.server = Some(server) + server + } + } + + def close(): Unit = { + val unbind = unbindServer() + unbind.onComplete(_ => actorSystem.terminate()) + Await.result( + unbind, + 10.seconds + ) + } + + private def unbindServer(): Future[Done] = { + server.map(_.flatMap(_.unbind())).getOrElse(Future.successful(Done)) + } +} diff --git a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala index 27a6eda..40aaf82 100644 --- a/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala +++ b/core/src/test/scala/com/softwaremill/sttp/testing/streaming/StreamingTest.scala @@ -6,10 +6,15 @@ import org.scalatest.{AsyncFreeSpec, BeforeAndAfterAll, Matchers} import scala.language.higherKinds import com.softwaremill.sttp.testing.ConvertToFuture +import com.softwaremill.sttp.testing.TestHttpServer -trait StreamingTest[R[_], S] extends AsyncFreeSpec with Matchers with BeforeAndAfterAll with ForceWrapped { +trait StreamingTest[R[_], S] + extends AsyncFreeSpec + with Matchers + with ForceWrapped + with BeforeAndAfterAll + with TestHttpServer { - private val endpoint = "localhost:51823" private val body = "streaming test" implicit def backend: SttpBackend[R, S] diff --git a/docs/backends/custom.rst b/docs/backends/custom.rst index 86bfb9d..c36b1d2 100644 --- a/docs/backends/custom.rst +++ b/docs/backends/custom.rst @@ -146,6 +146,8 @@ Implementing a new backend is made easy as the tests are published in the ``core "com.softwaremill.sttp" %% "core" % sttpVersion % "test" classifier "tests", "com.github.pathikrit" %% "better-files" % "3.4.0" % "test", + "com.typesafe.akka" %% "akka-http" % "10.1.1" % "test", + "com.typesafe.akka" %% "akka-stream" % "2.5.12" % "test", "org.scalatest" %% "scalatest" % "3.0.5" % "test" Implement your backend and extend the ``HttpTest`` class:: diff --git a/project/PollingUtils.scala b/project/PollingUtils.scala deleted file mode 100644 index 34b92ac..0000000 --- a/project/PollingUtils.scala +++ /dev/null @@ -1,45 +0,0 @@ -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 b334460..fa55342 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,5 +1,4 @@ -// using '-coursier' because of https://github.com/lucidsoftware/neo-sbt-scalafmt/issues/64 -addSbtPlugin("com.lucidchart" % "sbt-scalafmt-coursier" % "1.15") +addSbtPlugin("com.lucidchart" % "sbt-scalafmt" % "1.15") addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "2.0") @@ -8,5 +7,3 @@ 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") - -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 deleted file mode 100644 index b9f5c5a..0000000 Binary files a/test-server/src/main/resources/binaryfile.jpg and /dev/null differ diff --git a/test-server/src/main/resources/textfile.txt b/test-server/src/main/resources/textfile.txt deleted file mode 100644 index 9904f90..0000000 --- a/test-server/src/main/resources/textfile.txt +++ /dev/null @@ -1,100 +0,0 @@ -- 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 deleted file mode 100644 index 906154d..0000000 --- a/test-server/src/main/scala/com/softwaremill/sttp/server/TestHttpServer.scala +++ /dev/null @@ -1,197 +0,0 @@ -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 - ) - } -} -- cgit v1.2.3