aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authoradamw <adam@warski.org>2017-07-16 09:59:09 +0200
committeradamw <adam@warski.org>2017-07-16 09:59:09 +0200
commitb1844f09d78e035d85302310e2ba55929cd5fc52 (patch)
treef1659e57ff863dd81f6935e422d9f9043fc66f40
parent062ab1ac1bd4fa9f4602ae31419c60a7e6d991bf (diff)
downloadsttp-b1844f09d78e035d85302310e2ba55929cd5fc52.tar.gz
sttp-b1844f09d78e035d85302310e2ba55929cd5fc52.tar.bz2
sttp-b1844f09d78e035d85302310e2ba55929cd5fc52.zip
Streaming tests, comments
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala3
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/Response.scala12
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala15
-rw-r--r--core/src/main/scala/com/softwaremill/sttp/package.scala91
-rw-r--r--tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala43
-rw-r--r--tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala64
-rw-r--r--tests/src/test/scala/com/softwaremill/sttp/testHelpers.scala50
7 files changed, 205 insertions, 73 deletions
diff --git a/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala b/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala
index dc6f8a2..50f6d76 100644
--- a/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/HttpConnectionSttpHandler.scala
@@ -120,7 +120,8 @@ object HttpConnectionSttpHandler extends SttpHandler[Id, Nothing] {
r.parse(asString(enc))
case ResponseAsStream() =>
- // only possible when the user requests the response as a stream of Nothing. Oh well ...
+ // only possible when the user requests the response as a stream of
+ // Nothing. Oh well ...
throw new IllegalStateException()
}
}
diff --git a/core/src/main/scala/com/softwaremill/sttp/Response.scala b/core/src/main/scala/com/softwaremill/sttp/Response.scala
index a4d60a5..44c39c3 100644
--- a/core/src/main/scala/com/softwaremill/sttp/Response.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/Response.scala
@@ -47,9 +47,9 @@ case class Cookie(name: String,
object Cookie {
def apply(hc: HttpCookie, h: String): Cookie = {
- // HttpCookie.parse has special handling for the expires attribute and turns it into max-age
- // if the cookie contains an expires header; hand-parsing in such case to preserve the
- // values from the cookie
+ // HttpCookie.parse has special handling for the expires attribute and
+ // turns it into max-age if the cookie contains an expires header;
+ // hand-parsing in such case to preserve the values from the cookie
val lch = h.toLowerCase
val (expires, maxAge) = if (lch.contains("expires=")) {
val tokenizer = new StringTokenizer(h, ";")
@@ -85,7 +85,8 @@ object Cookie {
}
/**
- * Modified version of `HttpCookie.expiryDate2DeltaSeconds` to return a `ZonedDateTime`, not a second-delta.
+ * Modified version of `HttpCookie.expiryDate2DeltaSeconds` to return a
+ * `ZonedDateTime`, not a second-delta.
*/
private def expiryDate2ZonedDateTime(
dateString: String): Option[ZonedDateTime] = {
@@ -98,7 +99,8 @@ object Cookie {
df.set2DigitYearStart(cal.getTime)
try {
cal.setTime(df.parse(dateString))
- if (!format.contains("yyyy")) { // 2-digit years following the standard set
+ if (!format.contains("yyyy")) {
+ // 2-digit years following the standard set
// out it rfc 6265
var year = cal.get(Calendar.YEAR)
year %= 100
diff --git a/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala b/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala
index 1216a1e..a8099a4 100644
--- a/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/UriInterpolator.scala
@@ -44,9 +44,10 @@ object UriInterpolator {
case Array(x) =>
if (!x.matches("[a-zA-Z0-9+\\.\\-]*")) {
- // anything else than the allowed characters in scheme suggest that there is no scheme
- // assuming whatever we parsed so far is part of authority, and parsing the rest
- // see https://stackoverflow.com/questions/3641722/valid-characters-for-uri-schemes
+ // anything else than the allowed characters in scheme suggest that
+ // there is no scheme assuming whatever we parsed so far is part of
+ // authority, and parsing the rest; see
+ // https://stackoverflow.com/questions/3641722/valid-characters-for-uri-schemes
Authority(Scheme(""), v).parseS(x)
} else append(x)
}
@@ -54,8 +55,8 @@ object UriInterpolator {
override def parseE(e: Any): UriBuilder = {
def encodeIfNotInitialEndpoint(s: String) = {
- // special case: when this is the first expression, contains a complete schema with :// and nothing is yet parsed
- // not escaping the contents
+ // special case: when this is the first expression, contains a complete
+ // schema with :// and nothing is yet parsed not escaping the contents
if (v.isEmpty && s.contains("://")) s else encode(s)
}
@@ -314,8 +315,8 @@ object UriInterpolator {
private def encode(s: Any): String = {
// space is encoded as a +, which is only valid in the query;
- // in other contexts, it must be percent-encoded
- // see https://stackoverflow.com/questions/2678551/when-to-encode-space-to-plus-or-20
+ // in other contexts, it must be percent-encoded; see
+ // https://stackoverflow.com/questions/2678551/when-to-encode-space-to-plus-or-20
URLEncoder.encode(String.valueOf(s), "UTF-8").replaceAll("\\+", "%20")
}
diff --git a/core/src/main/scala/com/softwaremill/sttp/package.scala b/core/src/main/scala/com/softwaremill/sttp/package.scala
index baf4061..763b6c3 100644
--- a/core/src/main/scala/com/softwaremill/sttp/package.scala
+++ b/core/src/main/scala/com/softwaremill/sttp/package.scala
@@ -26,6 +26,10 @@ package object sttp {
ResponseAsString(encoding)
def responseAsByteArray: ResponseAs[Array[Byte], Nothing] =
ResponseAsByteArray
+
+ /**
+ * Uses `utf-8` encoding.
+ */
def responseAsParams: ResponseAs[Seq[(String, String)], Nothing] =
responseAsParams(Utf8)
def responseAsParams(
@@ -35,8 +39,9 @@ package object sttp {
def responseAsStream[S]: ResponseAs[S, S] = ResponseAsStream[S, S]()
/**
- * Use the factory methods `multiPart` to conveniently create instances of this class. A part can be then
- * further customised using `fileName`, `contentType` and `header` methods.
+ * Use the factory methods `multiPart` to conveniently create instances of
+ * this class. A part can be then further customised using `fileName`,
+ * `contentType` and `header` methods.
*/
case class MultiPart(name: String,
data: BasicRequestBody,
@@ -50,7 +55,8 @@ package object sttp {
}
/**
- * Content type will be set to `text/plain` with `utf-8` encoding, can be overridden later using the `contentType` method.
+ * Content type will be set to `text/plain` with `utf-8` encoding, can be
+ * overridden later using the `contentType` method.
*/
def multiPart(name: String, data: String): MultiPart =
MultiPart(name,
@@ -59,7 +65,8 @@ package object sttp {
Some(contentTypeWithEncoding(TextPlainContentType, Utf8)))
/**
- * Content type will be set to `text/plain` with `utf-8` encoding, can be overridden later using the `contentType` method.
+ * Content type will be set to `text/plain` with `utf-8` encoding, can be
+ * overridden later using the `contentType` method.
*/
def multiPart(name: String, data: String, encoding: String): MultiPart =
MultiPart(name,
@@ -68,7 +75,8 @@ package object sttp {
Some(contentTypeWithEncoding(TextPlainContentType, Utf8)))
/**
- * Content type will be set to `application/octet-stream`, can be overridden later using the `contentType` method.
+ * Content type will be set to `application/octet-stream`, can be overridden
+ * later using the `contentType` method.
*/
def multiPart(name: String, data: Array[Byte]): MultiPart =
MultiPart(name,
@@ -76,7 +84,8 @@ package object sttp {
contentType = Some(ApplicationOctetStreamContentType))
/**
- * Content type will be set to `application/octet-stream`, can be overridden later using the `contentType` method.
+ * Content type will be set to `application/octet-stream`, can be overridden
+ * later using the `contentType` method.
*/
def multiPart(name: String, data: ByteBuffer): MultiPart =
MultiPart(name,
@@ -84,7 +93,8 @@ package object sttp {
contentType = Some(ApplicationOctetStreamContentType))
/**
- * Content type will be set to `application/octet-stream`, can be overridden later using the `contentType` method.
+ * Content type will be set to `application/octet-stream`, can be overridden
+ * later using the `contentType` method.
*/
def multiPart(name: String, data: InputStream): MultiPart =
MultiPart(name,
@@ -92,13 +102,15 @@ package object sttp {
contentType = Some(ApplicationOctetStreamContentType))
/**
- * Content type will be set to `application/octet-stream`, can be overridden later using the `contentType` method.
+ * Content type will be set to `application/octet-stream`, can be overridden
+ * later using the `contentType` method.
*/
def multiPart(name: String, data: File): MultiPart =
multiPart(name, data.toPath)
/**
- * Content type will be set to `application/octet-stream`, can be overridden later using the `contentType` method.
+ * Content type will be set to `application/octet-stream`, can be overridden
+ * later using the `contentType` method.
*/
def multiPart(name: String, data: Path): MultiPart =
MultiPart(name,
@@ -106,6 +118,17 @@ package object sttp {
fileName = Some(data.getFileName.toString),
contentType = Some(ApplicationOctetStreamContentType))
+ /**
+ * @tparam U Specifies if the method & uri are specified. By default can be
+ * either:
+ * * `Empty`, which is a type constructor which always resolves to
+ * `None`. This type of request is aliased to `PartialRequest`:
+ * there's no method and uri specified, and the request cannot be
+ * sent.
+ * * `Id`, which is an identity type constructor. This type of
+ * request is aliased to `Request`: the method and uri are
+ * specified, and the request can be sent.
+ */
case class RequestTemplate[U[_]](
method: U[Method],
uri: U[URI],
@@ -159,12 +182,14 @@ package object sttp {
/**
* Uses the `utf-8` encoding.
- * If content type is not yet specified, will be set to `text/plain` with `utf-8` encoding.
+ * If content type is not yet specified, will be set to `text/plain`
+ * with `utf-8` encoding.
*/
def body(b: String): RequestTemplate[U] = body(b, Utf8)
/**
- * If content type is not yet specified, will be set to `text/plain` with the given encoding.
+ * If content type is not yet specified, will be set to `text/plain`
+ * with the given encoding.
*/
def body(b: String, encoding: String): RequestTemplate[U] =
setContentTypeIfMissing(
@@ -172,33 +197,38 @@ package object sttp {
.copy(body = StringBody(b, encoding))
/**
- * If content type is not yet specified, will be set to `application/octet-stream`.
+ * If content type is not yet specified, will be set to
+ * `application/octet-stream`.
*/
def body(b: Array[Byte]): RequestTemplate[U] =
setContentTypeIfMissing(ApplicationOctetStreamContentType).copy(
body = ByteArrayBody(b))
/**
- * If content type is not yet specified, will be set to `application/octet-stream`.
+ * If content type is not yet specified, will be set to
+ * `application/octet-stream`.
*/
def body(b: ByteBuffer): RequestTemplate[U] =
setContentTypeIfMissing(ApplicationOctetStreamContentType).copy(
body = ByteBufferBody(b))
/**
- * If content type is not yet specified, will be set to `application/octet-stream`.
+ * If content type is not yet specified, will be set to
+ * `application/octet-stream`.
*/
def body(b: InputStream): RequestTemplate[U] =
setContentTypeIfMissing(ApplicationOctetStreamContentType).copy(
body = InputStreamBody(b))
/**
- * If content type is not yet specified, will be set to `application/octet-stream`.
+ * If content type is not yet specified, will be set to
+ * `application/octet-stream`.
*/
def body(b: File): RequestTemplate[U] = body(b.toPath)
/**
- * If content type is not yet specified, will be set to `application/octet-stream`.
+ * If content type is not yet specified, will be set to
+ * `application/octet-stream`.
*/
def body(b: Path): RequestTemplate[U] =
setContentTypeIfMissing(ApplicationOctetStreamContentType).copy(
@@ -206,34 +236,39 @@ package object sttp {
/**
* Encodes the given parameters as form data using `utf-8`.
- * If content type is not yet specified, will be set to `application/x-www-form-urlencoded`.
+ * If content type is not yet specified, will be set to
+ * `application/x-www-form-urlencoded`.
*/
def body(fs: Map[String, String]): RequestTemplate[U] =
formDataBody(fs.toList, Utf8)
/**
* Encodes the given parameters as form data.
- * If content type is not yet specified, will be set to `application/x-www-form-urlencoded`.
+ * If content type is not yet specified, will be set to
+ * `application/x-www-form-urlencoded`.
*/
def body(fs: Map[String, String], encoding: String): RequestTemplate[U] =
formDataBody(fs.toList, encoding)
/**
* Encodes the given parameters as form data using `utf-8`.
- * If content type is not yet specified, will be set to `application/x-www-form-urlencoded`.
+ * If content type is not yet specified, will be set to
+ * `application/x-www-form-urlencoded`.
*/
def body(fs: (String, String)*): RequestTemplate[U] =
formDataBody(fs.toList, Utf8)
/**
* Encodes the given parameters as form data.
- * If content type is not yet specified, will be set to `application/x-www-form-urlencoded`.
+ * If content type is not yet specified, will be set to
+ * `application/x-www-form-urlencoded`.
*/
def body(fs: Seq[(String, String)], encoding: String): RequestTemplate[U] =
formDataBody(fs, encoding)
/**
- * If content type is not yet specified, will be set to `application/octet-stream`.
+ * If content type is not yet specified, will be set to
+ * `application/octet-stream`.
*/
def body[T: BodySerializer](b: T): RequestTemplate[U] =
setContentTypeIfMissing(ApplicationOctetStreamContentType).copy(
@@ -242,10 +277,13 @@ package object sttp {
//def multipartData(parts: MultiPart*): RequestTemplate[U] = ???
/**
- * @param responseAs What's the target type to which the response should be read. Needs to be specified upfront
- * so that the response is always consumed and hence there are no requirements on client code
- * to consume it. An exception to this are streaming responses, which need to fully consumed
- * by the client if such a response type is requested.
+ * @param responseAs What's the target type to which the response body
+ * should be read. Needs to be specified upfront
+ * so that the response is always consumed and hence
+ * there are no requirements on client code to consume
+ * it. An exception to this are streaming responses,
+ * which need to fully consumed by the client if such
+ * a response type is requested.
*/
def send[R[_], S, T](responseAs: ResponseAs[T, S])(
implicit handler: SttpHandler[R, S],
@@ -293,7 +331,8 @@ package object sttp {
type Request = RequestTemplate[Id]
@implicitNotFound(
- "This is a partial request, the method & url are not specified. Use .get(...), .post(...) etc. to obtain a non-partial request.")
+ "This is a partial request, the method & url are not specified. Use " +
+ ".get(...), .post(...) etc. to obtain a non-partial request.")
private type IsRequest[U[_]] = RequestTemplate[U] =:= Request
val sttp: RequestTemplate[Empty] = RequestTemplate.empty
diff --git a/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala b/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala
index def843a..ff6b113 100644
--- a/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala
+++ b/tests/src/test/scala/com/softwaremill/sttp/BasicTests.scala
@@ -4,13 +4,11 @@ import java.io.ByteArrayInputStream
import java.nio.ByteBuffer
import java.time.{ZoneId, ZonedDateTime}
-import akka.stream.ActorMaterializer
-import akka.actor.ActorSystem
-import akka.http.scaladsl.Http
import akka.http.scaladsl.model.{DateTime, FormData}
import akka.http.scaladsl.model.headers._
import akka.http.scaladsl.model.headers.CacheDirectives._
import akka.http.scaladsl.server.Directives._
+import akka.http.scaladsl.server.Route
import akka.http.scaladsl.server.directives.Credentials
import com.softwaremill.sttp.akkahttp.AkkaHttpSttpHandler
import com.typesafe.scalalogging.StrictLogging
@@ -18,7 +16,6 @@ import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}
import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers}
import better.files._
-import scala.concurrent.Future
import scala.language.higherKinds
class BasicTests
@@ -27,11 +24,14 @@ class BasicTests
with BeforeAndAfterAll
with ScalaFutures
with StrictLogging
- with IntegrationPatience {
+ with IntegrationPatience
+ with TestHttpServer
+ with ForceWrapped {
+
private def paramsToString(m: Map[String, String]): String =
m.toList.sortBy(_._1).map(p => s"${p._1}=${p._2}").mkString(" ")
- private val serverRoutes =
+ override val serverRoutes: Route =
pathPrefix("echo") {
pathPrefix("form_params") {
formFieldMap { params =>
@@ -105,37 +105,12 @@ class BasicTests
}
}
- private implicit val actorSystem: ActorSystem = ActorSystem("sttp-test")
- import actorSystem.dispatcher
-
- private implicit val materializer = ActorMaterializer()
- private val endpoint = "http://localhost:51823"
-
- override protected def beforeAll(): Unit = {
- Http().bindAndHandle(serverRoutes, "localhost", 51823).futureValue
- }
-
- override protected def afterAll(): Unit = {
- actorSystem.terminate().futureValue
- }
-
- trait ForceWrappedValue[R[_]] {
- def force[T](wrapped: R[T]): T
- }
- implicit class ForceDecorator[R[_], T](wrapped: R[T]) {
- def force()(implicit fwv: ForceWrappedValue[R]): T = fwv.force(wrapped)
- }
+ override def port = 51823
runTests("HttpURLConnection")(HttpConnectionSttpHandler,
- new ForceWrappedValue[Id] {
- override def force[T](wrapped: Id[T]): T =
- wrapped
- })
+ ForceWrappedValue.id)
runTests("Akka HTTP")(new AkkaHttpSttpHandler(actorSystem),
- new ForceWrappedValue[Future] {
- override def force[T](wrapped: Future[T]): T =
- wrapped.futureValue
- })
+ ForceWrappedValue.future)
def runTests[R[_]](name: String)(
implicit handler: SttpHandler[R, Nothing],
diff --git a/tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala b/tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala
new file mode 100644
index 0000000..ab77753
--- /dev/null
+++ b/tests/src/test/scala/com/softwaremill/sttp/StreamingTests.scala
@@ -0,0 +1,64 @@
+package com.softwaremill.sttp
+
+import akka.http.scaladsl.server.Directives._
+import akka.http.scaladsl.server.Route
+import akka.stream.scaladsl.Source
+import akka.util.ByteString
+import com.softwaremill.sttp.akkahttp.AkkaHttpSttpHandler
+import com.typesafe.scalalogging.StrictLogging
+import org.scalatest.{BeforeAndAfterAll, FlatSpec, Matchers}
+import org.scalatest.concurrent.{IntegrationPatience, ScalaFutures}
+import com.softwaremill.sttp.akkahttp._
+
+class StreamingTests
+ extends FlatSpec
+ with Matchers
+ with BeforeAndAfterAll
+ with ScalaFutures
+ with StrictLogging
+ with IntegrationPatience
+ with TestHttpServer {
+
+ override val serverRoutes: Route =
+ path("echo") {
+ post {
+ parameterMap { params =>
+ entity(as[String]) { body: String =>
+ complete(body)
+ }
+ }
+ }
+ }
+
+ override def port = 51824
+
+ akkaStreamingTests()
+
+ def akkaStreamingTests(): Unit = {
+ implicit val handler = new AkkaHttpSttpHandler(actorSystem)
+
+ val body = "streaming test"
+
+ "Akka HTTP" should "stream request body" in {
+ val response = sttp
+ .post(uri"$endpoint/echo")
+ .body(Source.single(ByteString(body)))
+ .send(responseAsString)
+ .futureValue
+
+ response.body should be(body)
+ }
+
+ it should "receive a stream" in {
+ val response = sttp
+ .post(uri"$endpoint/echo")
+ .body(body)
+ .send(responseAsStream[Source[ByteString, Any]])
+ .futureValue
+
+ val responseBody = response.body.runReduce(_ ++ _).futureValue.utf8String
+
+ responseBody should be(body)
+ }
+ }
+}
diff --git a/tests/src/test/scala/com/softwaremill/sttp/testHelpers.scala b/tests/src/test/scala/com/softwaremill/sttp/testHelpers.scala
new file mode 100644
index 0000000..bc7eccd
--- /dev/null
+++ b/tests/src/test/scala/com/softwaremill/sttp/testHelpers.scala
@@ -0,0 +1,50 @@
+package com.softwaremill.sttp
+
+import akka.actor.ActorSystem
+import akka.http.scaladsl.Http
+import akka.http.scaladsl.server.Route
+import akka.stream.ActorMaterializer
+import org.scalatest.{BeforeAndAfterAll, Suite}
+import org.scalatest.concurrent.ScalaFutures
+
+import scala.concurrent.Future
+import scala.language.higherKinds
+
+trait TestHttpServer extends BeforeAndAfterAll with ScalaFutures {
+ 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 { this: Suite =>
+ trait ForceWrappedValue[R[_]] {
+ def force[T](wrapped: R[T]): T
+ }
+ object ForceWrappedValue {
+ val id = new ForceWrappedValue[Id] {
+ override def force[T](wrapped: Id[T]): T =
+ wrapped
+ }
+ val future = new ForceWrappedValue[Future] {
+ override def force[T](wrapped: Future[T]): T =
+ wrapped.futureValue
+ }
+ }
+ implicit class ForceDecorator[R[_], T](wrapped: R[T]) {
+ def force()(implicit fwv: ForceWrappedValue[R]): T = fwv.force(wrapped)
+ }
+}