+str #17361: Unified http java/scala projects except marshallers
This commit is contained in:
parent
454a393af1
commit
be82e85ffc
182 changed files with 13693 additions and 0 deletions
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.testkit
|
||||
|
||||
import org.junit.rules.ExternalResource
|
||||
import org.junit.{ Rule, Assert }
|
||||
import scala.concurrent.duration._
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.http.scaladsl.model.HttpResponse
|
||||
|
||||
/**
|
||||
* A RouteTest that uses JUnit assertions.
|
||||
*/
|
||||
abstract class JUnitRouteTestBase extends RouteTest {
|
||||
protected def systemResource: ActorSystemResource
|
||||
implicit def system: ActorSystem = systemResource.system
|
||||
implicit def materializer: ActorFlowMaterializer = systemResource.materializer
|
||||
|
||||
protected def createTestResponse(response: HttpResponse): TestResponse =
|
||||
new TestResponse(response, awaitDuration)(system.dispatcher, materializer) {
|
||||
protected def assertEquals(expected: AnyRef, actual: AnyRef, message: String): Unit =
|
||||
Assert.assertEquals(message, expected, actual)
|
||||
|
||||
protected def assertEquals(expected: Int, actual: Int, message: String): Unit =
|
||||
Assert.assertEquals(message, expected, actual)
|
||||
|
||||
protected def assertTrue(predicate: Boolean, message: String): Unit =
|
||||
Assert.assertTrue(message, predicate)
|
||||
|
||||
protected def fail(message: String): Nothing = {
|
||||
Assert.fail(message)
|
||||
throw new IllegalStateException("Assertion should have failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
abstract class JUnitRouteTest extends JUnitRouteTestBase {
|
||||
private[this] val _systemResource = new ActorSystemResource
|
||||
@Rule
|
||||
protected def systemResource: ActorSystemResource = _systemResource
|
||||
}
|
||||
|
||||
class ActorSystemResource extends ExternalResource {
|
||||
protected def createSystem(): ActorSystem = ActorSystem()
|
||||
protected def createFlowMaterializer(system: ActorSystem): ActorFlowMaterializer = ActorFlowMaterializer()(system)
|
||||
|
||||
implicit def system: ActorSystem = _system
|
||||
implicit def materializer: ActorFlowMaterializer = _materializer
|
||||
|
||||
private[this] var _system: ActorSystem = null
|
||||
private[this] var _materializer: ActorFlowMaterializer = null
|
||||
|
||||
override def before(): Unit = {
|
||||
require((_system eq null) && (_materializer eq null))
|
||||
_system = createSystem()
|
||||
_materializer = createFlowMaterializer(_system)
|
||||
}
|
||||
override def after(): Unit = {
|
||||
_system.shutdown()
|
||||
_system.awaitTermination(5.seconds)
|
||||
_system = null
|
||||
_materializer = null
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.testkit
|
||||
|
||||
import scala.annotation.varargs
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.duration._
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.http.scaladsl.server
|
||||
import akka.http.javadsl.model.HttpRequest
|
||||
import akka.http.javadsl.server.{ Route, Directives }
|
||||
import akka.http.impl.util.JavaMapping.Implicits._
|
||||
import akka.http.impl.server.RouteImplementation
|
||||
import akka.http.scaladsl.model.HttpResponse
|
||||
import akka.http.scaladsl.server.{ RouteResult, RoutingSettings, Route ⇒ ScalaRoute }
|
||||
import akka.actor.ActorSystem
|
||||
import akka.event.NoLogging
|
||||
import akka.http.impl.util._
|
||||
|
||||
abstract class RouteTest {
|
||||
implicit def system: ActorSystem
|
||||
implicit def materializer: ActorFlowMaterializer
|
||||
implicit def executionContext: ExecutionContext = system.dispatcher
|
||||
|
||||
protected def awaitDuration: FiniteDuration = 500.millis
|
||||
|
||||
def runRoute(route: Route, request: HttpRequest): TestResponse = {
|
||||
val scalaRoute = ScalaRoute.seal(RouteImplementation(route))
|
||||
val result = scalaRoute(new server.RequestContextImpl(request.asScala, NoLogging, RoutingSettings(system)))
|
||||
|
||||
result.awaitResult(awaitDuration) match {
|
||||
case RouteResult.Complete(response) ⇒ createTestResponse(response)
|
||||
}
|
||||
}
|
||||
|
||||
@varargs
|
||||
def testRoute(first: Route, others: Route*): TestRoute =
|
||||
new TestRoute {
|
||||
val underlying: Route = Directives.route(first, others: _*)
|
||||
|
||||
def run(request: HttpRequest): TestResponse = runRoute(underlying, request)
|
||||
}
|
||||
|
||||
protected def createTestResponse(response: HttpResponse): TestResponse
|
||||
}
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.testkit
|
||||
|
||||
import scala.reflect.ClassTag
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.concurrent.duration.FiniteDuration
|
||||
import akka.util.ByteString
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshal
|
||||
import akka.http.scaladsl.model.HttpResponse
|
||||
import akka.http.impl.util._
|
||||
import akka.http.impl.server.UnmarshallerImpl
|
||||
import akka.http.impl.util.JavaMapping.Implicits._
|
||||
import akka.http.javadsl.server.Unmarshaller
|
||||
import akka.http.javadsl.model._
|
||||
|
||||
/**
|
||||
* A wrapper for responses
|
||||
*/
|
||||
abstract class TestResponse(_response: HttpResponse, awaitAtMost: FiniteDuration)(implicit ec: ExecutionContext, materializer: ActorFlowMaterializer) {
|
||||
lazy val entity: HttpEntityStrict =
|
||||
_response.entity.toStrict(awaitAtMost).awaitResult(awaitAtMost)
|
||||
lazy val response: HttpResponse = _response.withEntity(entity)
|
||||
|
||||
// FIXME: add header getters / assertions
|
||||
|
||||
def mediaType: MediaType = extractFromResponse(_.entity.contentType.mediaType)
|
||||
def mediaTypeString: String = mediaType.toString
|
||||
def entityBytes: ByteString = entity.data()
|
||||
def entityAs[T](unmarshaller: Unmarshaller[T]): T =
|
||||
Unmarshal(response)
|
||||
.to(unmarshaller.asInstanceOf[UnmarshallerImpl[T]].scalaUnmarshaller(ec, materializer), ec)
|
||||
.awaitResult(awaitAtMost)
|
||||
def entityAsString: String = entity.data().utf8String
|
||||
def status: StatusCode = response.status.asJava
|
||||
def statusCode: Int = response.status.intValue
|
||||
def header[T <: HttpHeader](clazz: Class[T]): T =
|
||||
response.header(ClassTag(clazz))
|
||||
.getOrElse(fail(s"Expected header of type ${clazz.getSimpleName} but wasn't found."))
|
||||
|
||||
def assertStatusCode(expected: Int): TestResponse =
|
||||
assertStatusCode(StatusCodes.get(expected))
|
||||
def assertStatusCode(expected: StatusCode): TestResponse =
|
||||
assertEqualsKind(expected, status, "status code")
|
||||
def assertMediaType(expected: String): TestResponse =
|
||||
assertEqualsKind(expected, mediaTypeString, "media type")
|
||||
def assertMediaType(expected: MediaType): TestResponse =
|
||||
assertEqualsKind(expected, mediaType, "media type")
|
||||
def assertEntity(expected: String): TestResponse =
|
||||
assertEqualsKind(expected, entityAsString, "entity")
|
||||
def assertEntityBytes(expected: ByteString): TestResponse =
|
||||
assertEqualsKind(expected, entityBytes, "entity")
|
||||
def assertEntityAs[T <: AnyRef](unmarshaller: Unmarshaller[T], expected: T): TestResponse =
|
||||
assertEqualsKind(expected, entityAs(unmarshaller), "entity")
|
||||
def assertHeaderExists(expected: HttpHeader): TestResponse = {
|
||||
assertTrue(response.headers.exists(_ == expected), s"Header $expected was missing.")
|
||||
this
|
||||
}
|
||||
def assertHeaderKindExists(name: String): TestResponse = {
|
||||
val lowercased = name.toRootLowerCase
|
||||
assertTrue(response.headers.exists(_.is(lowercased)), s"Expected `$name` header was missing.")
|
||||
this
|
||||
}
|
||||
def assertHeaderExists(name: String, value: String): TestResponse = {
|
||||
val lowercased = name.toRootLowerCase
|
||||
val headers = response.headers.filter(_.is(lowercased))
|
||||
if (headers.isEmpty) fail(s"Expected `$name` header was missing.")
|
||||
else assertTrue(headers.exists(_.value == value),
|
||||
s"`$name` header was found but had the wrong value. Found headers: ${headers.mkString(", ")}")
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
private[this] def extractFromResponse[T](f: HttpResponse ⇒ T): T =
|
||||
if (response eq null) fail("Request didn't complete with response")
|
||||
else f(response)
|
||||
|
||||
protected def assertEqualsKind(expected: AnyRef, actual: AnyRef, kind: String): TestResponse = {
|
||||
assertEquals(expected, actual, s"Unexpected $kind!")
|
||||
this
|
||||
}
|
||||
protected def assertEqualsKind(expected: Int, actual: Int, kind: String): TestResponse = {
|
||||
assertEquals(expected, actual, s"Unexpected $kind!")
|
||||
this
|
||||
}
|
||||
|
||||
protected def fail(message: String): Nothing
|
||||
protected def assertEquals(expected: AnyRef, actual: AnyRef, message: String): Unit
|
||||
protected def assertEquals(expected: Int, actual: Int, message: String): Unit
|
||||
protected def assertTrue(predicate: Boolean, message: String): Unit
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.testkit
|
||||
|
||||
import akka.http.javadsl.model.HttpRequest
|
||||
import akka.http.javadsl.server.Route
|
||||
|
||||
trait TestRoute {
|
||||
def underlying: Route
|
||||
def run(request: HttpRequest): TestResponse
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.testkit
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ ExecutionContext, Await }
|
||||
import akka.http.scaladsl.unmarshalling.{ Unmarshal, FromEntityUnmarshaller }
|
||||
import akka.http.scaladsl.marshalling._
|
||||
import akka.http.scaladsl.model.HttpEntity
|
||||
import akka.stream.FlowMaterializer
|
||||
|
||||
import scala.util.Try
|
||||
|
||||
trait MarshallingTestUtils {
|
||||
def marshal[T: ToEntityMarshaller](value: T)(implicit ec: ExecutionContext, mat: FlowMaterializer): HttpEntity.Strict =
|
||||
Await.result(Marshal(value).to[HttpEntity].flatMap(_.toStrict(1.second)), 1.second)
|
||||
|
||||
def unmarshalValue[T: FromEntityUnmarshaller](entity: HttpEntity)(implicit ec: ExecutionContext, mat: FlowMaterializer): T =
|
||||
unmarshal(entity).get
|
||||
|
||||
def unmarshal[T: FromEntityUnmarshaller](entity: HttpEntity)(implicit ec: ExecutionContext, mat: FlowMaterializer): Try[T] = {
|
||||
val fut = Unmarshal(entity).to[T]
|
||||
Await.ready(fut, 1.second)
|
||||
fut.value.get
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.testkit
|
||||
|
||||
import com.typesafe.config.{ ConfigFactory, Config }
|
||||
import scala.collection.immutable
|
||||
import scala.concurrent.{ Await, Future }
|
||||
import scala.concurrent.duration._
|
||||
import scala.util.DynamicVariable
|
||||
import scala.reflect.ClassTag
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.http.scaladsl.client.RequestBuilding
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.unmarshalling._
|
||||
import akka.http.scaladsl.model._
|
||||
import headers.Host
|
||||
import FastFuture._
|
||||
|
||||
trait RouteTest extends RequestBuilding with RouteTestResultComponent with MarshallingTestUtils {
|
||||
this: TestFrameworkInterface ⇒
|
||||
|
||||
/** Override to supply a custom ActorSystem */
|
||||
protected def createActorSystem(): ActorSystem =
|
||||
ActorSystem(actorSystemNameFrom(getClass), testConfig)
|
||||
|
||||
def actorSystemNameFrom(clazz: Class[_]) =
|
||||
clazz.getName
|
||||
.replace('.', '-')
|
||||
.replace('_', '-')
|
||||
.filter(_ != '$')
|
||||
|
||||
def testConfigSource: String = ""
|
||||
def testConfig: Config = {
|
||||
val source = testConfigSource
|
||||
val config = if (source.isEmpty) ConfigFactory.empty() else ConfigFactory.parseString(source)
|
||||
config.withFallback(ConfigFactory.load())
|
||||
}
|
||||
implicit val system = createActorSystem()
|
||||
implicit def executor = system.dispatcher
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
|
||||
def cleanUp(): Unit = system.shutdown()
|
||||
|
||||
private val dynRR = new DynamicVariable[RouteTestResult](null)
|
||||
private def result =
|
||||
if (dynRR.value ne null) dynRR.value
|
||||
else sys.error("This value is only available inside of a `check` construct!")
|
||||
|
||||
def check[T](body: ⇒ T): RouteTestResult ⇒ T = result ⇒ dynRR.withValue(result.awaitResult)(body)
|
||||
|
||||
def handled: Boolean = result.handled
|
||||
def response: HttpResponse = result.response
|
||||
def responseEntity: HttpEntity = result.entity
|
||||
def chunks: immutable.Seq[HttpEntity.ChunkStreamPart] = result.chunks
|
||||
def entityAs[T: FromEntityUnmarshaller: ClassTag](implicit timeout: Duration = 1.second): T = {
|
||||
def msg(e: Throwable) = s"Could not unmarshal entity to type '${implicitly[ClassTag[T]]}' for `entityAs` assertion: $e\n\nResponse was: $response"
|
||||
Await.result(Unmarshal(responseEntity).to[T].fast.recover[T] { case error ⇒ failTest(msg(error)) }, timeout)
|
||||
}
|
||||
def responseAs[T: FromResponseUnmarshaller: ClassTag](implicit timeout: Duration = 1.second): T = {
|
||||
def msg(e: Throwable) = s"Could not unmarshal response to type '${implicitly[ClassTag[T]]}' for `responseAs` assertion: $e\n\nResponse was: $response"
|
||||
Await.result(Unmarshal(response).to[T].fast.recover[T] { case error ⇒ failTest(msg(error)) }, timeout)
|
||||
}
|
||||
def contentType: ContentType = responseEntity.contentType
|
||||
def mediaType: MediaType = contentType.mediaType
|
||||
def charset: HttpCharset = contentType.charset
|
||||
def definedCharset: Option[HttpCharset] = contentType.definedCharset
|
||||
def headers: immutable.Seq[HttpHeader] = response.headers
|
||||
def header[T <: HttpHeader: ClassTag]: Option[T] = response.header[T]
|
||||
def header(name: String): Option[HttpHeader] = response.headers.find(_.is(name.toLowerCase))
|
||||
def status: StatusCode = response.status
|
||||
|
||||
def closingExtension: String = chunks.lastOption match {
|
||||
case Some(HttpEntity.LastChunk(extension, _)) ⇒ extension
|
||||
case _ ⇒ ""
|
||||
}
|
||||
def trailer: immutable.Seq[HttpHeader] = chunks.lastOption match {
|
||||
case Some(HttpEntity.LastChunk(_, trailer)) ⇒ trailer
|
||||
case _ ⇒ Nil
|
||||
}
|
||||
|
||||
def rejections: immutable.Seq[Rejection] = result.rejections
|
||||
def rejection: Rejection = {
|
||||
val r = rejections
|
||||
if (r.size == 1) r.head else failTest("Expected a single rejection but got %s (%s)".format(r.size, r))
|
||||
}
|
||||
|
||||
/**
|
||||
* A dummy that can be used as `~> runRoute` to run the route but without blocking for the result.
|
||||
* The result of the pipeline is the result that can later be checked with `check`. See the
|
||||
* "separate running route from checking" example from ScalatestRouteTestSpec.scala.
|
||||
*/
|
||||
def runRoute: RouteTestResult ⇒ RouteTestResult = akka.http.impl.util.identityFunc
|
||||
|
||||
// there is already an implicit class WithTransformation in scope (inherited from akka.http.scaladsl.testkit.TransformerPipelineSupport)
|
||||
// however, this one takes precedence
|
||||
implicit class WithTransformation2(request: HttpRequest) {
|
||||
def ~>[A, B](f: A ⇒ B)(implicit ta: TildeArrow[A, B]): ta.Out = ta(request, f)
|
||||
}
|
||||
|
||||
abstract class TildeArrow[A, B] {
|
||||
type Out
|
||||
def apply(request: HttpRequest, f: A ⇒ B): Out
|
||||
}
|
||||
|
||||
case class DefaultHostInfo(host: Host, securedConnection: Boolean)
|
||||
object DefaultHostInfo {
|
||||
implicit def defaultHost: DefaultHostInfo = DefaultHostInfo(Host("example.com"), securedConnection = false)
|
||||
}
|
||||
object TildeArrow {
|
||||
implicit object InjectIntoRequestTransformer extends TildeArrow[HttpRequest, HttpRequest] {
|
||||
type Out = HttpRequest
|
||||
def apply(request: HttpRequest, f: HttpRequest ⇒ HttpRequest) = f(request)
|
||||
}
|
||||
implicit def injectIntoRoute(implicit timeout: RouteTestTimeout, setup: RoutingSetup,
|
||||
defaultHostInfo: DefaultHostInfo) =
|
||||
new TildeArrow[RequestContext, Future[RouteResult]] {
|
||||
type Out = RouteTestResult
|
||||
def apply(request: HttpRequest, route: Route): Out = {
|
||||
val routeTestResult = new RouteTestResult(timeout.duration)
|
||||
val effectiveRequest =
|
||||
request.withEffectiveUri(
|
||||
securedConnection = defaultHostInfo.securedConnection,
|
||||
defaultHostHeader = defaultHostInfo.host)
|
||||
val ctx = new RequestContextImpl(effectiveRequest, setup.routingLog.requestLog(effectiveRequest), setup.settings)
|
||||
val sealedExceptionHandler = setup.exceptionHandler.seal(setup.settings)
|
||||
val semiSealedRoute = // sealed for exceptions but not for rejections
|
||||
Directives.handleExceptions(sealedExceptionHandler) { route }
|
||||
val deferrableRouteResult = semiSealedRoute(ctx)
|
||||
deferrableRouteResult.fast.foreach(routeTestResult.handleResult)(setup.executor)
|
||||
routeTestResult
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//FIXME: trait Specs2RouteTest extends RouteTest with Specs2Interface
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.testkit
|
||||
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import scala.collection.immutable
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.ExecutionContext
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
import akka.http.scaladsl.model.HttpEntity.ChunkStreamPart
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.impl.util._
|
||||
|
||||
trait RouteTestResultComponent {
|
||||
|
||||
def failTest(msg: String): Nothing
|
||||
|
||||
/**
|
||||
* A receptacle for the response or rejections created by a route.
|
||||
*/
|
||||
class RouteTestResult(timeout: FiniteDuration)(implicit fm: FlowMaterializer) {
|
||||
private[this] var result: Option[Either[immutable.Seq[Rejection], HttpResponse]] = None
|
||||
private[this] val latch = new CountDownLatch(1)
|
||||
|
||||
def handled: Boolean = synchronized { result.isDefined && result.get.isRight }
|
||||
|
||||
def rejections: immutable.Seq[Rejection] = synchronized {
|
||||
result match {
|
||||
case Some(Left(rejections)) ⇒ rejections
|
||||
case Some(Right(response)) ⇒ failTest("Request was not rejected, response was " + response)
|
||||
case None ⇒ failNeitherCompletedNorRejected()
|
||||
}
|
||||
}
|
||||
|
||||
def response: HttpResponse = rawResponse.copy(entity = entity)
|
||||
|
||||
/** Returns a "fresh" entity with a "fresh" unconsumed byte- or chunk stream (if not strict) */
|
||||
def entity: ResponseEntity = entityRecreator()
|
||||
|
||||
def chunks: immutable.Seq[ChunkStreamPart] =
|
||||
entity match {
|
||||
case HttpEntity.Chunked(_, chunks) ⇒ awaitAllElements[ChunkStreamPart](chunks)
|
||||
case _ ⇒ Nil
|
||||
}
|
||||
|
||||
def ~>[T](f: RouteTestResult ⇒ T): T = f(this)
|
||||
|
||||
private def rawResponse: HttpResponse = synchronized {
|
||||
result match {
|
||||
case Some(Right(response)) ⇒ response
|
||||
case Some(Left(Nil)) ⇒ failTest("Request was rejected")
|
||||
case Some(Left(rejection :: Nil)) ⇒ failTest("Request was rejected with rejection " + rejection)
|
||||
case Some(Left(rejections)) ⇒ failTest("Request was rejected with rejections " + rejections)
|
||||
case None ⇒ failNeitherCompletedNorRejected()
|
||||
}
|
||||
}
|
||||
|
||||
private[testkit] def handleResult(rr: RouteResult)(implicit ec: ExecutionContext): Unit =
|
||||
synchronized {
|
||||
if (result.isEmpty) {
|
||||
result = rr match {
|
||||
case RouteResult.Complete(response) ⇒ Some(Right(response))
|
||||
case RouteResult.Rejected(rejections) ⇒ Some(Left(RejectionHandler.applyTransformations(rejections)))
|
||||
}
|
||||
latch.countDown()
|
||||
} else failTest("Route completed/rejected more than once")
|
||||
}
|
||||
|
||||
private[testkit] def awaitResult: this.type = {
|
||||
latch.await(timeout.toMillis, MILLISECONDS)
|
||||
this
|
||||
}
|
||||
|
||||
private[this] lazy val entityRecreator: () ⇒ ResponseEntity =
|
||||
rawResponse.entity match {
|
||||
case s: HttpEntity.Strict ⇒ () ⇒ s
|
||||
|
||||
case HttpEntity.Default(contentType, contentLength, data) ⇒
|
||||
val dataChunks = awaitAllElements(data);
|
||||
{ () ⇒ HttpEntity.Default(contentType, contentLength, Source(dataChunks)) }
|
||||
|
||||
case HttpEntity.CloseDelimited(contentType, data) ⇒
|
||||
val dataChunks = awaitAllElements(data);
|
||||
{ () ⇒ HttpEntity.CloseDelimited(contentType, Source(dataChunks)) }
|
||||
|
||||
case HttpEntity.Chunked(contentType, chunks) ⇒
|
||||
val dataChunks = awaitAllElements(chunks);
|
||||
{ () ⇒ HttpEntity.Chunked(contentType, Source(dataChunks)) }
|
||||
}
|
||||
|
||||
private def failNeitherCompletedNorRejected(): Nothing =
|
||||
failTest("Request was neither completed nor rejected within " + timeout)
|
||||
|
||||
private def awaitAllElements[T](data: Source[T, _]): immutable.Seq[T] =
|
||||
data.grouped(100000).runWith(Sink.head).awaitResult(timeout)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.testkit
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import akka.actor.ActorSystem
|
||||
import akka.testkit._
|
||||
|
||||
case class RouteTestTimeout(duration: FiniteDuration)
|
||||
|
||||
object RouteTestTimeout {
|
||||
implicit def default(implicit system: ActorSystem) = RouteTestTimeout(1.second dilated)
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.testkit
|
||||
|
||||
import scala.util.Try
|
||||
import scala.concurrent.{ ExecutionContext, Future, Await }
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.Suite
|
||||
import org.scalatest.matchers.Matcher
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.http.scaladsl.model.HttpEntity
|
||||
import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller
|
||||
|
||||
trait ScalatestUtils extends MarshallingTestUtils {
|
||||
import org.scalatest.Matchers._
|
||||
def evaluateTo[T](value: T): Matcher[Future[T]] =
|
||||
equal(value).matcher[T] compose (x ⇒ Await.result(x, 1.second))
|
||||
|
||||
def haveFailedWith(t: Throwable): Matcher[Future[_]] =
|
||||
equal(t).matcher[Throwable] compose (x ⇒ Await.result(x.failed, 1.second))
|
||||
|
||||
def unmarshalToValue[T: FromEntityUnmarshaller](value: T)(implicit ec: ExecutionContext, mat: FlowMaterializer): Matcher[HttpEntity] =
|
||||
equal(value).matcher[T] compose (unmarshalValue(_))
|
||||
|
||||
def unmarshalTo[T: FromEntityUnmarshaller](value: Try[T])(implicit ec: ExecutionContext, mat: FlowMaterializer): Matcher[HttpEntity] =
|
||||
equal(value).matcher[Try[T]] compose (unmarshal(_))
|
||||
}
|
||||
|
||||
trait ScalatestRouteTest extends RouteTest with TestFrameworkInterface.Scalatest with ScalatestUtils { this: Suite ⇒ }
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.testkit
|
||||
|
||||
import org.scalatest.exceptions.TestFailedException
|
||||
import org.scalatest.{ BeforeAndAfterAll, Suite }
|
||||
|
||||
//# source-quote
|
||||
trait TestFrameworkInterface {
|
||||
|
||||
def cleanUp()
|
||||
|
||||
def failTest(msg: String): Nothing
|
||||
}
|
||||
//#
|
||||
|
||||
object TestFrameworkInterface {
|
||||
|
||||
trait Scalatest extends TestFrameworkInterface with BeforeAndAfterAll {
|
||||
this: Suite ⇒
|
||||
|
||||
def failTest(msg: String) = throw new TestFailedException(msg, 11)
|
||||
|
||||
abstract override protected def afterAll(): Unit = {
|
||||
cleanUp()
|
||||
super.afterAll()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.testkit
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.FreeSpec
|
||||
import org.scalatest.Matchers
|
||||
import akka.testkit.TestProbe
|
||||
import akka.util.Timeout
|
||||
import akka.pattern.ask
|
||||
import akka.http.scaladsl.model.headers.RawHeader
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.model._
|
||||
import StatusCodes._
|
||||
import HttpMethods._
|
||||
import Directives._
|
||||
|
||||
class ScalatestRouteTestSpec extends FreeSpec with Matchers with ScalatestRouteTest {
|
||||
|
||||
"The ScalatestRouteTest should support" - {
|
||||
|
||||
"the most simple and direct route test" in {
|
||||
Get() ~> complete(HttpResponse()) ~> { rr ⇒ rr.awaitResult; rr.response } shouldEqual HttpResponse()
|
||||
}
|
||||
|
||||
"a test using a directive and some checks" in {
|
||||
val pinkHeader = RawHeader("Fancy", "pink")
|
||||
Get() ~> addHeader(pinkHeader) ~> {
|
||||
respondWithHeader(pinkHeader) {
|
||||
complete("abc")
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
responseEntity shouldEqual HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc")
|
||||
header("Fancy") shouldEqual Some(pinkHeader)
|
||||
}
|
||||
}
|
||||
|
||||
"proper rejection collection" in {
|
||||
Post("/abc", "content") ~> {
|
||||
(get | put) {
|
||||
complete("naah")
|
||||
}
|
||||
} ~> check {
|
||||
rejections shouldEqual List(MethodRejection(GET), MethodRejection(PUT))
|
||||
}
|
||||
}
|
||||
|
||||
"separation of route execution from checking" in {
|
||||
val pinkHeader = RawHeader("Fancy", "pink")
|
||||
|
||||
case object Command
|
||||
val service = TestProbe()
|
||||
val handler = TestProbe()
|
||||
implicit def serviceRef = service.ref
|
||||
implicit val askTimeout: Timeout = 1.second
|
||||
|
||||
val result =
|
||||
Get() ~> pinkHeader ~> {
|
||||
respondWithHeader(pinkHeader) {
|
||||
complete(handler.ref.ask(Command).mapTo[String])
|
||||
}
|
||||
} ~> runRoute
|
||||
|
||||
handler.expectMsg(Command)
|
||||
handler.reply("abc")
|
||||
|
||||
check {
|
||||
status shouldEqual OK
|
||||
responseEntity shouldEqual HttpEntity(ContentTypes.`text/plain(UTF-8)`, "abc")
|
||||
header("Fancy") shouldEqual Some(pinkHeader)
|
||||
}(result)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove once RespondWithDirectives have been ported
|
||||
def respondWithHeader(responseHeader: HttpHeader): Directive0 =
|
||||
mapResponseHeaders(responseHeader +: _)
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.examples.petstore;
|
||||
|
||||
public class Pet {
|
||||
private int id;
|
||||
private String name;
|
||||
|
||||
private Pet(){}
|
||||
public Pet(int id, String name) {
|
||||
this.id = id;
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public int getId() {
|
||||
return id;
|
||||
}
|
||||
public void setId(int id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.examples.petstore;
|
||||
|
||||
import akka.http.javadsl.server.RequestContext;
|
||||
import akka.http.javadsl.server.RouteResult;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public class PetStoreController {
|
||||
private Map<Integer, Pet> dataStore;
|
||||
|
||||
public PetStoreController(Map<Integer, Pet> dataStore) {
|
||||
this.dataStore = dataStore;
|
||||
}
|
||||
public RouteResult deletePet(RequestContext ctx, int petId) {
|
||||
dataStore.remove(petId);
|
||||
return ctx.completeWithStatus(200);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.examples.petstore;
|
||||
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.http.javadsl.marshallers.jackson.Jackson;
|
||||
import akka.http.javadsl.server.*;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
|
||||
public class PetStoreExample {
|
||||
static PathMatcher<Integer> petId = PathMatchers.integerNumber();
|
||||
static RequestVal<Pet> petEntity = RequestVals.entityAs(Jackson.jsonAs(Pet.class));
|
||||
|
||||
public static Route appRoute(final Map<Integer, Pet> pets) {
|
||||
PetStoreController controller = new PetStoreController(pets);
|
||||
|
||||
final RequestVal<Pet> existingPet = RequestVals.lookupInMap(petId, Pet.class, pets);
|
||||
|
||||
Handler1<Pet> putPetHandler = new Handler1<Pet>() {
|
||||
public RouteResult handle(RequestContext ctx, Pet thePet) {
|
||||
pets.put(thePet.getId(), thePet);
|
||||
return ctx.completeAs(Jackson.json(), thePet);
|
||||
}
|
||||
};
|
||||
|
||||
return
|
||||
route(
|
||||
path().route(
|
||||
getFromResource("web/index.html")
|
||||
),
|
||||
path("pet", petId).route(
|
||||
// demonstrates three different ways of handling requests:
|
||||
|
||||
// 1. using a predefined route that completes with an extraction
|
||||
get(extractAndComplete(Jackson.<Pet>json(), existingPet)),
|
||||
|
||||
// 2. using a handler
|
||||
put(handleWith(petEntity, putPetHandler)),
|
||||
|
||||
// 3. calling a method of a controller instance reflectively
|
||||
delete(handleWith(controller, "deletePet", petId))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Map<Integer, Pet> pets = new ConcurrentHashMap<Integer, Pet>();
|
||||
Pet dog = new Pet(0, "dog");
|
||||
Pet cat = new Pet(1, "cat");
|
||||
pets.put(0, dog);
|
||||
pets.put(1, cat);
|
||||
|
||||
ActorSystem system = ActorSystem.create();
|
||||
try {
|
||||
HttpService.bindRoute("localhost", 8080, appRoute(pets), system);
|
||||
System.out.println("Type RETURN to exit");
|
||||
System.in.read();
|
||||
} finally {
|
||||
system.shutdown();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.examples.simple;
|
||||
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.http.javadsl.server.*;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class SimpleServerApp extends HttpApp {
|
||||
static Parameter<Integer> x = Parameters.integer("x");
|
||||
static Parameter<Integer> y = Parameters.integer("y");
|
||||
|
||||
static PathMatcher<Integer> xSegment = PathMatchers.integerNumber();
|
||||
static PathMatcher<Integer> ySegment = PathMatchers.integerNumber();
|
||||
|
||||
public static RouteResult multiply(RequestContext ctx, int x, int y) {
|
||||
int result = x * y;
|
||||
return ctx.complete(String.format("%d * %d = %d", x, y, result));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Route createRoute() {
|
||||
Handler addHandler = new Handler() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx) {
|
||||
int xVal = x.get(ctx);
|
||||
int yVal = y.get(ctx);
|
||||
int result = xVal + yVal;
|
||||
return ctx.complete(String.format("%d + %d = %d", xVal, yVal, result));
|
||||
}
|
||||
};
|
||||
Handler2<Integer, Integer> subtractHandler = new Handler2<Integer, Integer>() {
|
||||
public RouteResult handle(RequestContext ctx, Integer xVal, Integer yVal) {
|
||||
int result = xVal - yVal;
|
||||
return ctx.complete(String.format("%d - %d = %d", xVal, yVal, result));
|
||||
}
|
||||
};
|
||||
return
|
||||
route(
|
||||
// matches the empty path
|
||||
pathSingleSlash().route(
|
||||
getFromResource("web/calculator.html")
|
||||
),
|
||||
// matches paths like this: /add?x=42&y=23
|
||||
path("add").route(
|
||||
handleWith(addHandler, x, y)
|
||||
),
|
||||
path("subtract").route(
|
||||
handleWith(x, y, subtractHandler)
|
||||
),
|
||||
// matches paths like this: /multiply/{x}/{y}
|
||||
path("multiply", xSegment, ySegment).route(
|
||||
// bind handler by reflection
|
||||
handleWith(SimpleServerApp.class, "multiply", xSegment, ySegment)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
ActorSystem system = ActorSystem.create();
|
||||
new SimpleServerApp().bindRoute("localhost", 8080, system);
|
||||
System.out.println("Type RETURN to exit");
|
||||
System.in.read();
|
||||
system.shutdown();
|
||||
}
|
||||
}
|
||||
23
akka-http-tests/src/main/resources/web/calculator.html
Normal file
23
akka-http-tests/src/main/resources/web/calculator.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<html>
|
||||
<body>
|
||||
<h1>Calculator</h1>
|
||||
|
||||
<h2>Add</h2>
|
||||
<form action="add">
|
||||
<label for="x">x:</label><input id="x" name="x" type="text" value="42"/>
|
||||
<label for="y">y:</label><input id="y" name="y" type="text" value="23"/>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
|
||||
<h2>Subtract</h2>
|
||||
<form action="subtract">
|
||||
<label for="x">x:</label><input id="x" name="x" type="text" value="42"/>
|
||||
<label for="y">y:</label><input id="y" name="y" type="text" value="23"/>
|
||||
<input type="submit" />
|
||||
</form>
|
||||
|
||||
<h3>Multiply</h3>
|
||||
<a href="/multiply/42/23">/multiply/42/23</a>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import org.junit.Test;
|
||||
import scala.Option;
|
||||
import scala.concurrent.Future;
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.model.headers.Authorization;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
|
||||
public class AuthenticationDirectivesTest extends JUnitRouteTest {
|
||||
HttpBasicAuthenticator<String> authenticatedUser =
|
||||
new HttpBasicAuthenticator<String>("test-realm") {
|
||||
@Override
|
||||
public Future<Option<String>> authenticate(BasicUserCredentials credentials) {
|
||||
if (credentials.available() && // no anonymous access
|
||||
credentials.userName().equals("sina") &&
|
||||
credentials.verifySecret("1234"))
|
||||
return authenticateAs("Sina");
|
||||
else return refuseAccess();
|
||||
}
|
||||
};
|
||||
|
||||
Handler1<String> helloWorldHandler =
|
||||
new Handler1<String>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, String user) {
|
||||
return ctx.complete("Hello "+user+"!");
|
||||
}
|
||||
};
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("secure").route(
|
||||
authenticatedUser.route(
|
||||
handleWith(authenticatedUser, helloWorldHandler)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@Test
|
||||
public void testCorrectUser() {
|
||||
HttpRequest authenticatedRequest =
|
||||
HttpRequest.GET("/secure")
|
||||
.addHeader(Authorization.basic("sina", "1234"));
|
||||
|
||||
route.run(authenticatedRequest)
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Hello Sina!");
|
||||
}
|
||||
@Test
|
||||
public void testRejectAnonymousAccess() {
|
||||
route.run(HttpRequest.GET("/secure"))
|
||||
.assertStatusCode(401)
|
||||
.assertEntity("The resource requires authentication, which was not supplied with the request")
|
||||
.assertHeaderExists("WWW-Authenticate", "Basic realm=\"test-realm\"");
|
||||
}
|
||||
@Test
|
||||
public void testRejectUnknownUser() {
|
||||
HttpRequest authenticatedRequest =
|
||||
HttpRequest.GET("/secure")
|
||||
.addHeader(Authorization.basic("joe", "0000"));
|
||||
|
||||
route.run(authenticatedRequest)
|
||||
.assertStatusCode(401)
|
||||
.assertEntity("The supplied authentication is invalid");
|
||||
}
|
||||
@Test
|
||||
public void testRejectWrongPassword() {
|
||||
HttpRequest authenticatedRequest =
|
||||
HttpRequest.GET("/secure")
|
||||
.addHeader(Authorization.basic("sina", "1235"));
|
||||
|
||||
route.run(authenticatedRequest)
|
||||
.assertStatusCode(401)
|
||||
.assertEntity("The supplied authentication is invalid");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.model.headers.AcceptEncoding;
|
||||
import akka.http.javadsl.model.headers.ContentEncoding;
|
||||
import akka.http.javadsl.model.headers.HttpEncodings;
|
||||
import akka.stream.ActorFlowMaterializer;
|
||||
import akka.util.ByteString;
|
||||
import org.junit.*;
|
||||
import scala.concurrent.Await;
|
||||
import scala.concurrent.duration.Duration;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CodingDirectivesTest extends JUnitRouteTest {
|
||||
|
||||
static ActorSystem system;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
system = ActorSystem.create("FlowGraphDocTest");
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown() {
|
||||
system.shutdown();
|
||||
system.awaitTermination();
|
||||
system = null;
|
||||
}
|
||||
|
||||
final ActorFlowMaterializer mat = ActorFlowMaterializer.create(system);
|
||||
|
||||
@Test
|
||||
public void testAutomaticEncodingWhenNoEncodingRequested() throws Exception {
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
encodeResponse(
|
||||
complete("TestString")
|
||||
)
|
||||
);
|
||||
|
||||
TestResponse response = route.run(HttpRequest.create());
|
||||
response
|
||||
.assertStatusCode(200);
|
||||
|
||||
Assert.assertEquals("TestString", response.entityBytes().utf8String());
|
||||
}
|
||||
@Test
|
||||
public void testAutomaticEncodingWhenDeflateRequested() throws Exception {
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
encodeResponse(
|
||||
complete("tester")
|
||||
)
|
||||
);
|
||||
|
||||
HttpRequest request = HttpRequest.create().addHeader(AcceptEncoding.create(HttpEncodings.DEFLATE));
|
||||
TestResponse response = route.run(request);
|
||||
response
|
||||
.assertStatusCode(200)
|
||||
.assertHeaderExists(ContentEncoding.create(HttpEncodings.DEFLATE));
|
||||
|
||||
ByteString decompressed =
|
||||
Await.result(Coder.Deflate.decode(response.entityBytes(), mat), Duration.apply(3, TimeUnit.SECONDS));
|
||||
Assert.assertEquals("tester", decompressed.utf8String());
|
||||
}
|
||||
@Test
|
||||
public void testEncodingWhenDeflateRequestedAndGzipSupported() {
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
encodeResponse(Coder.Gzip).route(
|
||||
complete("tester")
|
||||
)
|
||||
);
|
||||
|
||||
HttpRequest request = HttpRequest.create().addHeader(AcceptEncoding.create(HttpEncodings.DEFLATE));
|
||||
route.run(request)
|
||||
.assertStatusCode(406)
|
||||
.assertEntity("Resource representation is only available with these Content-Encodings:\ngzip");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutomaticDecoding() {}
|
||||
@Test
|
||||
public void testGzipDecoding() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import akka.dispatch.Futures;
|
||||
import akka.http.javadsl.marshallers.jackson.Jackson;
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.model.MediaTypes;
|
||||
import org.junit.Test;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
|
||||
public class CompleteTest extends JUnitRouteTest {
|
||||
@Test
|
||||
public void completeWithString() {
|
||||
Route route = complete("Everything OK!");
|
||||
|
||||
HttpRequest request = HttpRequest.create();
|
||||
|
||||
runRoute(route, request)
|
||||
.assertStatusCode(200)
|
||||
.assertMediaType(MediaTypes.TEXT_PLAIN)
|
||||
.assertEntity("Everything OK!");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completeAsJacksonJson() {
|
||||
class Person {
|
||||
public String getFirstName() { return "Peter"; }
|
||||
public String getLastName() { return "Parker"; }
|
||||
public int getAge() { return 138; }
|
||||
}
|
||||
Route route = completeAs(Jackson.json(), new Person());
|
||||
|
||||
HttpRequest request = HttpRequest.create();
|
||||
|
||||
runRoute(route, request)
|
||||
.assertStatusCode(200)
|
||||
.assertMediaType("application/json")
|
||||
.assertEntity("{\"age\":138,\"firstName\":\"Peter\",\"lastName\":\"Parker\"}");
|
||||
}
|
||||
@Test
|
||||
public void completeWithFuture() {
|
||||
Parameter<Integer> x = Parameters.integer("x");
|
||||
Parameter<Integer> y = Parameters.integer("y");
|
||||
|
||||
Handler2<Integer, Integer> slowCalc = new Handler2<Integer, Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(final RequestContext ctx, final Integer x, final Integer y) {
|
||||
return ctx.completeWith(Futures.future(new Callable<RouteResult>() {
|
||||
@Override
|
||||
public RouteResult call() throws Exception {
|
||||
int result = x + y;
|
||||
return ctx.complete(String.format("%d + %d = %d",x, y, result));
|
||||
}
|
||||
}, executionContext()));
|
||||
}
|
||||
};
|
||||
|
||||
Route route = handleWith(x, y, slowCalc);
|
||||
runRoute(route, HttpRequest.GET("add?x=42&y=23"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("42 + 23 = 65");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import akka.http.scaladsl.model.HttpRequest;
|
||||
import org.junit.Test;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
|
||||
public class HandlerBindingTest extends JUnitRouteTest {
|
||||
@Test
|
||||
public void testHandlerWithoutExtractions() {
|
||||
Route route = handleWith(
|
||||
new Handler() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx) {
|
||||
return ctx.complete("Ok");
|
||||
}
|
||||
}
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("/"))
|
||||
.assertEntity("Ok");
|
||||
}
|
||||
@Test
|
||||
public void testHandlerWithSomeExtractions() {
|
||||
final Parameter<Integer> a = Parameters.integer("a");
|
||||
final Parameter<Integer> b = Parameters.integer("b");
|
||||
|
||||
Route route = handleWith(
|
||||
new Handler() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx) {
|
||||
return ctx.complete("Ok a:" + a.get(ctx) +" b:" + b.get(ctx));
|
||||
}
|
||||
}, a, b
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("?a=23&b=42"))
|
||||
.assertEntity("Ok a:23 b:42");
|
||||
}
|
||||
@Test
|
||||
public void testHandlerIfExtractionFails() {
|
||||
final Parameter<Integer> a = Parameters.integer("a");
|
||||
|
||||
Route route = handleWith(
|
||||
new Handler() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx) {
|
||||
return ctx.complete("Ok " + a.get(ctx));
|
||||
}
|
||||
}, a
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("/"))
|
||||
.assertStatusCode(404)
|
||||
.assertEntity("Request is missing required query parameter 'a'");
|
||||
}
|
||||
@Test
|
||||
public void testHandler1() {
|
||||
final Parameter<Integer> a = Parameters.integer("a");
|
||||
|
||||
Route route = handleWith(
|
||||
a,
|
||||
new Handler1<Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, Integer a) {
|
||||
return ctx.complete("Ok " + a);
|
||||
}
|
||||
}
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("?a=23"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Ok 23");
|
||||
}
|
||||
@Test
|
||||
public void testHandler2() {
|
||||
Route route = handleWith(
|
||||
Parameters.integer("a"),
|
||||
Parameters.integer("b"),
|
||||
new Handler2<Integer, Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, Integer a, Integer b) {
|
||||
return ctx.complete("Sum: " + (a + b));
|
||||
}
|
||||
}
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("?a=23&b=42"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Sum: 65");
|
||||
}
|
||||
@Test
|
||||
public void testHandler3() {
|
||||
Route route = handleWith(
|
||||
Parameters.integer("a"),
|
||||
Parameters.integer("b"),
|
||||
Parameters.integer("c"),
|
||||
new Handler3<Integer, Integer, Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, Integer a, Integer b, Integer c) {
|
||||
return ctx.complete("Sum: " + (a + b + c));
|
||||
}
|
||||
}
|
||||
);
|
||||
TestResponse response = runRoute(route, HttpRequest.GET("?a=23&b=42&c=30"));
|
||||
response.assertStatusCode(200);
|
||||
response.assertEntity("Sum: 95");
|
||||
}
|
||||
@Test
|
||||
public void testHandler4() {
|
||||
Route route = handleWith(
|
||||
Parameters.integer("a"),
|
||||
Parameters.integer("b"),
|
||||
Parameters.integer("c"),
|
||||
Parameters.integer("d"),
|
||||
new Handler4<Integer, Integer, Integer, Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, Integer a, Integer b, Integer c, Integer d) {
|
||||
return ctx.complete("Sum: " + (a + b + c + d));
|
||||
}
|
||||
}
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("?a=23&b=42&c=30&d=45"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Sum: 140");
|
||||
}
|
||||
@Test
|
||||
public void testReflectiveInstanceHandler() {
|
||||
class Test {
|
||||
public RouteResult negate(RequestContext ctx, int a) {
|
||||
return ctx.complete("Negated: " + (- a));
|
||||
}
|
||||
}
|
||||
Route route = handleWith(new Test(), "negate", Parameters.integer("a"));
|
||||
runRoute(route, HttpRequest.GET("?a=23"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Negated: -23");
|
||||
}
|
||||
|
||||
public static RouteResult squared(RequestContext ctx, int a) {
|
||||
return ctx.complete("Squared: " + (a * a));
|
||||
}
|
||||
@Test
|
||||
public void testStaticReflectiveHandler() {
|
||||
Route route = handleWith(HandlerBindingTest.class, "squared", Parameters.integer("a"));
|
||||
runRoute(route, HttpRequest.GET("?a=23"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Squared: 529");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import akka.http.scaladsl.model.HttpRequest;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PathDirectivesTest extends JUnitRouteTest {
|
||||
@Test
|
||||
public void testPathPrefixAndPath() {
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
pathPrefix("pet").route(
|
||||
path("cat").route(complete("The cat!")),
|
||||
path("dog").route(complete("The dog!")),
|
||||
pathSingleSlash().route(complete("Here are only pets."))
|
||||
)
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/pet/"))
|
||||
.assertEntity("Here are only pets.");
|
||||
|
||||
route.run(HttpRequest.GET("/pet")) // missing trailing slash
|
||||
.assertStatusCode(404);
|
||||
|
||||
route.run(HttpRequest.GET("/pet/cat"))
|
||||
.assertEntity("The cat!");
|
||||
|
||||
route.run(HttpRequest.GET("/pet/dog"))
|
||||
.assertEntity("The dog!");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRawPathPrefix() {
|
||||
TestRoute route1 =
|
||||
testRoute(
|
||||
rawPathPrefix(PathMatchers.SLASH(), "pet", PathMatchers.SLASH(), "", PathMatchers.SLASH(), "cat").route(
|
||||
complete("The cat!")
|
||||
)
|
||||
);
|
||||
|
||||
route1.run(HttpRequest.GET("/pet//cat"))
|
||||
.assertEntity("The cat!");
|
||||
|
||||
// any suffix allowed
|
||||
route1.run(HttpRequest.GET("/pet//cat/abcdefg"))
|
||||
.assertEntity("The cat!");
|
||||
|
||||
TestRoute route2 =
|
||||
testRoute(
|
||||
rawPathPrefix(PathMatchers.SLASH(), "pet", PathMatchers.SLASH(), "", PathMatchers.SLASH(), "cat", PathMatchers.END()).route(
|
||||
complete("The cat!")
|
||||
)
|
||||
);
|
||||
|
||||
route2.run(HttpRequest.GET("/pet//cat"))
|
||||
.assertEntity("The cat!");
|
||||
|
||||
route2.run(HttpRequest.GET("/pet//cat/abcdefg"))
|
||||
.assertStatusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSegment() {
|
||||
PathMatcher<String> name = PathMatchers.segment();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("hey", name).route(toStringEcho(name))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/hey/jude"))
|
||||
.assertEntity("jude");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleSlash() {
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
pathSingleSlash().route(complete("Ok"))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/"))
|
||||
.assertEntity("Ok");
|
||||
|
||||
route.run(HttpRequest.GET("/abc"))
|
||||
.assertStatusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntegerMatcher() {
|
||||
PathMatcher<Integer> age = PathMatchers.integerNumber();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("age", age).route(toStringEcho(age))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/age/38"))
|
||||
.assertEntity("38");
|
||||
|
||||
route.run(HttpRequest.GET("/age/abc"))
|
||||
.assertStatusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTwoVals() {
|
||||
// tests that `x` and `y` have different identities which is important for
|
||||
// retrieving the values
|
||||
PathMatcher<Integer> x = PathMatchers.integerNumber();
|
||||
PathMatcher<Integer> y = PathMatchers.integerNumber();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("multiply", x, y).route(
|
||||
handleWith(x, y, new Handler2<Integer, Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, Integer x, Integer y) {
|
||||
return ctx.complete(String.format("%d * %d = %d", x, y, x * y));
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/multiply/3/6"))
|
||||
.assertEntity("3 * 6 = 18");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHexIntegerMatcher() {
|
||||
PathMatcher<Integer> color = PathMatchers.hexIntegerNumber();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("color", color).route(toStringEcho(color))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/color/a0c2ef"))
|
||||
.assertEntity(Integer.toString(0xa0c2ef));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongMatcher() {
|
||||
PathMatcher<Long> bigAge = PathMatchers.longNumber();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("bigage", bigAge).route(toStringEcho(bigAge))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/bigage/12345678901"))
|
||||
.assertEntity("12345678901");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHexLongMatcher() {
|
||||
PathMatcher<Long> code = PathMatchers.hexLongNumber();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("code", code).route(toStringEcho(code))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/code/a0b1c2d3e4f5"))
|
||||
.assertEntity(Long.toString(0xa0b1c2d3e4f5L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestMatcher() {
|
||||
PathMatcher<String> rest = PathMatchers.rest();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("pets", rest).route(toStringEcho(rest))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/pets/afdaoisd/asda/sfasfasf/asf"))
|
||||
.assertEntity("afdaoisd/asda/sfasfasf/asf");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUUIDMatcher() {
|
||||
PathMatcher<UUID> uuid = PathMatchers.uuid();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("by-uuid", uuid).route(toStringEcho(uuid))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/by-uuid/6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
|
||||
.assertEntity("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSegmentsMatcher() {
|
||||
PathMatcher<List<String>> segments = PathMatchers.segments();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("pets", segments).route(toStringEcho(segments))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/pets/cat/dog"))
|
||||
.assertEntity("[cat, dog]");
|
||||
}
|
||||
|
||||
private <T> Route toStringEcho(RequestVal<T> value) {
|
||||
return handleWith(value, new Handler1<T>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, T t) {
|
||||
return ctx.complete(t.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.examples.petstore;
|
||||
|
||||
import akka.http.javadsl.marshallers.jackson.Jackson;
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.model.MediaTypes;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import akka.http.javadsl.testkit.TestRoute;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class PetStoreAPITest extends JUnitRouteTest {
|
||||
@Test
|
||||
public void testGetPet() {
|
||||
TestResponse response = createRoute().run(HttpRequest.GET("/pet/1"));
|
||||
|
||||
response
|
||||
.assertStatusCode(200)
|
||||
.assertMediaType("application/json");
|
||||
|
||||
Pet pet = response.entityAs(Jackson.jsonAs(Pet.class));
|
||||
assertEquals("cat", pet.getName());
|
||||
assertEquals(1, pet.getId());
|
||||
}
|
||||
@Test
|
||||
public void testGetMissingPet() {
|
||||
createRoute().run(HttpRequest.GET("/pet/999"))
|
||||
.assertStatusCode(404);
|
||||
}
|
||||
@Test
|
||||
public void testPutPet() {
|
||||
HttpRequest request =
|
||||
HttpRequest.PUT("/pet/1")
|
||||
.withEntity(MediaTypes.APPLICATION_JSON.toContentType(), "{\"id\": 1, \"name\": \"giraffe\"}");
|
||||
|
||||
TestResponse response = createRoute().run(request);
|
||||
|
||||
response.assertStatusCode(200);
|
||||
|
||||
Pet pet = response.entityAs(Jackson.jsonAs(Pet.class));
|
||||
assertEquals("giraffe", pet.getName());
|
||||
assertEquals(1, pet.getId());
|
||||
}
|
||||
@Test
|
||||
public void testDeletePet() {
|
||||
Map<Integer, Pet> data = createData();
|
||||
|
||||
HttpRequest request = HttpRequest.DELETE("/pet/0");
|
||||
|
||||
createRoute(data).run(request)
|
||||
.assertStatusCode(200);
|
||||
|
||||
// test actual deletion from data store
|
||||
assertFalse(data.containsKey(0));
|
||||
}
|
||||
|
||||
private TestRoute createRoute() {
|
||||
return createRoute(createData());
|
||||
}
|
||||
private TestRoute createRoute(Map<Integer, Pet> pets) {
|
||||
return testRoute(PetStoreExample.appRoute(pets));
|
||||
}
|
||||
private Map<Integer, Pet> createData() {
|
||||
Map<Integer, Pet> pets = new HashMap<Integer, Pet>();
|
||||
Pet dog = new Pet(0, "dog");
|
||||
Pet cat = new Pet(1, "cat");
|
||||
pets.put(0, dog);
|
||||
pets.put(1, cat);
|
||||
|
||||
return pets;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.examples.simple;
|
||||
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SimpleServerTest extends JUnitRouteTest {
|
||||
TestRoute route = testRoute(new SimpleServerApp().createRoute());
|
||||
|
||||
@Test
|
||||
public void testAdd() {
|
||||
TestResponse response = route.run(HttpRequest.GET("/add?x=42&y=23"));
|
||||
|
||||
response
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("42 + 23 = 65");
|
||||
}
|
||||
}
|
||||
1
akka-http-tests/src/test/resources/sample.html
Normal file
1
akka-http-tests/src/test/resources/sample.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<p>Lorem ipsum!</p>
|
||||
1
akka-http-tests/src/test/resources/sample.xyz
Normal file
1
akka-http-tests/src/test/resources/sample.xyz
Normal file
|
|
@ -0,0 +1 @@
|
|||
XyZ
|
||||
1
akka-http-tests/src/test/resources/someDir/fileA.txt
Normal file
1
akka-http-tests/src/test/resources/someDir/fileA.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
123
|
||||
0
akka-http-tests/src/test/resources/someDir/fileB.xml
Normal file
0
akka-http-tests/src/test/resources/someDir/fileB.xml
Normal file
0
akka-http-tests/src/test/resources/someDir/sub/file.html
Normal file
0
akka-http-tests/src/test/resources/someDir/sub/file.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
123
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.{ BeforeAndAfterAll, Matchers, WordSpec }
|
||||
import org.scalatest.concurrent.ScalaFutures
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshal
|
||||
import akka.http.scaladsl.marshalling.Marshal
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
class FormDataSpec extends WordSpec with Matchers with ScalaFutures with BeforeAndAfterAll {
|
||||
implicit val system = ActorSystem(getClass.getSimpleName)
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
val formData = FormData(Map("surname" -> "Smith", "age" -> "42"))
|
||||
|
||||
"The FormData infrastructure" should {
|
||||
"properly round-trip the fields of www-urlencoded forms" in {
|
||||
Marshal(formData).to[HttpEntity]
|
||||
.flatMap(Unmarshal(_).to[FormData]).futureValue shouldEqual formData
|
||||
}
|
||||
|
||||
"properly marshal www-urlencoded forms containing special chars" in {
|
||||
Marshal(FormData(Map("name" -> "Smith&Wesson"))).to[HttpEntity]
|
||||
.flatMap(Unmarshal(_).to[String]).futureValue shouldEqual "name=Smith%26Wesson"
|
||||
|
||||
Marshal(FormData(Map("name" -> "Smith+Wesson; hopefully!"))).to[HttpEntity]
|
||||
.flatMap(Unmarshal(_).to[String]).futureValue shouldEqual "name=Smith%2BWesson%3B+hopefully%21"
|
||||
}
|
||||
}
|
||||
|
||||
override def afterAll() = {
|
||||
system.shutdown()
|
||||
system.awaitTermination(10.seconds)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.{ Suite, BeforeAndAfterAll, Matchers }
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.util.ByteString
|
||||
|
||||
trait CodecSpecSupport extends Matchers with BeforeAndAfterAll { self: Suite ⇒
|
||||
|
||||
def readAs(string: String, charset: String = "UTF8") = equal(string).matcher[String] compose { (_: ByteString).decodeString(charset) }
|
||||
def hexDump(bytes: ByteString) = bytes.map("%02x".format(_)).mkString
|
||||
def fromHexDump(dump: String) = dump.grouped(2).toArray.map(chars ⇒ Integer.parseInt(new String(chars), 16).toByte)
|
||||
|
||||
def printBytes(i: Int, id: String) = {
|
||||
def byte(i: Int) = (i & 0xFF).toHexString
|
||||
println(id + ": " + byte(i) + ":" + byte(i >> 8) + ":" + byte(i >> 16) + ":" + byte(i >> 24))
|
||||
i
|
||||
}
|
||||
|
||||
lazy val smallTextBytes = ByteString(smallText, "UTF8")
|
||||
lazy val largeTextBytes = ByteString(largeText, "UTF8")
|
||||
|
||||
val smallText = "Yeah!"
|
||||
val largeText =
|
||||
"""Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore
|
||||
magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
|
||||
gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing
|
||||
elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos
|
||||
et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor
|
||||
sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
|
||||
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
|
||||
gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||
|
||||
Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat
|
||||
nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis
|
||||
dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh
|
||||
euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
|
||||
|
||||
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo
|
||||
consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu
|
||||
feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit
|
||||
augue duis dolore te feugait nulla facilisi.
|
||||
|
||||
Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim
|
||||
assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet
|
||||
dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit
|
||||
lobortis nisl ut aliquip ex ea commodo consequat.
|
||||
|
||||
Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat
|
||||
nulla facilisis.
|
||||
|
||||
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem
|
||||
ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt
|
||||
ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
|
||||
Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
|
||||
consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et
|
||||
et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua.
|
||||
est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
|
||||
invidunt ut labore et dolore magna aliquyam erat.
|
||||
|
||||
Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
|
||||
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus
|
||||
est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy e""".replace("\r\n", "\n")
|
||||
|
||||
implicit val system = ActorSystem(getClass.getSimpleName)
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
|
||||
override def afterAll() = {
|
||||
system.shutdown()
|
||||
system.awaitTermination(10.seconds)
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.WordSpec
|
||||
import akka.util.ByteString
|
||||
import akka.stream.stage.{ SyncDirective, Context, PushStage, Stage }
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.impl.util._
|
||||
import headers._
|
||||
import HttpMethods.POST
|
||||
|
||||
class DecoderSpec extends WordSpec with CodecSpecSupport {
|
||||
|
||||
"A Decoder" should {
|
||||
"not transform the message if it doesn't contain a Content-Encoding header" in {
|
||||
val request = HttpRequest(POST, entity = HttpEntity(smallText))
|
||||
DummyDecoder.decode(request) shouldEqual request
|
||||
}
|
||||
"correctly transform the message if it contains a Content-Encoding header" in {
|
||||
val request = HttpRequest(POST, entity = HttpEntity(smallText), headers = List(`Content-Encoding`(DummyDecoder.encoding)))
|
||||
val decoded = DummyDecoder.decode(request)
|
||||
decoded.headers shouldEqual Nil
|
||||
decoded.entity.toStrict(1.second).awaitResult(1.second) shouldEqual HttpEntity(dummyDecompress(smallText))
|
||||
}
|
||||
}
|
||||
|
||||
def dummyDecompress(s: String): String = dummyDecompress(ByteString(s, "UTF8")).decodeString("UTF8")
|
||||
def dummyDecompress(bytes: ByteString): ByteString = DummyDecoder.decode(bytes).awaitResult(1.second)
|
||||
|
||||
case object DummyDecoder extends StreamDecoder {
|
||||
val encoding = HttpEncodings.compress
|
||||
|
||||
def newDecompressorStage(maxBytesPerChunk: Int): () ⇒ Stage[ByteString, ByteString] =
|
||||
() ⇒ new PushStage[ByteString, ByteString] {
|
||||
def onPush(elem: ByteString, ctx: Context[ByteString]): SyncDirective =
|
||||
ctx.push(elem ++ ByteString("compressed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.util.ByteString
|
||||
|
||||
import java.io.{ InputStream, OutputStream }
|
||||
import java.util.zip._
|
||||
|
||||
class DeflateSpec extends CoderSpec {
|
||||
protected def Coder: Coder with StreamDecoder = Deflate
|
||||
|
||||
protected def newDecodedInputStream(underlying: InputStream): InputStream =
|
||||
new InflaterInputStream(underlying)
|
||||
|
||||
protected def newEncodedOutputStream(underlying: OutputStream): OutputStream =
|
||||
new DeflaterOutputStream(underlying)
|
||||
|
||||
override def extraTests(): Unit = {
|
||||
"throw early if header is corrupt" in {
|
||||
(the[RuntimeException] thrownBy {
|
||||
ourDecode(ByteString(0, 1, 2, 3, 4))
|
||||
}).getCause should be(a[DataFormatException])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.util.ByteString
|
||||
import org.scalatest.WordSpec
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
import HttpMethods.POST
|
||||
import scala.concurrent.duration._
|
||||
import akka.http.impl.util._
|
||||
|
||||
class EncoderSpec extends WordSpec with CodecSpecSupport {
|
||||
|
||||
"An Encoder" should {
|
||||
"not transform the message if messageFilter returns false" in {
|
||||
val request = HttpRequest(POST, entity = HttpEntity(smallText.getBytes("UTF8")))
|
||||
DummyEncoder.encode(request) shouldEqual request
|
||||
}
|
||||
"correctly transform the HttpMessage if messageFilter returns true" in {
|
||||
val request = HttpRequest(POST, entity = HttpEntity(smallText))
|
||||
val encoded = DummyEncoder.encode(request)
|
||||
encoded.headers shouldEqual List(`Content-Encoding`(DummyEncoder.encoding))
|
||||
encoded.entity.toStrict(1.second).awaitResult(1.second) shouldEqual HttpEntity(dummyCompress(smallText))
|
||||
}
|
||||
}
|
||||
|
||||
def dummyCompress(s: String): String = dummyCompress(ByteString(s, "UTF8")).utf8String
|
||||
def dummyCompress(bytes: ByteString): ByteString = DummyCompressor.compressAndFinish(bytes)
|
||||
|
||||
case object DummyEncoder extends Encoder {
|
||||
val messageFilter = Encoder.DefaultFilter
|
||||
val encoding = HttpEncodings.compress
|
||||
def newCompressor = DummyCompressor
|
||||
}
|
||||
|
||||
case object DummyCompressor extends Compressor {
|
||||
def compress(input: ByteString) = input ++ ByteString("compressed")
|
||||
def flush() = ByteString.empty
|
||||
def finish() = ByteString.empty
|
||||
|
||||
def compressAndFlush(input: ByteString): ByteString = compress(input)
|
||||
def compressAndFinish(input: ByteString): ByteString = compress(input)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.http.impl.util._
|
||||
|
||||
import java.io.{ InputStream, OutputStream }
|
||||
import java.util.zip.{ ZipException, GZIPInputStream, GZIPOutputStream }
|
||||
|
||||
import akka.util.ByteString
|
||||
|
||||
class GzipSpec extends CoderSpec {
|
||||
protected def Coder: Coder with StreamDecoder = Gzip
|
||||
|
||||
protected def newDecodedInputStream(underlying: InputStream): InputStream =
|
||||
new GZIPInputStream(underlying)
|
||||
|
||||
protected def newEncodedOutputStream(underlying: OutputStream): OutputStream =
|
||||
new GZIPOutputStream(underlying)
|
||||
|
||||
override def extraTests(): Unit = {
|
||||
"decode concatenated compressions" in {
|
||||
pending // FIXME: unbreak
|
||||
ourDecode(Seq(encode("Hello, "), encode("dear "), encode("User!")).join) should readAs("Hello, dear User!")
|
||||
}
|
||||
"provide a better compression ratio than the standard Gzip/Gunzip streams" in {
|
||||
ourEncode(largeTextBytes).length should be < streamEncode(largeTextBytes).length
|
||||
}
|
||||
"throw an error on truncated input" in {
|
||||
pending // FIXME: unbreak
|
||||
val ex = the[RuntimeException] thrownBy ourDecode(streamEncode(smallTextBytes).dropRight(5))
|
||||
ex.getCause.getMessage should equal("Truncated GZIP stream")
|
||||
}
|
||||
"throw an error if compressed data is just missing the trailer at the end" in {
|
||||
def brokenCompress(payload: String) = Gzip.newCompressor.compress(ByteString(payload, "UTF-8"))
|
||||
|
||||
val ex = the[RuntimeException] thrownBy ourDecode(brokenCompress("abcdefghijkl"))
|
||||
ex.getCause.getMessage should equal("Truncated GZIP stream")
|
||||
}
|
||||
"throw early if header is corrupt" in {
|
||||
val cause = (the[RuntimeException] thrownBy ourDecode(ByteString(0, 1, 2, 3, 4))).getCause
|
||||
cause should (be(a[ZipException]) and have message "Not in GZIP format")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import java.io.{ OutputStream, InputStream }
|
||||
|
||||
class NoCodingSpec extends CoderSpec {
|
||||
protected def Coder: Coder with StreamDecoder = NoCoding
|
||||
|
||||
override protected def corruptInputCheck = false
|
||||
|
||||
protected def newEncodedOutputStream(underlying: OutputStream): OutputStream = underlying
|
||||
protected def newDecodedInputStream(underlying: InputStream): InputStream = underlying
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshallers
|
||||
|
||||
import akka.http.scaladsl.marshalling.ToEntityMarshaller
|
||||
import akka.http.scaladsl.model.{ HttpCharsets, HttpEntity, MediaTypes }
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller
|
||||
import akka.http.impl.util._
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
case class Employee(fname: String, name: String, age: Int, id: Long, boardMember: Boolean) {
|
||||
require(!boardMember || age > 40, "Board members must be older than 40")
|
||||
}
|
||||
|
||||
object Employee {
|
||||
val simple = Employee("Frank", "Smith", 42, 12345, false)
|
||||
val json = """{"fname":"Frank","name":"Smith","age":42,"id":12345,"boardMember":false}"""
|
||||
|
||||
val utf8 = Employee("Fränk", "Smi√", 42, 12345, false)
|
||||
val utf8json =
|
||||
"""{
|
||||
| "fname": "Fränk",
|
||||
| "name": "Smi√",
|
||||
| "age": 42,
|
||||
| "id": 12345,
|
||||
| "boardMember": false
|
||||
|}""".stripMargin.getBytes(HttpCharsets.`UTF-8`.nioCharset)
|
||||
|
||||
val illegalEmployeeJson = """{"fname":"Little Boy","name":"Smith","age":7,"id":12345,"boardMember":true}"""
|
||||
}
|
||||
|
||||
/** Common infrastructure needed for several json support subprojects */
|
||||
abstract class JsonSupportSpec extends WordSpec with Matchers with ScalatestRouteTest {
|
||||
require(getClass.getSimpleName.endsWith("Spec"))
|
||||
// assuming that the classname ends with "Spec"
|
||||
def name: String = getClass.getSimpleName.dropRight(4)
|
||||
implicit def marshaller: ToEntityMarshaller[Employee]
|
||||
implicit def unmarshaller: FromEntityUnmarshaller[Employee]
|
||||
|
||||
"The " + name should {
|
||||
"provide unmarshalling support for a case class" in {
|
||||
HttpEntity(MediaTypes.`application/json`, Employee.json) should unmarshalToValue(Employee.simple)
|
||||
}
|
||||
"provide marshalling support for a case class" in {
|
||||
val marshalled = marshal(Employee.simple)
|
||||
|
||||
marshalled.data.utf8String shouldEqual
|
||||
"""{
|
||||
| "age": 42,
|
||||
| "boardMember": false,
|
||||
| "fname": "Frank",
|
||||
| "id": 12345,
|
||||
| "name": "Smith"
|
||||
|}""".stripMarginWithNewline("\n")
|
||||
}
|
||||
"use UTF-8 as the default charset for JSON source decoding" in {
|
||||
HttpEntity(MediaTypes.`application/json`, Employee.utf8json) should unmarshalToValue(Employee.utf8)
|
||||
}
|
||||
"provide proper error messages for requirement errors" in {
|
||||
val result = unmarshal(HttpEntity(MediaTypes.`application/json`, Employee.illegalEmployeeJson))
|
||||
|
||||
result.isFailure shouldEqual true
|
||||
val ex = result.failed.get
|
||||
ex.getMessage shouldEqual "requirement failed: Board members must be older than 40"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshallers.sprayjson
|
||||
|
||||
import java.lang.StringBuilder
|
||||
|
||||
import akka.http.scaladsl.marshallers.{ JsonSupportSpec, Employee }
|
||||
import akka.http.scaladsl.marshalling.ToEntityMarshaller
|
||||
import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller
|
||||
import spray.json.{ JsValue, PrettyPrinter, JsonPrinter, DefaultJsonProtocol }
|
||||
|
||||
import scala.collection.immutable.ListMap
|
||||
|
||||
class SprayJsonSupportSpec extends JsonSupportSpec {
|
||||
object EmployeeJsonProtocol extends DefaultJsonProtocol {
|
||||
implicit val employeeFormat = jsonFormat5(Employee.apply)
|
||||
}
|
||||
import EmployeeJsonProtocol._
|
||||
|
||||
implicit val orderedFieldPrint: JsonPrinter = new PrettyPrinter {
|
||||
override protected def printObject(members: Map[String, JsValue], sb: StringBuilder, indent: Int): Unit =
|
||||
super.printObject(ListMap(members.toSeq.sortBy(_._1): _*), sb, indent)
|
||||
}
|
||||
|
||||
implicit def marshaller: ToEntityMarshaller[Employee] = SprayJsonSupport.sprayJsonMarshaller[Employee]
|
||||
implicit def unmarshaller: FromEntityUnmarshaller[Employee] = SprayJsonSupport.sprayJsonUnmarshaller[Employee]
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshallers.xml
|
||||
|
||||
import scala.xml.NodeSeq
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import akka.http.scaladsl.unmarshalling.{ Unmarshaller, Unmarshal }
|
||||
import akka.http.scaladsl.model._
|
||||
import HttpCharsets._
|
||||
import MediaTypes._
|
||||
|
||||
class ScalaXmlSupportSpec extends WordSpec with Matchers with ScalatestRouteTest {
|
||||
import ScalaXmlSupport._
|
||||
|
||||
"ScalaXmlSupport" should {
|
||||
"NodeSeqMarshaller should marshal xml snippets to `text/xml` content in UTF-8" in {
|
||||
marshal(<employee><nr>Ha“llo</nr></employee>) shouldEqual
|
||||
HttpEntity(ContentType(`text/xml`, `UTF-8`), "<employee><nr>Ha“llo</nr></employee>")
|
||||
}
|
||||
"nodeSeqUnmarshaller should unmarshal `text/xml` content in UTF-8 to NodeSeqs" in {
|
||||
Unmarshal(HttpEntity(`text/xml`, "<int>Hällö</int>")).to[NodeSeq].map(_.text) should evaluateTo("Hällö")
|
||||
}
|
||||
"nodeSeqUnmarshaller should reject `application/octet-stream`" in {
|
||||
Unmarshal(HttpEntity(`application/octet-stream`, "<int>Hällö</int>")).to[NodeSeq].map(_.text) should
|
||||
haveFailedWith(Unmarshaller.UnsupportedContentTypeException(nodeSeqContentTypeRanges: _*))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.{ Matchers, FreeSpec }
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
import akka.http.scaladsl.model._
|
||||
import MediaTypes._
|
||||
import HttpCharsets._
|
||||
|
||||
class ContentNegotiationSpec extends FreeSpec with Matchers {
|
||||
|
||||
"Content Negotiation should work properly for requests with header(s)" - {
|
||||
|
||||
"(without headers)" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept: */*" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept: */*;q=.8" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept: text/*" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`)
|
||||
accept(`audio/ogg`) should reject
|
||||
}
|
||||
|
||||
"Accept: text/*;q=.8" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`)
|
||||
accept(`audio/ogg`) should reject
|
||||
}
|
||||
|
||||
"Accept: text/*;q=0" in test { accept ⇒
|
||||
accept(`text/plain`) should reject
|
||||
accept(`text/xml` withCharset `UTF-16`) should reject
|
||||
accept(`audio/ogg`) should reject
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-16" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should reject
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-16, UTF-8" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-8;q=.2, UTF-16" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-8;q=.2" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `ISO-8859-1`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
|
||||
}
|
||||
|
||||
"Accept-Charset: latin1;q=.1, UTF-8;q=.2" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
|
||||
}
|
||||
|
||||
"Accept-Charset: *" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept-Charset: *;q=0" in test { accept ⇒
|
||||
accept(`text/plain`) should reject
|
||||
accept(`text/plain` withCharset `UTF-16`) should reject
|
||||
}
|
||||
|
||||
"Accept-Charset: us;q=0.1,*;q=0" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `US-ASCII`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should reject
|
||||
}
|
||||
|
||||
"Accept: text/xml, text/html;q=.5" in test { accept ⇒
|
||||
accept(`text/plain`) should reject
|
||||
accept(`text/xml`) should select(`text/xml`, `UTF-8`)
|
||||
accept(`text/html`) should select(`text/html`, `UTF-8`)
|
||||
accept(`text/html`, `text/xml`) should select(`text/xml`, `UTF-8`)
|
||||
accept(`text/xml`, `text/html`) should select(`text/xml`, `UTF-8`)
|
||||
accept(`text/plain`, `text/xml`) should select(`text/xml`, `UTF-8`)
|
||||
accept(`text/plain`, `text/html`) should select(`text/html`, `UTF-8`)
|
||||
}
|
||||
|
||||
"""Accept: text/html, text/plain;q=0.8, application/*;q=.5, *;q= .2
|
||||
Accept-Charset: UTF-16""" in test { accept ⇒
|
||||
accept(`text/plain`, `text/html`, `audio/ogg`) should select(`text/html`, `UTF-16`)
|
||||
accept(`text/plain`, `text/html` withCharset `UTF-8`, `audio/ogg`) should select(`text/plain`, `UTF-16`)
|
||||
accept(`audio/ogg`, `application/javascript`, `text/plain` withCharset `UTF-8`) should select(`application/javascript`, `UTF-16`)
|
||||
accept(`image/gif`, `application/javascript`) should select(`application/javascript`, `UTF-16`)
|
||||
accept(`image/gif`, `audio/ogg`) should select(`image/gif`, `UTF-16`)
|
||||
}
|
||||
}
|
||||
|
||||
def test[U](body: ((ContentType*) ⇒ Option[ContentType]) ⇒ U): String ⇒ U = { example ⇒
|
||||
val headers =
|
||||
if (example != "(without headers)") {
|
||||
example.split('\n').toList map { rawHeader ⇒
|
||||
val Array(name, value) = rawHeader.split(':')
|
||||
HttpHeader.parse(name.trim, value) match {
|
||||
case HttpHeader.ParsingResult.Ok(header, Nil) ⇒ header
|
||||
case result ⇒ fail(result.errors.head.formatPretty)
|
||||
}
|
||||
}
|
||||
} else Nil
|
||||
val request = HttpRequest(headers = headers)
|
||||
body { contentTypes ⇒
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
implicit val marshallers = contentTypes map {
|
||||
case ct @ ContentType(mt, Some(cs)) ⇒ Marshaller.withFixedCharset(mt, cs)((s: String) ⇒ HttpEntity(ct, s))
|
||||
case ContentType(mt, None) ⇒ Marshaller.withOpenCharset(mt)((s: String, cs) ⇒ HttpEntity(ContentType(mt, cs), s))
|
||||
}
|
||||
Await.result(Marshal("foo").toResponseFor(request)
|
||||
.fast.map(response ⇒ Some(response.entity.contentType))
|
||||
.fast.recover { case _: Marshal.UnacceptableResponseContentTypeException ⇒ None }, 1.second)
|
||||
}
|
||||
}
|
||||
|
||||
def reject = equal(None)
|
||||
def select(mediaType: MediaType, charset: HttpCharset) = equal(Some(ContentType(mediaType, charset)))
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import akka.http.scaladsl.testkit.MarshallingTestUtils
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
|
||||
|
||||
import scala.collection.immutable.ListMap
|
||||
import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers }
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.stream.scaladsl.Source
|
||||
import akka.http.impl.util._
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
import HttpCharsets._
|
||||
import MediaTypes._
|
||||
|
||||
class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with MultipartMarshallers with MarshallingTestUtils {
|
||||
implicit val system = ActorSystem(getClass.getSimpleName)
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
"The PredefinedToEntityMarshallers." - {
|
||||
"StringMarshaller should marshal strings to `text/plain` content in UTF-8" in {
|
||||
marshal("Ha“llo") shouldEqual HttpEntity("Ha“llo")
|
||||
}
|
||||
"CharArrayMarshaller should marshal char arrays to `text/plain` content in UTF-8" in {
|
||||
marshal("Ha“llo".toCharArray) shouldEqual HttpEntity("Ha“llo")
|
||||
}
|
||||
"FormDataMarshaller should marshal FormData instances to application/x-www-form-urlencoded content" in {
|
||||
marshal(FormData(Map("name" -> "Bob", "pass" -> "hällo", "admin" -> ""))) shouldEqual
|
||||
HttpEntity(ContentType(`application/x-www-form-urlencoded`, `UTF-8`), "name=Bob&pass=h%C3%A4llo&admin=")
|
||||
}
|
||||
}
|
||||
|
||||
"The GenericMarshallers." - {
|
||||
"optionMarshaller should enable marshalling of Option[T]" in {
|
||||
|
||||
marshal(Some("Ha“llo")) shouldEqual HttpEntity("Ha“llo")
|
||||
marshal(None: Option[String]) shouldEqual HttpEntity.Empty
|
||||
}
|
||||
"eitherMarshaller should enable marshalling of Either[A, B]" in {
|
||||
marshal[Either[Array[Char], String]](Right("right")) shouldEqual HttpEntity("right")
|
||||
marshal[Either[Array[Char], String]](Left("left".toCharArray)) shouldEqual HttpEntity("left")
|
||||
}
|
||||
}
|
||||
|
||||
"The MultipartMarshallers." - {
|
||||
"multipartMarshaller should correctly marshal multipart content with" - {
|
||||
"one empty part" in {
|
||||
marshal(Multipart.General(`multipart/mixed`, Multipart.General.BodyPart.Strict(""))) shouldEqual HttpEntity(
|
||||
contentType = ContentType(`multipart/mixed` withBoundary randomBoundary),
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=UTF-8
|
||||
|
|
||||
|
|
||||
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
|
||||
}
|
||||
"one non-empty part" in {
|
||||
marshal(Multipart.General(`multipart/alternative`, Multipart.General.BodyPart.Strict(
|
||||
entity = HttpEntity(ContentType(`text/plain`, `UTF-8`), "test@there.com"),
|
||||
headers = `Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")) :: Nil))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/alternative` withBoundary randomBoundary),
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=UTF-8
|
||||
|Content-Disposition: form-data; name=email
|
||||
|
|
||||
|test@there.com
|
||||
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
|
||||
}
|
||||
"two different parts" in {
|
||||
marshal(Multipart.General(`multipart/related`,
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentType(`text/plain`, Some(`US-ASCII`)), "first part, with a trailing linebreak\r\n")),
|
||||
Multipart.General.BodyPart.Strict(
|
||||
HttpEntity(ContentType(`application/octet-stream`), "filecontent"),
|
||||
RawHeader("Content-Transfer-Encoding", "binary") :: Nil))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/related` withBoundary randomBoundary),
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=US-ASCII
|
||||
|
|
||||
|first part, with a trailing linebreak
|
||||
|
|
||||
|--$randomBoundary
|
||||
|Content-Type: application/octet-stream
|
||||
|Content-Transfer-Encoding: binary
|
||||
|
|
||||
|filecontent
|
||||
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
|
||||
}
|
||||
}
|
||||
|
||||
"multipartFormDataMarshaller should correctly marshal 'multipart/form-data' content with" - {
|
||||
"two fields" in {
|
||||
marshal(Multipart.FormData(ListMap(
|
||||
"surname" -> HttpEntity("Mike"),
|
||||
"age" -> marshal(<int>42</int>)))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/form-data` withBoundary randomBoundary),
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=UTF-8
|
||||
|Content-Disposition: form-data; name=surname
|
||||
|
|
||||
|Mike
|
||||
|--$randomBoundary
|
||||
|Content-Type: text/xml; charset=UTF-8
|
||||
|Content-Disposition: form-data; name=age
|
||||
|
|
||||
|<int>42</int>
|
||||
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
|
||||
}
|
||||
|
||||
"two fields having a custom `Content-Disposition`" in {
|
||||
marshal(Multipart.FormData(Source(List(
|
||||
Multipart.FormData.BodyPart("attachment[0]", HttpEntity(`text/csv`, "name,age\r\n\"John Doe\",20\r\n"),
|
||||
Map("filename" -> "attachment.csv")),
|
||||
Multipart.FormData.BodyPart("attachment[1]", HttpEntity("naice!".getBytes),
|
||||
Map("filename" -> "attachment2.csv"), List(RawHeader("Content-Transfer-Encoding", "binary"))))))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/form-data` withBoundary randomBoundary),
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/csv
|
||||
|Content-Disposition: form-data; filename=attachment.csv; name="attachment[0]"
|
||||
|
|
||||
|name,age
|
||||
|"John Doe",20
|
||||
|
|
||||
|--$randomBoundary
|
||||
|Content-Type: application/octet-stream
|
||||
|Content-Disposition: form-data; filename=attachment2.csv; name="attachment[1]"
|
||||
|Content-Transfer-Encoding: binary
|
||||
|
|
||||
|naice!
|
||||
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def afterAll() = system.shutdown()
|
||||
|
||||
protected class FixedRandom extends java.util.Random {
|
||||
override def nextBytes(array: Array[Byte]): Unit = "my-stable-boundary".getBytes("UTF-8").copyToArray(array)
|
||||
}
|
||||
override protected val multipartBoundaryRandom = new FixedRandom // fix for stable value
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.model
|
||||
import model.HttpMethods._
|
||||
import model.StatusCodes
|
||||
|
||||
class BasicRouteSpecs extends RoutingSpec {
|
||||
|
||||
"routes created by the concatenation operator '~'" should {
|
||||
"yield the first sub route if it succeeded" in {
|
||||
Get() ~> {
|
||||
get { complete("first") } ~ get { complete("second") }
|
||||
} ~> check { responseAs[String] shouldEqual "first" }
|
||||
}
|
||||
"yield the second sub route if the first did not succeed" in {
|
||||
Get() ~> {
|
||||
post { complete("first") } ~ get { complete("second") }
|
||||
} ~> check { responseAs[String] shouldEqual "second" }
|
||||
}
|
||||
"collect rejections from both sub routes" in {
|
||||
Delete() ~> {
|
||||
get { completeOk } ~ put { completeOk }
|
||||
} ~> check { rejections shouldEqual Seq(MethodRejection(GET), MethodRejection(PUT)) }
|
||||
}
|
||||
"clear rejections that have already been 'overcome' by previous directives" in {
|
||||
pending
|
||||
/*Put() ~> {
|
||||
put { parameter('yeah) { echoComplete } } ~
|
||||
get { completeOk }
|
||||
} ~> check { rejection shouldEqual MissingQueryParamRejection("yeah") }*/
|
||||
}
|
||||
}
|
||||
|
||||
"Route conjunction" should {
|
||||
val stringDirective = provide("The cat")
|
||||
val intDirective = provide(42)
|
||||
val doubleDirective = provide(23.0)
|
||||
|
||||
val dirStringInt = stringDirective & intDirective
|
||||
val dirStringIntDouble = dirStringInt & doubleDirective
|
||||
val dirDoubleStringInt = doubleDirective & dirStringInt
|
||||
val dirStringIntStringInt = dirStringInt & dirStringInt
|
||||
|
||||
"work for two elements" in {
|
||||
Get("/abc") ~> {
|
||||
dirStringInt { (str, i) ⇒
|
||||
complete(s"$str ${i + 1}")
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "The cat 43" }
|
||||
}
|
||||
"work for 2 + 1" in {
|
||||
Get("/abc") ~> {
|
||||
dirStringIntDouble { (str, i, d) ⇒
|
||||
complete(s"$str ${i + 1} ${d + 0.1}")
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "The cat 43 23.1" }
|
||||
}
|
||||
"work for 1 + 2" in {
|
||||
Get("/abc") ~> {
|
||||
dirDoubleStringInt { (d, str, i) ⇒
|
||||
complete(s"$str ${i + 1} ${d + 0.1}")
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "The cat 43 23.1" }
|
||||
}
|
||||
"work for 2 + 2" in {
|
||||
Get("/abc") ~> {
|
||||
dirStringIntStringInt { (str, i, str2, i2) ⇒
|
||||
complete(s"$str ${i + i2} $str2")
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "The cat 84 The cat" }
|
||||
}
|
||||
}
|
||||
"Route disjunction" should {
|
||||
"work in the happy case" in {
|
||||
val route = Route.seal((path("abc") | path("def")) {
|
||||
completeOk
|
||||
})
|
||||
|
||||
Get("/abc") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
Get("/def") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
Get("/ghi") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.NotFound
|
||||
}
|
||||
}
|
||||
"don't apply alternative if inner route rejects" in {
|
||||
object MyRejection extends Rejection
|
||||
val route = (path("abc") | post) {
|
||||
reject(MyRejection)
|
||||
}
|
||||
Get("/abc") ~> route ~> check {
|
||||
rejection shouldEqual MyRejection
|
||||
}
|
||||
}
|
||||
}
|
||||
"Case class extraction with Directive.as" should {
|
||||
"extract one argument" in {
|
||||
case class MyNumber(i: Int)
|
||||
|
||||
val abcPath = path("abc" / IntNumber).as(MyNumber)(echoComplete)
|
||||
|
||||
Get("/abc/5") ~> abcPath ~> check {
|
||||
responseAs[String] shouldEqual "MyNumber(5)"
|
||||
}
|
||||
}
|
||||
"extract two arguments" in {
|
||||
case class Person(name: String, age: Int)
|
||||
|
||||
val personPath = path("person" / Segment / IntNumber).as(Person)(echoComplete)
|
||||
|
||||
Get("/person/john/38") ~> personPath ~> check {
|
||||
responseAs[String] shouldEqual "Person(john,38)"
|
||||
}
|
||||
}
|
||||
}
|
||||
"Dynamic execution of inner routes of Directive0" should {
|
||||
"re-execute inner routes every time" in {
|
||||
var a = ""
|
||||
val dynamicRoute = get { a += "x"; complete(a) }
|
||||
def expect(route: Route, s: String) = Get() ~> route ~> check { responseAs[String] shouldEqual s }
|
||||
|
||||
expect(dynamicRoute, "x")
|
||||
expect(dynamicRoute, "xx")
|
||||
expect(dynamicRoute, "xxx")
|
||||
expect(dynamicRoute, "xxxx")
|
||||
}
|
||||
}
|
||||
|
||||
case object MyException extends RuntimeException
|
||||
"Route sealing" should {
|
||||
"catch route execution exceptions" in {
|
||||
Get("/abc") ~> Route.seal {
|
||||
get { ctx ⇒
|
||||
throw MyException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
}
|
||||
}
|
||||
"catch route building exceptions" in {
|
||||
Get("/abc") ~> Route.seal {
|
||||
get {
|
||||
throw MyException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
}
|
||||
}
|
||||
"convert all rejections to responses" in {
|
||||
object MyRejection extends Rejection
|
||||
Get("/abc") ~> Route.seal {
|
||||
get {
|
||||
reject(MyRejection)
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
}
|
||||
}
|
||||
"always prioritize MethodRejections over AuthorizationFailedRejections" in {
|
||||
Get("/abc") ~> Route.seal {
|
||||
post { completeOk } ~
|
||||
authorize(false) { completeOk }
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.MethodNotAllowed
|
||||
responseAs[String] shouldEqual "HTTP method not allowed, supported methods: POST"
|
||||
}
|
||||
|
||||
Get("/abc") ~> Route.seal {
|
||||
authorize(false) { completeOk } ~
|
||||
post { completeOk }
|
||||
} ~> check { status shouldEqual StatusCodes.MethodNotAllowed }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import org.scalatest.{ WordSpec, Suite, Matchers }
|
||||
import akka.http.scaladsl.model.HttpResponse
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
|
||||
trait GenericRoutingSpec extends Matchers with Directives with ScalatestRouteTest { this: Suite ⇒
|
||||
val Ok = HttpResponse()
|
||||
val completeOk = complete(Ok)
|
||||
|
||||
def echoComplete[T]: T ⇒ Route = { x ⇒ complete(x.toString) }
|
||||
def echoComplete2[T, U]: (T, U) ⇒ Route = { (x, y) ⇒ complete(s"$x $y") }
|
||||
}
|
||||
|
||||
abstract class RoutingSpec extends WordSpec with GenericRoutingSpec
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport
|
||||
import akka.http.scaladsl.server.directives.UserCredentials
|
||||
import com.typesafe.config.{ ConfigFactory, Config }
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.http.scaladsl.Http
|
||||
|
||||
object TestServer extends App {
|
||||
val testConf: Config = ConfigFactory.parseString("""
|
||||
akka.loglevel = INFO
|
||||
akka.log-dead-letters = off""")
|
||||
implicit val system = ActorSystem("ServerTest", testConf)
|
||||
import system.dispatcher
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
|
||||
import ScalaXmlSupport._
|
||||
import Directives._
|
||||
|
||||
def auth: AuthenticatorPF[String] = {
|
||||
case p @ UserCredentials.Provided(name) if p.verifySecret(name + "-password") ⇒ name
|
||||
}
|
||||
|
||||
val bindingFuture = Http().bindAndHandle({
|
||||
get {
|
||||
path("") {
|
||||
complete(index)
|
||||
} ~
|
||||
path("secure") {
|
||||
authenticateBasicPF("My very secure site", auth) { user ⇒
|
||||
complete(<html><body>Hello <b>{ user }</b>. Access has been granted!</body></html>)
|
||||
}
|
||||
} ~
|
||||
path("ping") {
|
||||
complete("PONG!")
|
||||
} ~
|
||||
path("crash") {
|
||||
complete(sys.error("BOOM!"))
|
||||
}
|
||||
}
|
||||
}, interface = "localhost", port = 8080)
|
||||
|
||||
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
|
||||
Console.readLine()
|
||||
|
||||
bindingFuture.flatMap(_.unbind()).onComplete(_ ⇒ system.shutdown())
|
||||
|
||||
lazy val index =
|
||||
<html>
|
||||
<body>
|
||||
<h1>Say hello to <i>akka-http-core</i>!</h1>
|
||||
<p>Defined resources:</p>
|
||||
<ul>
|
||||
<li><a href="/ping">/ping</a></li>
|
||||
<li><a href="/secure">/secure</a> Use any username and '<username>-password' as credentials</li>
|
||||
<li><a href="/crash">/crash</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
class BasicDirectivesSpec extends RoutingSpec {
|
||||
|
||||
"The `mapUnmatchedPath` directive" should {
|
||||
"map the unmatched path" in {
|
||||
Get("/abc") ~> {
|
||||
mapUnmatchedPath(_ / "def") {
|
||||
path("abc" / "def") { completeOk }
|
||||
}
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"The `extract` directive" should {
|
||||
"extract from the RequestContext" in {
|
||||
Get("/abc") ~> {
|
||||
extract(_.request.method.value) {
|
||||
echoComplete
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "GET" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import StatusCodes._
|
||||
import headers._
|
||||
|
||||
class CacheConditionDirectivesSpec extends RoutingSpec {
|
||||
|
||||
"the `conditional` directive" should {
|
||||
val timestamp = DateTime.now - 2000
|
||||
val ifUnmodifiedSince = `If-Unmodified-Since`(timestamp)
|
||||
val ifModifiedSince = `If-Modified-Since`(timestamp)
|
||||
val tag = EntityTag("fresh")
|
||||
val responseHeaders = List(ETag(tag), `Last-Modified`(timestamp))
|
||||
|
||||
def taggedAndTimestamped = conditional(tag, timestamp) { completeOk }
|
||||
def weak = conditional(tag.copy(weak = true), timestamp) { completeOk }
|
||||
|
||||
"return OK for new resources" in {
|
||||
Get() ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
"return OK for non-matching resources" in {
|
||||
Get() ~> `If-None-Match`(EntityTag("old")) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-Modified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(EntityTag("old")) ~> `If-Modified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
"ignore If-Modified-Since if If-None-Match is defined" in {
|
||||
Get() ~> `If-None-Match`(tag) ~> `If-Modified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
}
|
||||
Get() ~> `If-None-Match`(EntityTag("old")) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
}
|
||||
}
|
||||
|
||||
"return PreconditionFailed for matched but unsafe resources" in {
|
||||
Put() ~> `If-None-Match`(tag) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual PreconditionFailed
|
||||
headers shouldEqual Nil
|
||||
}
|
||||
}
|
||||
|
||||
"return NotModified for matching resources" in {
|
||||
Get() ~> `If-None-Match`.`*` ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(tag) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(tag) ~> `If-Modified-Since`(timestamp + 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(tag.copy(weak = true)) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(tag, EntityTag("some"), EntityTag("other")) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
"return NotModified when only one matching header is set" in {
|
||||
Get() ~> `If-None-Match`.`*` ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(tag) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
"return NotModified for matching weak resources" in {
|
||||
val weakTag = tag.copy(weak = true)
|
||||
Get() ~> `If-None-Match`(tag) ~> weak ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (List(ETag(weakTag), `Last-Modified`(timestamp)))
|
||||
}
|
||||
Get() ~> `If-None-Match`(weakTag) ~> weak ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (List(ETag(weakTag), `Last-Modified`(timestamp)))
|
||||
}
|
||||
}
|
||||
|
||||
"return normally for matching If-Match/If-Unmodified" in {
|
||||
Put() ~> `If-Match`.`*` ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Put() ~> `If-Match`(tag) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Put() ~> ifUnmodifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
"return PreconditionFailed for non-matching If-Match/If-Unmodified" in {
|
||||
Put() ~> `If-Match`(EntityTag("old")) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual PreconditionFailed
|
||||
headers shouldEqual Nil
|
||||
}
|
||||
Put() ~> `If-Unmodified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual PreconditionFailed
|
||||
headers shouldEqual Nil
|
||||
}
|
||||
}
|
||||
|
||||
"ignore If-Unmodified-Since if If-Match is defined" in {
|
||||
Put() ~> `If-Match`(tag) ~> `If-Unmodified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
}
|
||||
Put() ~> `If-Match`(EntityTag("old")) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual PreconditionFailed
|
||||
}
|
||||
}
|
||||
|
||||
"not filter out a `Range` header if `If-Range` does match the timestamp" in {
|
||||
Get() ~> `If-Range`(timestamp) ~> Range(ByteRange(0, 10)) ~> {
|
||||
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] should startWith("Some")
|
||||
}
|
||||
}
|
||||
|
||||
"filter out a `Range` header if `If-Range` doesn't match the timestamp" in {
|
||||
Get() ~> `If-Range`(timestamp - 1000) ~> Range(ByteRange(0, 10)) ~> {
|
||||
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] shouldEqual "None"
|
||||
}
|
||||
}
|
||||
|
||||
"not filter out a `Range` header if `If-Range` does match the ETag" in {
|
||||
Get() ~> `If-Range`(tag) ~> Range(ByteRange(0, 10)) ~> {
|
||||
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] should startWith("Some")
|
||||
}
|
||||
}
|
||||
|
||||
"filter out a `Range` header if `If-Range` doesn't match the ETag" in {
|
||||
Get() ~> `If-Range`(EntityTag("other")) ~> Range(ByteRange(0, 10)) ~> {
|
||||
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] shouldEqual "None"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import org.scalatest.matchers.Matcher
|
||||
import akka.util.ByteString
|
||||
import akka.stream.scaladsl.Source
|
||||
import akka.http.impl.util._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.coding._
|
||||
import headers._
|
||||
import HttpEntity.{ ChunkStreamPart, Chunk }
|
||||
import HttpCharsets._
|
||||
import HttpEncodings._
|
||||
import MediaTypes._
|
||||
import StatusCodes._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class CodingDirectivesSpec extends RoutingSpec {
|
||||
|
||||
val echoRequestContent: Route = { ctx ⇒ ctx.complete(ctx.request.entity.dataBytes.utf8String) }
|
||||
|
||||
val yeah = complete("Yeah!")
|
||||
lazy val yeahGzipped = compress("Yeah!", Gzip)
|
||||
lazy val yeahDeflated = compress("Yeah!", Deflate)
|
||||
|
||||
lazy val helloGzipped = compress("Hello", Gzip)
|
||||
lazy val helloDeflated = compress("Hello", Deflate)
|
||||
|
||||
"the NoEncoding decoder" should {
|
||||
"decode the request content if it has encoding 'identity'" in {
|
||||
Post("/", "yes") ~> `Content-Encoding`(identity) ~> {
|
||||
decodeRequestWith(NoCoding) { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"reject requests with content encoded with 'deflate'" in {
|
||||
Post("/", "yes") ~> `Content-Encoding`(deflate) ~> {
|
||||
decodeRequestWith(NoCoding) { echoRequestContent }
|
||||
} ~> check { rejection shouldEqual UnsupportedRequestEncodingRejection(identity) }
|
||||
}
|
||||
"decode the request content if no Content-Encoding header is present" in {
|
||||
Post("/", "yes") ~> decodeRequestWith(NoCoding) { echoRequestContent } ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"leave request without content unchanged" in {
|
||||
Post() ~> decodeRequestWith(Gzip) { completeOk } ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"the Gzip decoder" should {
|
||||
"decode the request content if it has encoding 'gzip'" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeRequestWith(Gzip) { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "Hello" }
|
||||
}
|
||||
"reject the request content if it has encoding 'gzip' but is corrupt" in {
|
||||
Post("/", fromHexDump("000102")) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeRequestWith(Gzip) { echoRequestContent }
|
||||
} ~> check {
|
||||
status shouldEqual BadRequest
|
||||
responseAs[String] shouldEqual "The request's encoding is corrupt"
|
||||
}
|
||||
}
|
||||
"reject truncated gzip request content" in {
|
||||
Post("/", helloGzipped.dropRight(2)) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeRequestWith(Gzip) { echoRequestContent }
|
||||
} ~> check {
|
||||
status shouldEqual BadRequest
|
||||
responseAs[String] shouldEqual "The request's encoding is corrupt"
|
||||
}
|
||||
}
|
||||
"reject requests with content encoded with 'deflate'" in {
|
||||
Post("/", "Hello") ~> `Content-Encoding`(deflate) ~> {
|
||||
decodeRequestWith(Gzip) { completeOk }
|
||||
} ~> check { rejection shouldEqual UnsupportedRequestEncodingRejection(gzip) }
|
||||
}
|
||||
"reject requests without Content-Encoding header" in {
|
||||
Post("/", "Hello") ~> {
|
||||
decodeRequestWith(Gzip) { completeOk }
|
||||
} ~> check { rejection shouldEqual UnsupportedRequestEncodingRejection(gzip) }
|
||||
}
|
||||
"leave request without content unchanged" in {
|
||||
Post() ~> {
|
||||
decodeRequestWith(Gzip) { completeOk }
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"a (decodeRequestWith(Gzip) | decodeRequestWith(NoEncoding)) compound directive" should {
|
||||
lazy val decodeWithGzipOrNoEncoding = decodeRequestWith(Gzip) | decodeRequestWith(NoCoding)
|
||||
"decode the request content if it has encoding 'gzip'" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeWithGzipOrNoEncoding { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "Hello" }
|
||||
}
|
||||
"decode the request content if it has encoding 'identity'" in {
|
||||
Post("/", "yes") ~> `Content-Encoding`(identity) ~> {
|
||||
decodeWithGzipOrNoEncoding { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"decode the request content if no Content-Encoding header is present" in {
|
||||
Post("/", "yes") ~> decodeWithGzipOrNoEncoding { echoRequestContent } ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"reject requests with content encoded with 'deflate'" in {
|
||||
Post("/", "yes") ~> `Content-Encoding`(deflate) ~> {
|
||||
decodeWithGzipOrNoEncoding { echoRequestContent }
|
||||
} ~> check {
|
||||
rejections shouldEqual Seq(
|
||||
UnsupportedRequestEncodingRejection(gzip),
|
||||
UnsupportedRequestEncodingRejection(identity))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the Gzip encoder" should {
|
||||
"encode the response content with GZIP if the client accepts it with a dedicated Accept-Encoding header" in {
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"encode the response content with GZIP if the request has no Accept-Encoding header" in {
|
||||
Post() ~> {
|
||||
encodeResponseWith(Gzip) { yeah }
|
||||
} ~> check { strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped) }
|
||||
}
|
||||
"reject the request if the client does not accept GZIP encoding" in {
|
||||
Post() ~> `Accept-Encoding`(identity) ~> {
|
||||
encodeResponseWith(Gzip) { completeOk }
|
||||
} ~> check { rejection shouldEqual UnacceptedResponseEncodingRejection(gzip) }
|
||||
}
|
||||
"leave responses without content unchanged" in {
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"leave responses with an already set Content-Encoding header unchanged" in {
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) {
|
||||
RespondWithDirectives.respondWithHeader(`Content-Encoding`(identity)) { completeOk }
|
||||
}
|
||||
} ~> check { response shouldEqual Ok.withHeaders(`Content-Encoding`(identity)) }
|
||||
}
|
||||
"correctly encode the chunk stream produced by a chunked response" in {
|
||||
val text = "This is a somewhat lengthy text that is being chunked by the autochunk directive!"
|
||||
val textChunks =
|
||||
() ⇒ text.grouped(8).map { chars ⇒
|
||||
Chunk(chars.mkString): ChunkStreamPart
|
||||
}
|
||||
val chunkedTextEntity = HttpEntity.Chunked(MediaTypes.`text/plain`, Source(textChunks))
|
||||
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) {
|
||||
complete(chunkedTextEntity)
|
||||
}
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
chunks.size shouldEqual (11 + 1) // 11 regular + the last one
|
||||
val bytes = chunks.foldLeft(ByteString.empty)(_ ++ _.data)
|
||||
Gzip.decode(bytes).awaitResult(1.second) should readAs(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the encodeResponseWith(NoEncoding) directive" should {
|
||||
"produce a response if no Accept-Encoding is present in the request" in {
|
||||
Post() ~> encodeResponseWith(NoCoding) { completeOk } ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"produce a not encoded response if the client only accepts non matching encodings" in {
|
||||
Post() ~> `Accept-Encoding`(gzip, identity) ~> {
|
||||
encodeResponseWith(NoCoding) { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Deflate, NoCoding) { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"reject the request if the request has an 'Accept-Encoding: identity; q=0' header" in {
|
||||
Post() ~> `Accept-Encoding`(identity.withQValue(0f)) ~> {
|
||||
encodeResponseWith(NoCoding) { completeOk }
|
||||
} ~> check { rejection shouldEqual UnacceptedResponseEncodingRejection(identity) }
|
||||
}
|
||||
}
|
||||
|
||||
"a (encodeResponse(Gzip) | encodeResponse(NoEncoding)) compound directive" should {
|
||||
lazy val encodeGzipOrIdentity = encodeResponseWith(Gzip) | encodeResponseWith(NoCoding)
|
||||
"produce a not encoded response if the request has no Accept-Encoding header" in {
|
||||
Post() ~> {
|
||||
encodeGzipOrIdentity { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"produce a GZIP encoded response if the request has an `Accept-Encoding: deflate;q=0.5, gzip` header" in {
|
||||
Post() ~> `Accept-Encoding`(deflate.withQValue(.5f), gzip) ~> {
|
||||
encodeGzipOrIdentity { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"produce a non-encoded response if the request has an `Accept-Encoding: identity` header" in {
|
||||
Post() ~> `Accept-Encoding`(identity) ~> {
|
||||
encodeGzipOrIdentity { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"produce a non-encoded response if the request has an `Accept-Encoding: deflate` header" in {
|
||||
Post() ~> `Accept-Encoding`(deflate) ~> {
|
||||
encodeGzipOrIdentity { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the encodeResponse directive" should {
|
||||
"produce a non-encoded response if the request has no Accept-Encoding header" in {
|
||||
Get("/") ~> {
|
||||
encodeResponse { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"produce a GZIP encoded response if the request has an `Accept-Encoding: gzip, deflate` header" in {
|
||||
Get("/") ~> `Accept-Encoding`(gzip, deflate) ~> {
|
||||
encodeResponse { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"produce a Deflate encoded response if the request has an `Accept-Encoding: deflate` header" in {
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> {
|
||||
encodeResponse { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(deflate)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahDeflated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the encodeResponseWith directive" should {
|
||||
"produce a response encoded with the specified Encoder if the request has a matching Accept-Encoding header" in {
|
||||
Get("/") ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"produce a response encoded with one of the specified Encoders if the request has a matching Accept-Encoding header" in {
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> {
|
||||
encodeResponseWith(Gzip, Deflate) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(deflate)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahDeflated)
|
||||
}
|
||||
}
|
||||
"produce a response encoded with the first of the specified Encoders if the request has no Accept-Encoding header" in {
|
||||
Get("/") ~> {
|
||||
encodeResponseWith(Gzip, Deflate) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"produce a response with no encoding if the request has an empty Accept-Encoding header" in {
|
||||
Get("/") ~> `Accept-Encoding`() ~> {
|
||||
encodeResponseWith(Gzip, Deflate, NoCoding) { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"negotiate the correct content encoding" in {
|
||||
Get("/") ~> `Accept-Encoding`(identity.withQValue(.5f), deflate.withQValue(0f), gzip) ~> {
|
||||
encodeResponseWith(NoCoding, Deflate, Gzip) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"reject the request if it has an Accept-Encoding header with an encoding that doesn't match" in {
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> {
|
||||
encodeResponseWith(Gzip) { yeah }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
"reject the request if it has an Accept-Encoding header with an encoding that matches but is blacklisted" in {
|
||||
Get("/") ~> `Accept-Encoding`(gzip.withQValue(0f)) ~> {
|
||||
encodeResponseWith(Gzip) { yeah }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the decodeRequest directive" should {
|
||||
"decode the request content if it has a `Content-Encoding: gzip` header and the content is gzip encoded" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeRequest { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "Hello" }
|
||||
}
|
||||
"decode the request content if it has a `Content-Encoding: deflate` header and the content is deflate encoded" in {
|
||||
Post("/", helloDeflated) ~> `Content-Encoding`(deflate) ~> {
|
||||
decodeRequest { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "Hello" }
|
||||
}
|
||||
"decode the request content if it has a `Content-Encoding: identity` header and the content is not encoded" in {
|
||||
Post("/", "yes") ~> `Content-Encoding`(identity) ~> {
|
||||
decodeRequest { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"decode the request content using NoEncoding if no Content-Encoding header is present" in {
|
||||
Post("/", "yes") ~> decodeRequest { echoRequestContent } ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"reject the request if it has a `Content-Encoding: deflate` header but the request is encoded with Gzip" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(deflate) ~>
|
||||
decodeRequest { echoRequestContent } ~> check {
|
||||
status shouldEqual BadRequest
|
||||
responseAs[String] shouldEqual "The request's encoding is corrupt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the decodeRequestWith directive" should {
|
||||
"decode the request content if its `Content-Encoding` header matches the specified encoder" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeRequestWith(Gzip) { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "Hello" }
|
||||
}
|
||||
"reject the request if its `Content-Encoding` header doesn't match the specified encoder" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(deflate) ~> {
|
||||
decodeRequestWith(Gzip) { echoRequestContent }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
"reject the request when decodeing with GZIP and no Content-Encoding header is present" in {
|
||||
Post("/", "yes") ~> decodeRequestWith(Gzip) { echoRequestContent } ~> check {
|
||||
rejection shouldEqual UnsupportedRequestEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the (decodeRequest & encodeResponse) compound directive" should {
|
||||
lazy val decodeEncode = decodeRequest & encodeResponse
|
||||
"decode a GZIP encoded request and produce a none encoded response if the request has no Accept-Encoding header" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeEncode { echoRequestContent }
|
||||
} ~> check {
|
||||
response should haveNoContentEncoding
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), "Hello")
|
||||
}
|
||||
}
|
||||
"decode a GZIP encoded request and produce a Deflate encoded response if the request has an `Accept-Encoding: deflate` header" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> `Accept-Encoding`(deflate) ~> {
|
||||
decodeEncode { echoRequestContent }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(deflate)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), helloDeflated)
|
||||
}
|
||||
}
|
||||
"decode an unencoded request and produce a GZIP encoded response if the request has an `Accept-Encoding: gzip` header" in {
|
||||
Post("/", "Hello") ~> `Accept-Encoding`(gzip) ~> {
|
||||
decodeEncode { echoRequestContent }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), helloGzipped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def compress(input: String, encoder: Encoder): ByteString = {
|
||||
val compressor = encoder.newCompressor
|
||||
compressor.compressAndFlush(ByteString(input)) ++ compressor.finish()
|
||||
}
|
||||
|
||||
def hexDump(bytes: Array[Byte]) = bytes.map("%02x" format _).mkString
|
||||
def fromHexDump(dump: String) = dump.grouped(2).toArray.map(chars ⇒ Integer.parseInt(new String(chars), 16).toByte)
|
||||
|
||||
def haveNoContentEncoding: Matcher[HttpResponse] = be(None) compose { (_: HttpResponse).header[`Content-Encoding`] }
|
||||
def haveContentEncoding(encoding: HttpEncoding): Matcher[HttpResponse] =
|
||||
be(Some(`Content-Encoding`(encoding))) compose { (_: HttpResponse).header[`Content-Encoding`] }
|
||||
|
||||
def readAs(string: String, charset: String = "UTF8") = be(string) compose { (_: ByteString).decodeString(charset) }
|
||||
|
||||
def strictify(entity: HttpEntity) = entity.toStrict(1.second).awaitResult(1.second)
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import StatusCodes.OK
|
||||
import headers._
|
||||
|
||||
class CookieDirectivesSpec extends RoutingSpec {
|
||||
|
||||
val deletedTimeStamp = DateTime.fromIsoDateTimeString("1800-01-01T00:00:00")
|
||||
|
||||
"The 'cookie' directive" should {
|
||||
"extract the respectively named cookie" in {
|
||||
Get() ~> addHeader(Cookie(HttpCookie("fancy", "pants"))) ~> {
|
||||
cookie("fancy") { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "fancy=pants" }
|
||||
}
|
||||
"reject the request if the cookie is not present" in {
|
||||
Get() ~> {
|
||||
cookie("fancy") { echoComplete }
|
||||
} ~> check { rejection shouldEqual MissingCookieRejection("fancy") }
|
||||
}
|
||||
"properly pass through inner rejections" in {
|
||||
Get() ~> addHeader(Cookie(HttpCookie("fancy", "pants"))) ~> {
|
||||
cookie("fancy") { c ⇒ reject(ValidationRejection("Dont like " + c.content)) }
|
||||
} ~> check { rejection shouldEqual ValidationRejection("Dont like pants") }
|
||||
}
|
||||
}
|
||||
|
||||
"The 'deleteCookie' directive" should {
|
||||
"add a respective Set-Cookie headers to successful responses" in {
|
||||
Get() ~> {
|
||||
deleteCookie("myCookie", "test.com") { completeOk }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
header[`Set-Cookie`] shouldEqual Some(`Set-Cookie`(HttpCookie("myCookie", "deleted", expires = deletedTimeStamp,
|
||||
domain = Some("test.com"))))
|
||||
}
|
||||
}
|
||||
|
||||
"support deleting multiple cookies at a time" in {
|
||||
Get() ~> {
|
||||
deleteCookie(HttpCookie("myCookie", "test.com"), HttpCookie("myCookie2", "foobar.com")) { completeOk }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
headers.collect { case `Set-Cookie`(x) ⇒ x } shouldEqual List(
|
||||
HttpCookie("myCookie", "deleted", expires = deletedTimeStamp),
|
||||
HttpCookie("myCookie2", "deleted", expires = deletedTimeStamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'optionalCookie' directive" should {
|
||||
"produce a `Some(cookie)` extraction if the cookie is present" in {
|
||||
Get() ~> Cookie(HttpCookie("abc", "123")) ~> {
|
||||
optionalCookie("abc") { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Some(abc=123)" }
|
||||
}
|
||||
"produce a `None` extraction if the cookie is not present" in {
|
||||
Get() ~> optionalCookie("abc") { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"let rejections from its inner route pass through" in {
|
||||
Get() ~> {
|
||||
optionalCookie("test-cookie") { _ ⇒
|
||||
validate(false, "ouch") { completeOk }
|
||||
}
|
||||
} ~> check { rejection shouldEqual ValidationRejection("ouch") }
|
||||
}
|
||||
}
|
||||
|
||||
"The 'setCookie' directive" should {
|
||||
"add a respective Set-Cookie headers to successful responses" in {
|
||||
Get() ~> {
|
||||
setCookie(HttpCookie("myCookie", "test.com")) { completeOk }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
header[`Set-Cookie`] shouldEqual Some(`Set-Cookie`(HttpCookie("myCookie", "test.com")))
|
||||
}
|
||||
}
|
||||
|
||||
"support setting multiple cookies at a time" in {
|
||||
Get() ~> {
|
||||
setCookie(HttpCookie("myCookie", "test.com"), HttpCookie("myCookie2", "foobar.com")) { completeOk }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
headers.collect { case `Set-Cookie`(x) ⇒ x } shouldEqual List(
|
||||
HttpCookie("myCookie", "test.com"), HttpCookie("myCookie2", "foobar.com"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.http.impl.util._
|
||||
|
||||
class DebuggingDirectivesSpec extends RoutingSpec {
|
||||
var debugMsg = ""
|
||||
|
||||
def resetDebugMsg(): Unit = { debugMsg = "" }
|
||||
|
||||
val log = new LoggingAdapter {
|
||||
def isErrorEnabled = true
|
||||
def isWarningEnabled = true
|
||||
def isInfoEnabled = true
|
||||
def isDebugEnabled = true
|
||||
|
||||
def notifyError(message: String): Unit = {}
|
||||
def notifyError(cause: Throwable, message: String): Unit = {}
|
||||
def notifyWarning(message: String): Unit = {}
|
||||
def notifyInfo(message: String): Unit = {}
|
||||
def notifyDebug(message: String): Unit = { debugMsg += message + '\n' }
|
||||
}
|
||||
|
||||
"The 'logRequest' directive" should {
|
||||
"produce a proper log message for incoming requests" in {
|
||||
val route =
|
||||
withLog(log)(
|
||||
logRequest("1")(
|
||||
completeOk))
|
||||
|
||||
resetDebugMsg()
|
||||
Get("/hello") ~> route ~> check {
|
||||
response shouldEqual Ok
|
||||
debugMsg shouldEqual "1: HttpRequest(HttpMethod(GET),http://example.com/hello,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1))\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'logResponse' directive" should {
|
||||
"produce a proper log message for outgoing responses" in {
|
||||
val route =
|
||||
withLog(log)(
|
||||
logResult("2")(
|
||||
completeOk))
|
||||
|
||||
resetDebugMsg()
|
||||
Get("/hello") ~> route ~> check {
|
||||
response shouldEqual Ok
|
||||
debugMsg shouldEqual "2: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1)))\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'logRequestResponse' directive" should {
|
||||
"produce proper log messages for outgoing responses, thereby showing the corresponding request" in {
|
||||
val route =
|
||||
withLog(log)(
|
||||
logRequestResult("3")(
|
||||
completeOk))
|
||||
|
||||
resetDebugMsg()
|
||||
Get("/hello") ~> route ~> check {
|
||||
response shouldEqual Ok
|
||||
debugMsg shouldEqual """|3: Response for
|
||||
| Request : HttpRequest(HttpMethod(GET),http://example.com/hello,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1))
|
||||
| Response: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1)))
|
||||
|""".stripMarginWithNewline("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.{ MediaTypes, MediaRanges, StatusCodes }
|
||||
import akka.http.scaladsl.model.headers._
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class ExecutionDirectivesSpec extends RoutingSpec {
|
||||
object MyException extends RuntimeException
|
||||
val handler =
|
||||
ExceptionHandler {
|
||||
case MyException ⇒ complete(500, "Pling! Plong! Something went wrong!!!")
|
||||
}
|
||||
|
||||
"The `handleExceptions` directive" should {
|
||||
"handle an exception strictly thrown in the inner route with the supplied exception handler" in {
|
||||
exceptionShouldBeHandled {
|
||||
handleExceptions(handler) { ctx ⇒
|
||||
throw MyException
|
||||
}
|
||||
}
|
||||
}
|
||||
"handle an Future.failed RouteResult with the supplied exception handler" in {
|
||||
exceptionShouldBeHandled {
|
||||
handleExceptions(handler) { ctx ⇒
|
||||
Future.failed(MyException)
|
||||
}
|
||||
}
|
||||
}
|
||||
"handle an eventually failed Future[RouteResult] with the supplied exception handler" in {
|
||||
exceptionShouldBeHandled {
|
||||
handleExceptions(handler) { ctx ⇒
|
||||
Future {
|
||||
Thread.sleep(100)
|
||||
throw MyException
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"handle an exception happening during route building" in {
|
||||
exceptionShouldBeHandled {
|
||||
get {
|
||||
handleExceptions(handler) {
|
||||
throw MyException
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"not interfere with alternative routes" in {
|
||||
Get("/abc") ~>
|
||||
get {
|
||||
handleExceptions(handler)(reject) ~ { ctx ⇒
|
||||
throw MyException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
"not handle other exceptions" in {
|
||||
Get("/abc") ~>
|
||||
get {
|
||||
handleExceptions(handler) {
|
||||
throw new RuntimeException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
"always fall back to a default content type" in {
|
||||
Get("/abc") ~> Accept(MediaTypes.`application/json`) ~>
|
||||
get {
|
||||
handleExceptions(handler) {
|
||||
throw new RuntimeException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
|
||||
Get("/abc") ~> Accept(MediaTypes.`text/xml`, MediaRanges.`*/*`.withQValue(0f)) ~>
|
||||
get {
|
||||
handleExceptions(handler) {
|
||||
throw new RuntimeException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def exceptionShouldBeHandled(route: Route) =
|
||||
Get("/abc") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "Pling! Plong! Something went wrong!!!"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import java.io.{ File, FileOutputStream }
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ ExecutionContext, Future }
|
||||
import scala.util.Properties
|
||||
import org.scalatest.matchers.Matcher
|
||||
import org.scalatest.{ Inside, Inspectors }
|
||||
import akka.http.scaladsl.model.MediaTypes._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.impl.util._
|
||||
|
||||
class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Inside {
|
||||
|
||||
override def testConfigSource =
|
||||
"""akka.http.scaladsl.routing {
|
||||
| file-chunking-threshold-size = 16
|
||||
| file-chunking-chunk-size = 8
|
||||
| range-coalescing-threshold = 1
|
||||
|}""".stripMargin
|
||||
|
||||
"getFromFile" should {
|
||||
"reject non-GET requests" in {
|
||||
Put() ~> getFromFile("some") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to non-existing files" in {
|
||||
Get() ~> getFromFile("nonExistentFile") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directories" in {
|
||||
Get() ~> getFromFile(Properties.javaHome) ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"return the file content with the MediaType matching the file extension" in {
|
||||
val file = File.createTempFile("akkaHttpTest", ".PDF")
|
||||
try {
|
||||
writeAllText("This is PDF", file)
|
||||
Get() ~> getFromFile(file.getPath) ~> check {
|
||||
mediaType shouldEqual `application/pdf`
|
||||
definedCharset shouldEqual None
|
||||
responseAs[String] shouldEqual "This is PDF"
|
||||
headers should contain(`Last-Modified`(DateTime(file.lastModified)))
|
||||
}
|
||||
} finally file.delete
|
||||
}
|
||||
"return the file content with MediaType 'application/octet-stream' on unknown file extensions" in {
|
||||
val file = File.createTempFile("akkaHttpTest", null)
|
||||
try {
|
||||
writeAllText("Some content", file)
|
||||
Get() ~> getFromFile(file) ~> check {
|
||||
mediaType shouldEqual `application/octet-stream`
|
||||
responseAs[String] shouldEqual "Some content"
|
||||
}
|
||||
} finally file.delete
|
||||
}
|
||||
|
||||
"return a single range from a file" in {
|
||||
val file = File.createTempFile("partialTest", null)
|
||||
try {
|
||||
writeAllText("ABCDEFGHIJKLMNOPQRSTUVWXYZ", file)
|
||||
Get() ~> addHeader(Range(ByteRange(0, 10))) ~> getFromFile(file) ~> check {
|
||||
status shouldEqual StatusCodes.PartialContent
|
||||
headers should contain(`Content-Range`(ContentRange(0, 10, 26)))
|
||||
responseAs[String] shouldEqual "ABCDEFGHIJK"
|
||||
}
|
||||
} finally file.delete
|
||||
}
|
||||
|
||||
"return multiple ranges from a file at once" in {
|
||||
pending // FIXME: reactivate
|
||||
val file = File.createTempFile("partialTest", null)
|
||||
try {
|
||||
writeAllText("ABCDEFGHIJKLMNOPQRSTUVWXYZ", file)
|
||||
val rangeHeader = Range(ByteRange(1, 10), ByteRange.suffix(10))
|
||||
Get() ~> addHeader(rangeHeader) ~> getFromFile(file, ContentTypes.`text/plain`) ~> check {
|
||||
status shouldEqual StatusCodes.PartialContent
|
||||
header[`Content-Range`] shouldEqual None
|
||||
mediaType.withParams(Map.empty) shouldEqual `multipart/byteranges`
|
||||
|
||||
val parts = responseAs[Multipart.ByteRanges].toStrict(1.second).awaitResult(3.seconds).strictParts
|
||||
parts.size shouldEqual 2
|
||||
parts(0).entity.data.utf8String shouldEqual "BCDEFGHIJK"
|
||||
parts(1).entity.data.utf8String shouldEqual "QRSTUVWXYZ"
|
||||
}
|
||||
} finally file.delete
|
||||
}
|
||||
}
|
||||
|
||||
"getFromResource" should {
|
||||
"reject non-GET requests" in {
|
||||
Put() ~> getFromResource("some") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to non-existing resources" in {
|
||||
Get() ~> getFromResource("nonExistingResource") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources" in {
|
||||
Get() ~> getFromResource("someDir") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources with trailing slash" in {
|
||||
Get() ~> getFromResource("someDir/") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources from an Archive " in {
|
||||
Get() ~> getFromResource("com/typesafe/config") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources from an Archive with trailing slash" in {
|
||||
Get() ~> getFromResource("com/typesafe/config/") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"return the resource content with the MediaType matching the file extension" in {
|
||||
val route = getFromResource("sample.html")
|
||||
|
||||
def runCheck() =
|
||||
Get() ~> route ~> check {
|
||||
mediaType shouldEqual `text/html`
|
||||
forAtLeast(1, headers) { h ⇒
|
||||
inside(h) {
|
||||
case `Last-Modified`(dt) ⇒
|
||||
DateTime(2011, 7, 1) should be < dt
|
||||
dt.clicks should be < System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
responseAs[String] shouldEqual "<p>Lorem ipsum!</p>"
|
||||
}
|
||||
|
||||
runCheck()
|
||||
runCheck() // additional test to check that no internal state is kept
|
||||
}
|
||||
"return the resource content from an Archive" in {
|
||||
Get() ~> getFromResource("com/typesafe/config/Config.class") ~> check {
|
||||
mediaType shouldEqual `application/octet-stream`
|
||||
responseEntity.toStrict(1.second).awaitResult(1.second).data.asByteBuffer.getInt shouldEqual 0xCAFEBABE
|
||||
}
|
||||
}
|
||||
"return the file content with MediaType 'application/octet-stream' on unknown file extensions" in {
|
||||
Get() ~> getFromResource("sample.xyz") ~> check {
|
||||
mediaType shouldEqual `application/octet-stream`
|
||||
responseAs[String] shouldEqual "XyZ"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"getFromResourceDirectory" should {
|
||||
"reject requests to non-existing resources" in {
|
||||
Get("not/found") ~> getFromResourceDirectory("subDirectory") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
val verify = check {
|
||||
mediaType shouldEqual `application/pdf`
|
||||
responseAs[String] shouldEqual "123"
|
||||
}
|
||||
"return the resource content with the MediaType matching the file extension - example 1" in {
|
||||
Get("empty.pdf") ~> getFromResourceDirectory("subDirectory") ~> verify
|
||||
}
|
||||
"return the resource content with the MediaType matching the file extension - example 2" in {
|
||||
Get("empty.pdf") ~> getFromResourceDirectory("subDirectory/") ~> verify
|
||||
}
|
||||
"return the resource content with the MediaType matching the file extension - example 3" in {
|
||||
Get("subDirectory/empty.pdf") ~> getFromResourceDirectory("") ~> verify
|
||||
}
|
||||
"return the resource content from an Archive" in {
|
||||
Get("Config.class") ~> getFromResourceDirectory("com/typesafe/config") ~> check {
|
||||
mediaType shouldEqual `application/octet-stream`
|
||||
responseEntity.toStrict(1.second).awaitResult(1.second).data.asByteBuffer.getInt shouldEqual 0xCAFEBABE
|
||||
}
|
||||
}
|
||||
"reject requests to directory resources" in {
|
||||
Get() ~> getFromResourceDirectory("subDirectory") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources with trailing slash" in {
|
||||
Get() ~> getFromResourceDirectory("subDirectory/") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to sub directory resources" in {
|
||||
Get("sub") ~> getFromResourceDirectory("someDir") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to sub directory resources with trailing slash" in {
|
||||
Get("sub/") ~> getFromResourceDirectory("someDir") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources from an Archive" in {
|
||||
Get() ~> getFromResourceDirectory("com/typesafe/config") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources from an Archive with trailing slash" in {
|
||||
Get() ~> getFromResourceDirectory("com/typesafe/config/") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
}
|
||||
|
||||
"listDirectoryContents" should {
|
||||
val base = new File(getClass.getClassLoader.getResource("").toURI).getPath
|
||||
new File(base, "subDirectory/emptySub").mkdir()
|
||||
def eraseDateTime(s: String) = s.replaceAll("""\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d""", "xxxx-xx-xx xx:xx:xx")
|
||||
implicit val settings = RoutingSettings.default.copy(renderVanityFooter = false)
|
||||
|
||||
"properly render a simple directory" in {
|
||||
Get() ~> listDirectoryContents(base + "/someDir") ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/sub/">sub/</a> xxxx-xx-xx xx:xx:xx
|
||||
|<a href="/fileA.txt">fileA.txt</a> xxxx-xx-xx xx:xx:xx 3 B
|
||||
|<a href="/fileB.xml">fileB.xml</a> xxxx-xx-xx xx:xx:xx 0 B
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render a sub directory" in {
|
||||
Get("/sub/") ~> listDirectoryContents(base + "/someDir") ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /sub/</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /sub/</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/">../</a>
|
||||
|<a href="/sub/file.html">file.html</a> xxxx-xx-xx xx:xx:xx 0 B
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render the union of several directories" in {
|
||||
Get() ~> listDirectoryContents(base + "/someDir", base + "/subDirectory") ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/emptySub/">emptySub/</a> xxxx-xx-xx xx:xx:xx
|
||||
|<a href="/sub/">sub/</a> xxxx-xx-xx xx:xx:xx
|
||||
|<a href="/empty.pdf">empty.pdf</a> xxxx-xx-xx xx:xx:xx 3 B
|
||||
|<a href="/fileA.txt">fileA.txt</a> xxxx-xx-xx xx:xx:xx 3 B
|
||||
|<a href="/fileB.xml">fileB.xml</a> xxxx-xx-xx xx:xx:xx 0 B
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render an empty sub directory with vanity footer" in {
|
||||
val settings = 0 // shadow implicit
|
||||
Get("/emptySub/") ~> listDirectoryContents(base + "/subDirectory") ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /emptySub/</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /emptySub/</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/">../</a>
|
||||
|</pre>
|
||||
|<hr>
|
||||
|<div style="width:100%;text-align:right;color:gray">
|
||||
|<small>rendered by <a href="http://akka.io">Akka Http</a> on xxxx-xx-xx xx:xx:xx</small>
|
||||
|</div>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render an empty top-level directory" in {
|
||||
Get() ~> listDirectoryContents(base + "/subDirectory/emptySub") ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|(no files)
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render a simple directory with a path prefix" in {
|
||||
Get("/files/") ~> pathPrefix("files")(listDirectoryContents(base + "/someDir")) ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /files/</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /files/</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/files/sub/">sub/</a> xxxx-xx-xx xx:xx:xx
|
||||
|<a href="/files/fileA.txt">fileA.txt</a> xxxx-xx-xx xx:xx:xx 3 B
|
||||
|<a href="/files/fileB.xml">fileB.xml</a> xxxx-xx-xx xx:xx:xx 0 B
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render a sub directory with a path prefix" in {
|
||||
Get("/files/sub/") ~> pathPrefix("files")(listDirectoryContents(base + "/someDir")) ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /files/sub/</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /files/sub/</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/files/">../</a>
|
||||
|<a href="/files/sub/file.html">file.html</a> xxxx-xx-xx xx:xx:xx 0 B
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render an empty top-level directory with a path prefix" in {
|
||||
Get("/files/") ~> pathPrefix("files")(listDirectoryContents(base + "/subDirectory/emptySub")) ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /files/</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /files/</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|(no files)
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"reject requests to file resources" in {
|
||||
Get() ~> listDirectoryContents(base + "subDirectory/empty.pdf") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
}
|
||||
|
||||
def prep(s: String) = s.stripMarginWithNewline("\n")
|
||||
|
||||
def writeAllText(text: String, file: File): Unit = {
|
||||
val fos = new FileOutputStream(file)
|
||||
try {
|
||||
fos.write(text.getBytes("UTF-8"))
|
||||
} finally fos.close()
|
||||
}
|
||||
|
||||
def evaluateTo[T](t: T, atMost: Duration = 100.millis)(implicit ec: ExecutionContext): Matcher[Future[T]] =
|
||||
be(t).compose[Future[T]] { fut ⇒
|
||||
fut.awaitResult(atMost)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.common.StrictForm
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshaller.HexInt
|
||||
import akka.http.scaladsl.model._
|
||||
import MediaTypes._
|
||||
|
||||
class FormFieldDirectivesSpec extends RoutingSpec {
|
||||
implicit val nodeSeqUnmarshaller =
|
||||
ScalaXmlSupport.nodeSeqUnmarshaller(`text/xml`, `text/html`, `text/plain`)
|
||||
|
||||
val nodeSeq: xml.NodeSeq = <b>yes</b>
|
||||
val urlEncodedForm = FormData(Map("firstName" -> "Mike", "age" -> "42"))
|
||||
val urlEncodedFormWithVip = FormData(Map("firstName" -> "Mike", "age" -> "42", "VIP" -> "true", "super" -> "<b>no</b>"))
|
||||
val multipartForm = Multipart.FormData {
|
||||
Map(
|
||||
"firstName" -> HttpEntity("Mike"),
|
||||
"age" -> HttpEntity(`text/xml`, "<int>42</int>"),
|
||||
"VIPBoolean" -> HttpEntity("true"))
|
||||
}
|
||||
val multipartFormWithTextHtml = Multipart.FormData {
|
||||
Map(
|
||||
"firstName" -> HttpEntity("Mike"),
|
||||
"age" -> HttpEntity(`text/xml`, "<int>42</int>"),
|
||||
"VIP" -> HttpEntity(`text/html`, "<b>yes</b>"),
|
||||
"super" -> HttpEntity("no"))
|
||||
}
|
||||
val multipartFormWithFile = Multipart.FormData(
|
||||
Multipart.FormData.BodyPart.Strict("file", HttpEntity(MediaTypes.`text/xml`, "<int>42</int>"),
|
||||
Map("filename" -> "age.xml")))
|
||||
|
||||
"The 'formFields' extraction directive" should {
|
||||
"properly extract the value of www-urlencoded form fields" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('firstName, "age".as[Int], 'sex?, "VIP" ? false) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "Mike42Nonefalse" }
|
||||
}
|
||||
"properly extract the value of www-urlencoded form fields when an explicit unmarshaller is given" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('firstName, "age".as(HexInt), 'sex?, "VIP" ? false) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "Mike66Nonefalse" }
|
||||
}
|
||||
"properly extract the value of multipart form fields" in {
|
||||
Post("/", multipartForm) ~> {
|
||||
formFields('firstName, "age", 'sex?, "VIP" ? nodeSeq) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "Mike<int>42</int>None<b>yes</b>" }
|
||||
}
|
||||
"extract StrictForm.FileData from a multipart part" in {
|
||||
Post("/", multipartFormWithFile) ~> {
|
||||
formFields('file.as[StrictForm.FileData]) {
|
||||
case StrictForm.FileData(name, HttpEntity.Strict(ct, data)) ⇒
|
||||
complete(s"type ${ct.mediaType} length ${data.length} filename ${name.get}")
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "type text/xml length 13 filename age.xml" }
|
||||
}
|
||||
"reject the request with a MissingFormFieldRejection if a required form field is missing" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('firstName, "age", 'sex, "VIP" ? false) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check { rejection shouldEqual MissingFormFieldRejection("sex") }
|
||||
}
|
||||
"properly extract the value if only a urlencoded deserializer is available for a multipart field that comes without a" +
|
||||
"Content-Type (or text/plain)" in {
|
||||
Post("/", multipartForm) ~> {
|
||||
formFields('firstName, "age", 'sex?, "VIPBoolean" ? false) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check {
|
||||
responseAs[String] shouldEqual "Mike<int>42</int>Nonetrue"
|
||||
}
|
||||
}
|
||||
"work even if only a FromStringUnmarshaller is available for a multipart field with custom Content-Type" in {
|
||||
Post("/", multipartFormWithTextHtml) ~> {
|
||||
formFields(('firstName, "age", 'super ? false)) { (firstName, age, vip) ⇒
|
||||
complete(firstName + age + vip)
|
||||
}
|
||||
} ~> check {
|
||||
responseAs[String] shouldEqual "Mike<int>42</int>false"
|
||||
}
|
||||
}
|
||||
"work even if only a FromEntityUnmarshaller is available for a www-urlencoded field" in {
|
||||
Post("/", urlEncodedFormWithVip) ~> {
|
||||
formFields('firstName, "age", 'sex?, "super" ? nodeSeq) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check {
|
||||
responseAs[String] shouldEqual "Mike42None<b>no</b>"
|
||||
}
|
||||
}
|
||||
}
|
||||
"The 'formField' requirement directive" should {
|
||||
"block requests that do not contain the required formField" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('name ! "Mr. Mike") { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"block requests that contain the required parameter but with an unmatching value" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('firstName ! "Pete") { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"let requests pass that contain the required parameter with its required value" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('firstName ! "Mike") { completeOk }
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"The 'formField' requirement with explicit unmarshaller directive" should {
|
||||
"block requests that do not contain the required formField" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('oldAge.as(HexInt) ! 78) { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"block requests that contain the required parameter but with an unmatching value" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('age.as(HexInt) ! 78) { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"let requests pass that contain the required parameter with its required value" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('age.as(HexInt) ! 66 /* hex! */ ) { completeOk }
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class FutureDirectivesSpec extends RoutingSpec {
|
||||
|
||||
class TestException(msg: String) extends Exception(msg)
|
||||
object TestException extends Exception("XXX")
|
||||
def throwTestException[T](msgPrefix: String): T ⇒ Nothing = t ⇒ throw new TestException(msgPrefix + t)
|
||||
|
||||
implicit val exceptionHandler = ExceptionHandler {
|
||||
case e: TestException ⇒ complete(StatusCodes.InternalServerError, "Oops. " + e)
|
||||
}
|
||||
|
||||
"The `onComplete` directive" should {
|
||||
"unwrap a Future in the success case" in {
|
||||
var i = 0
|
||||
def nextNumber() = { i += 1; i }
|
||||
val route = onComplete(Future.successful(nextNumber())) { echoComplete }
|
||||
Get() ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Success(1)"
|
||||
}
|
||||
Get() ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Success(2)"
|
||||
}
|
||||
}
|
||||
"unwrap a Future in the failure case" in {
|
||||
Get() ~> onComplete(Future.failed[String](new RuntimeException("no"))) { echoComplete } ~> check {
|
||||
responseAs[String] shouldEqual "Failure(java.lang.RuntimeException: no)"
|
||||
}
|
||||
}
|
||||
"catch an exception in the success case" in {
|
||||
Get() ~> onComplete(Future.successful("ok")) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "Oops. akka.http.scaladsl.server.directives.FutureDirectivesSpec$TestException: EX when Success(ok)"
|
||||
}
|
||||
}
|
||||
"catch an exception in the failure case" in {
|
||||
Get() ~> onComplete(Future.failed[String](new RuntimeException("no"))) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "Oops. akka.http.scaladsl.server.directives.FutureDirectivesSpec$TestException: EX when Failure(java.lang.RuntimeException: no)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The `onSuccess` directive" should {
|
||||
"unwrap a Future in the success case" in {
|
||||
Get() ~> onSuccess(Future.successful("yes")) { echoComplete } ~> check {
|
||||
responseAs[String] shouldEqual "yes"
|
||||
}
|
||||
}
|
||||
"propagate the exception in the failure case" in {
|
||||
Get() ~> onSuccess(Future.failed(TestException)) { echoComplete } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
}
|
||||
}
|
||||
"catch an exception in the success case" in {
|
||||
Get() ~> onSuccess(Future.successful("ok")) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "Oops. akka.http.scaladsl.server.directives.FutureDirectivesSpec$TestException: EX when ok"
|
||||
}
|
||||
}
|
||||
"catch an exception in the failure case" in {
|
||||
Get() ~> onSuccess(Future.failed(TestException)) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The `completeOrRecoverWith` directive" should {
|
||||
"complete the request with the Future's value if the future succeeds" in {
|
||||
Get() ~> completeOrRecoverWith(Future.successful("yes")) { echoComplete } ~> check {
|
||||
responseAs[String] shouldEqual "yes"
|
||||
}
|
||||
}
|
||||
"don't call the inner route if the Future succeeds" in {
|
||||
Get() ~> completeOrRecoverWith(Future.successful("ok")) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "ok"
|
||||
}
|
||||
}
|
||||
"recover using the inner route if the Future fails" in {
|
||||
val route = completeOrRecoverWith(Future.failed[String](TestException)) {
|
||||
case e ⇒ complete(s"Exception occurred: ${e.getMessage}")
|
||||
}
|
||||
|
||||
Get() ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Exception occurred: XXX"
|
||||
}
|
||||
}
|
||||
"catch an exception during recovery" in {
|
||||
Get() ~> completeOrRecoverWith(Future.failed[String](TestException)) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "Oops. akka.http.scaladsl.server.directives.FutureDirectivesSpec$TestException: EX when akka.http.scaladsl.server.directives.FutureDirectivesSpec$TestException$: XXX"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
import akka.http.scaladsl.server._
|
||||
import org.scalatest.Inside
|
||||
|
||||
class HeaderDirectivesSpec extends RoutingSpec with Inside {
|
||||
|
||||
"The headerValuePF directive" should {
|
||||
lazy val myHeaderValue = headerValuePF { case Connection(tokens) ⇒ tokens.head }
|
||||
|
||||
"extract the respective header value if a matching request header is present" in {
|
||||
Get("/abc") ~> addHeader(Connection("close")) ~> myHeaderValue { echoComplete } ~> check {
|
||||
responseAs[String] shouldEqual "close"
|
||||
}
|
||||
}
|
||||
|
||||
"reject with an empty rejection set if no matching request header is present" in {
|
||||
Get("/abc") ~> myHeaderValue { echoComplete } ~> check { rejections shouldEqual Nil }
|
||||
}
|
||||
|
||||
"reject with a MalformedHeaderRejection if the extract function throws an exception" in {
|
||||
Get("/abc") ~> addHeader(Connection("close")) ~> {
|
||||
(headerValuePF { case _ ⇒ sys.error("Naah!") }) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) { case MalformedHeaderRejection("Connection", "Naah!", _) ⇒ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The headerValueByType directive" should {
|
||||
lazy val route =
|
||||
headerValueByType[Origin]() { origin ⇒
|
||||
complete(s"The first origin was ${origin.origins.head}")
|
||||
}
|
||||
"extract a header if the type is matching" in {
|
||||
val originHeader = Origin(HttpOrigin("http://localhost:8080"))
|
||||
Get("abc") ~> originHeader ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The first origin was http://localhost:8080"
|
||||
}
|
||||
}
|
||||
"reject a request if no header of the given type is present" in {
|
||||
Get("abc") ~> route ~> check {
|
||||
inside(rejection) {
|
||||
case MissingHeaderRejection("Origin") ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The optionalHeaderValue directive" should {
|
||||
lazy val myHeaderValue = optionalHeaderValue {
|
||||
case Connection(tokens) ⇒ Some(tokens.head)
|
||||
case _ ⇒ None
|
||||
}
|
||||
|
||||
"extract the respective header value if a matching request header is present" in {
|
||||
Get("/abc") ~> addHeader(Connection("close")) ~> myHeaderValue { echoComplete } ~> check {
|
||||
responseAs[String] shouldEqual "Some(close)"
|
||||
}
|
||||
}
|
||||
|
||||
"extract None if no matching request header is present" in {
|
||||
Get("/abc") ~> myHeaderValue { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
|
||||
"reject with a MalformedHeaderRejection if the extract function throws an exception" in {
|
||||
Get("/abc") ~> addHeader(Connection("close")) ~> {
|
||||
val myHeaderValue = optionalHeaderValue { case _ ⇒ sys.error("Naaah!") }
|
||||
myHeaderValue { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) { case MalformedHeaderRejection("Connection", "Naaah!", _) ⇒ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The optionalHeaderValueByType directive" should {
|
||||
val route =
|
||||
optionalHeaderValueByType[Origin]() {
|
||||
case Some(origin) ⇒ complete(s"The first origin was ${origin.origins.head}")
|
||||
case None ⇒ complete("No Origin header found.")
|
||||
}
|
||||
"extract Some(header) if the type is matching" in {
|
||||
val originHeader = Origin(HttpOrigin("http://localhost:8080"))
|
||||
Get("abc") ~> originHeader ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The first origin was http://localhost:8080"
|
||||
}
|
||||
}
|
||||
"extract None if no header of the given type is present" in {
|
||||
Get("abc") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "No Origin header found."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.headers.Host
|
||||
import org.scalatest.FreeSpec
|
||||
|
||||
class HostDirectivesSpec extends FreeSpec with GenericRoutingSpec {
|
||||
"The 'host' directive" - {
|
||||
"in its simple String form should" - {
|
||||
"block requests to unmatched hosts" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("spray.com") { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
|
||||
"let requests to matching hosts pass" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("spray.com", "spray.io") { completeOk }
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"in its simple RegEx form" - {
|
||||
"block requests to unmatched hosts" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("hairspray.*".r) { echoComplete }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
|
||||
"let requests to matching hosts pass and extract the full host" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("spra.*".r) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "spray.io" }
|
||||
}
|
||||
}
|
||||
|
||||
"in its group RegEx form" - {
|
||||
"block requests to unmatched hosts" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("hairspray(.*)".r) { echoComplete }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
|
||||
"let requests to matching hosts pass and extract the full host" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("spra(.*)".r) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "y.io" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.xml.NodeSeq
|
||||
import org.scalatest.Inside
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport
|
||||
import akka.http.scaladsl.unmarshalling._
|
||||
import akka.http.scaladsl.marshalling._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
|
||||
import MediaTypes._
|
||||
import HttpCharsets._
|
||||
import headers._
|
||||
import spray.json.DefaultJsonProtocol._
|
||||
|
||||
class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
||||
import ScalaXmlSupport._
|
||||
|
||||
private val iso88592 = HttpCharsets.getForKey("iso-8859-2").get
|
||||
implicit val IntUnmarshaller: FromEntityUnmarshaller[Int] =
|
||||
nodeSeqUnmarshaller(ContentTypeRange(`text/xml`, iso88592), `text/html`) map {
|
||||
case NodeSeq.Empty ⇒ throw Unmarshaller.NoContentException
|
||||
case x ⇒ { val i = x.text.toInt; require(i >= 0); i }
|
||||
}
|
||||
|
||||
implicit val IntMarshaller: ToEntityMarshaller[Int] =
|
||||
Marshaller.oneOf(ContentType(`application/xhtml+xml`), ContentType(`text/xml`, `UTF-8`)) { contentType ⇒
|
||||
nodeSeqMarshaller(contentType).wrap(contentType) { (i: Int) ⇒ <int>{ i }</int> }
|
||||
}
|
||||
|
||||
"The 'entityAs' directive" should {
|
||||
"extract an object from the requests entity using the in-scope Unmarshaller" in {
|
||||
Put("/", <p>cool</p>) ~> {
|
||||
entity(as[NodeSeq]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "<p>cool</p>" }
|
||||
}
|
||||
"return a RequestEntityExpectedRejection rejection if the request has no entity" in {
|
||||
Put() ~> {
|
||||
entity(as[Int]) { echoComplete }
|
||||
} ~> check { rejection shouldEqual RequestEntityExpectedRejection }
|
||||
}
|
||||
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope" in {
|
||||
Put("/", HttpEntity(`text/css`, "<p>cool</p>")) ~> {
|
||||
entity(as[NodeSeq]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`))
|
||||
}
|
||||
Put("/", HttpEntity(ContentType(`text/xml`, `UTF-16`), "<int>26</int>")) ~> {
|
||||
entity(as[Int]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`))
|
||||
}
|
||||
}
|
||||
"cancel UnsupportedRequestContentTypeRejections if a subsequent `entity` directive succeeds" in {
|
||||
Put("/", HttpEntity(`text/plain`, "yeah")) ~> {
|
||||
entity(as[NodeSeq]) { _ ⇒ completeOk } ~
|
||||
entity(as[String]) { _ ⇒ validate(false, "Problem") { completeOk } }
|
||||
} ~> check { rejection shouldEqual ValidationRejection("Problem") }
|
||||
}
|
||||
"return a ValidationRejection if the request entity is semantically invalid (IllegalArgumentException)" in {
|
||||
Put("/", HttpEntity(ContentType(`text/xml`, iso88592), "<int>-3</int>")) ~> {
|
||||
entity(as[Int]) { _ ⇒ completeOk }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case ValidationRejection("requirement failed", Some(_: IllegalArgumentException)) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
"return a MalformedRequestContentRejection if unmarshalling failed due to a not further classified error" in {
|
||||
Put("/", HttpEntity(`text/xml`, "<foo attr='illegal xml'")) ~> {
|
||||
entity(as[NodeSeq]) { _ ⇒ completeOk }
|
||||
} ~> check {
|
||||
rejection shouldEqual MalformedRequestContentRejection(
|
||||
"XML document structures must start and end within the same entity.", None)
|
||||
}
|
||||
}
|
||||
"extract an Option[T] from the requests entity using the in-scope Unmarshaller" in {
|
||||
Put("/", <p>cool</p>) ~> {
|
||||
entity(as[Option[NodeSeq]]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Some(<p>cool</p>)" }
|
||||
}
|
||||
"extract an Option[T] as None if the request has no entity" in {
|
||||
Put() ~> {
|
||||
entity(as[Option[Int]]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope (for Option[T]s)" in {
|
||||
Put("/", HttpEntity(`text/css`, "<p>cool</p>")) ~> {
|
||||
entity(as[Option[NodeSeq]]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`))
|
||||
}
|
||||
}
|
||||
"properly extract with a super-unmarshaller" in {
|
||||
case class Person(name: String)
|
||||
val jsonUnmarshaller: FromEntityUnmarshaller[Person] = jsonFormat1(Person)
|
||||
val xmlUnmarshaller: FromEntityUnmarshaller[Person] =
|
||||
ScalaXmlSupport.nodeSeqUnmarshaller(`text/xml`).map(seq ⇒ Person(seq.text))
|
||||
|
||||
implicit val unmarshaller = Unmarshaller.firstOf(jsonUnmarshaller, xmlUnmarshaller)
|
||||
|
||||
val route = entity(as[Person]) { echoComplete }
|
||||
|
||||
Put("/", HttpEntity(`text/xml`, "<name>Peter Xml</name>")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Person(Peter Xml)"
|
||||
}
|
||||
Put("/", HttpEntity(`application/json`, """{ "name": "Paul Json" }""")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Person(Paul Json)"
|
||||
}
|
||||
Put("/", HttpEntity(`text/plain`, """name = Sir Text }""")) ~> route ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`application/json`, `text/xml`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'completeWith' directive" should {
|
||||
"provide a completion function converting custom objects to an HttpEntity using the in-scope marshaller" in {
|
||||
Get() ~> completeWith(instanceOf[Int]) { prod ⇒ prod(42) } ~> check {
|
||||
responseEntity shouldEqual HttpEntity(ContentType(`application/xhtml+xml`, `UTF-8`), "<int>42</int>")
|
||||
}
|
||||
}
|
||||
"return a UnacceptedResponseContentTypeRejection rejection if no acceptable marshaller is in scope" in {
|
||||
Get() ~> Accept(`text/css`) ~> completeWith(instanceOf[Int]) { prod ⇒ prod(42) } ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`)))
|
||||
}
|
||||
}
|
||||
"convert the response content to an accepted charset" in {
|
||||
Get() ~> `Accept-Charset`(`UTF-8`) ~> completeWith(instanceOf[String]) { prod ⇒ prod("Hällö") } ~> check {
|
||||
responseEntity shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), "Hällö")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'handleWith' directive" should {
|
||||
def times2(x: Int) = x * 2
|
||||
|
||||
"support proper round-trip content unmarshalling/marshalling to and from a function" in (
|
||||
Put("/", HttpEntity(`text/html`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
|
||||
~> check { responseEntity shouldEqual HttpEntity(ContentType(`text/xml`, `UTF-8`), "<int>84</int>") })
|
||||
|
||||
"result in UnsupportedRequestContentTypeRejection rejection if there is no unmarshaller supporting the requests charset" in (
|
||||
Put("/", HttpEntity(`text/xml`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
|
||||
~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`))
|
||||
})
|
||||
|
||||
"result in an UnacceptedResponseContentTypeRejection rejection if there is no marshaller supporting the requests Accept-Charset header" in (
|
||||
Put("/", HttpEntity(`text/html`, "<int>42</int>")) ~> addHeaders(Accept(`text/xml`), `Accept-Charset`(`UTF-16`)) ~>
|
||||
handleWith(times2) ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`)))
|
||||
})
|
||||
}
|
||||
|
||||
"The marshalling infrastructure for JSON" should {
|
||||
import spray.json._
|
||||
case class Foo(name: String)
|
||||
implicit val fooFormat = jsonFormat1(Foo)
|
||||
val foo = Foo("Hällö")
|
||||
|
||||
"render JSON with UTF-8 encoding if no `Accept-Charset` request header is present" in {
|
||||
Get() ~> complete(foo) ~> check {
|
||||
responseEntity shouldEqual HttpEntity(ContentType(`application/json`, `UTF-8`), foo.toJson.prettyPrint)
|
||||
}
|
||||
}
|
||||
"reject JSON rendering if an `Accept-Charset` request header requests a non-UTF-8 encoding" in {
|
||||
Get() ~> `Accept-Charset`(`ISO-8859-1`) ~> complete(foo) ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(ContentType(`application/json`)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model.{ StatusCodes, HttpMethods }
|
||||
import akka.http.scaladsl.server._
|
||||
|
||||
class MethodDirectivesSpec extends RoutingSpec {
|
||||
|
||||
"get | put" should {
|
||||
lazy val getOrPut = (get | put) { completeOk }
|
||||
|
||||
"block POST requests" in {
|
||||
Post() ~> getOrPut ~> check { handled shouldEqual false }
|
||||
}
|
||||
"let GET requests pass" in {
|
||||
Get() ~> getOrPut ~> check { response shouldEqual Ok }
|
||||
}
|
||||
"let PUT requests pass" in {
|
||||
Put() ~> getOrPut ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"two failed `get` directives" should {
|
||||
"only result in a single Rejection" in {
|
||||
Put() ~> {
|
||||
get { completeOk } ~
|
||||
get { completeOk }
|
||||
} ~> check {
|
||||
rejections shouldEqual List(MethodRejection(HttpMethods.GET))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"overrideMethodWithParameter" should {
|
||||
"change the request method" in {
|
||||
Get("/?_method=put") ~> overrideMethodWithParameter("_method") {
|
||||
get { complete("GET") } ~
|
||||
put { complete("PUT") }
|
||||
} ~> check { responseAs[String] shouldEqual "PUT" }
|
||||
}
|
||||
"not affect the request when not specified" in {
|
||||
Get() ~> overrideMethodWithParameter("_method") {
|
||||
get { complete("GET") } ~
|
||||
put { complete("PUT") }
|
||||
} ~> check { responseAs[String] shouldEqual "GET" }
|
||||
}
|
||||
"complete with 501 Not Implemented when not a valid method" in {
|
||||
Get("/?_method=hallo") ~> overrideMethodWithParameter("_method") {
|
||||
get { complete("GET") } ~
|
||||
put { complete("PUT") }
|
||||
} ~> check { status shouldEqual StatusCodes.NotImplemented }
|
||||
}
|
||||
}
|
||||
|
||||
"MethodRejections under a successful match" should {
|
||||
"be cancelled if the match happens after the rejection" in {
|
||||
Put() ~> {
|
||||
get { completeOk } ~
|
||||
put { reject(RequestEntityExpectedRejection) }
|
||||
} ~> check {
|
||||
rejections shouldEqual List(RequestEntityExpectedRejection)
|
||||
}
|
||||
}
|
||||
"be cancelled if the match happens after the rejection (example 2)" in {
|
||||
Put() ~> {
|
||||
(get & complete(Ok)) ~ (put & reject(RequestEntityExpectedRejection))
|
||||
} ~> check {
|
||||
rejections shouldEqual List(RequestEntityExpectedRejection)
|
||||
}
|
||||
}
|
||||
"be cancelled if the match happens before the rejection" in {
|
||||
Put() ~> {
|
||||
put { reject(RequestEntityExpectedRejection) } ~ get { completeOk }
|
||||
} ~> check {
|
||||
rejections shouldEqual List(RequestEntityExpectedRejection)
|
||||
}
|
||||
}
|
||||
"be cancelled if the match happens before the rejection (example 2)" in {
|
||||
Put() ~> {
|
||||
(put & reject(RequestEntityExpectedRejection)) ~ (get & complete(Ok))
|
||||
} ~> check {
|
||||
rejections shouldEqual List(RequestEntityExpectedRejection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
import HttpMethods._
|
||||
import MediaTypes._
|
||||
import Uri._
|
||||
|
||||
class MiscDirectivesSpec extends RoutingSpec {
|
||||
|
||||
"the extractClientIP directive" should {
|
||||
"extract from a X-Forwarded-For header" in {
|
||||
Get() ~> addHeaders(`X-Forwarded-For`("2.3.4.5"), RawHeader("x-real-ip", "1.2.3.4")) ~> {
|
||||
extractClientIP { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "2.3.4.5" }
|
||||
}
|
||||
"extract from a Remote-Address header" in {
|
||||
Get() ~> addHeaders(RawHeader("x-real-ip", "1.2.3.4"), `Remote-Address`(RemoteAddress("5.6.7.8"))) ~> {
|
||||
extractClientIP { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "5.6.7.8" }
|
||||
}
|
||||
"extract from a X-Real-IP header" in {
|
||||
Get() ~> addHeader(RawHeader("x-real-ip", "1.2.3.4")) ~> {
|
||||
extractClientIP { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "1.2.3.4" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import org.scalatest.{ FreeSpec, Inside }
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshaller.HexInt
|
||||
|
||||
class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Inside {
|
||||
|
||||
"when used with 'as[Int]' the parameter directive should" - {
|
||||
"extract a parameter value as Int" in {
|
||||
Get("/?amount=123") ~> {
|
||||
parameter('amount.as[Int]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "123" }
|
||||
}
|
||||
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
||||
Get("/?amount=1x3") ~> {
|
||||
parameter('amount.as[Int]) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case MalformedQueryParamRejection("amount", "'1x3' is not a valid 32-bit signed integer value", Some(_)) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
"supply typed default values" in {
|
||||
Get() ~> {
|
||||
parameter('amount ? 45) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "45" }
|
||||
}
|
||||
"create typed optional parameters that" - {
|
||||
"extract Some(value) when present" in {
|
||||
Get("/?amount=12") ~> {
|
||||
parameter("amount".as[Int]?) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Some(12)" }
|
||||
}
|
||||
"extract None when not present" in {
|
||||
Get() ~> {
|
||||
parameter("amount".as[Int]?) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
||||
Get("/?amount=x") ~> {
|
||||
parameter("amount".as[Int]?) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case MalformedQueryParamRejection("amount", "'x' is not a valid 32-bit signed integer value", Some(_)) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"when used with 'as(HexInt)' the parameter directive should" - {
|
||||
"extract parameter values as Int" in {
|
||||
Get("/?amount=1f") ~> {
|
||||
parameter('amount.as(HexInt)) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "31" }
|
||||
}
|
||||
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
||||
Get("/?amount=1x3") ~> {
|
||||
parameter('amount.as(HexInt)) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case MalformedQueryParamRejection("amount", "'1x3' is not a valid 32-bit hexadecimal integer value", Some(_)) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
"supply typed default values" in {
|
||||
Get() ~> {
|
||||
parameter('amount.as(HexInt) ? 45) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "45" }
|
||||
}
|
||||
"create typed optional parameters that" - {
|
||||
"extract Some(value) when present" in {
|
||||
Get("/?amount=A") ~> {
|
||||
parameter("amount".as(HexInt)?) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Some(10)" }
|
||||
}
|
||||
"extract None when not present" in {
|
||||
Get() ~> {
|
||||
parameter("amount".as(HexInt)?) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
||||
Get("/?amount=x") ~> {
|
||||
parameter("amount".as(HexInt)?) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case MalformedQueryParamRejection("amount", "'x' is not a valid 32-bit hexadecimal integer value", Some(_)) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"when used with 'as[Boolean]' the parameter directive should" - {
|
||||
"extract parameter values as Boolean" in {
|
||||
Get("/?really=true") ~> {
|
||||
parameter('really.as[Boolean]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "true" }
|
||||
Get("/?really=no") ~> {
|
||||
parameter('really.as[Boolean]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "false" }
|
||||
}
|
||||
"extract optional parameter values as Boolean" in {
|
||||
Get() ~> {
|
||||
parameter('really.as[Boolean] ? false) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "false" }
|
||||
}
|
||||
"cause a MalformedQueryParamRejection on illegal Boolean values" in {
|
||||
Get("/?really=absolutely") ~> {
|
||||
parameter('really.as[Boolean]) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case MalformedQueryParamRejection("really", "'absolutely' is not a valid Boolean value", None) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'parameters' extraction directive should" - {
|
||||
"extract the value of given parameters" in {
|
||||
Get("/?name=Parsons&FirstName=Ellen") ~> {
|
||||
parameters("name", 'FirstName) { (name, firstName) ⇒
|
||||
complete(firstName + name)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "EllenParsons" }
|
||||
}
|
||||
"correctly extract an optional parameter" in {
|
||||
Get("/?foo=bar") ~> parameters('foo ?) { echoComplete } ~> check { responseAs[String] shouldEqual "Some(bar)" }
|
||||
Get("/?foo=bar") ~> parameters('baz ?) { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"ignore additional parameters" in {
|
||||
Get("/?name=Parsons&FirstName=Ellen&age=29") ~> {
|
||||
parameters("name", 'FirstName) { (name, firstName) ⇒
|
||||
complete(firstName + name)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "EllenParsons" }
|
||||
}
|
||||
"reject the request with a MissingQueryParamRejection if a required parameter is missing" in {
|
||||
Get("/?name=Parsons&sex=female") ~> {
|
||||
parameters('name, 'FirstName, 'age) { (name, firstName, age) ⇒
|
||||
completeOk
|
||||
}
|
||||
} ~> check { rejection shouldEqual MissingQueryParamRejection("FirstName") }
|
||||
}
|
||||
"supply the default value if an optional parameter is missing" in {
|
||||
Get("/?name=Parsons&FirstName=Ellen") ~> {
|
||||
parameters("name"?, 'FirstName, 'age ? "29", 'eyes?) { (name, firstName, age, eyes) ⇒
|
||||
complete(firstName + name + age + eyes)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "EllenSome(Parsons)29None" }
|
||||
}
|
||||
}
|
||||
|
||||
"The 'parameter' requirement directive should" - {
|
||||
"block requests that do not contain the required parameter" in {
|
||||
Get("/person?age=19") ~> {
|
||||
parameter('nose ! "large") { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"block requests that contain the required parameter but with an unmatching value" in {
|
||||
Get("/person?age=19&nose=small") ~> {
|
||||
parameter('nose ! "large") { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"let requests pass that contain the required parameter with its required value" in {
|
||||
Get("/person?nose=large&eyes=blue") ~> {
|
||||
parameter('nose ! "large") { completeOk }
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
"be useable for method tunneling" in {
|
||||
val route = {
|
||||
(post | parameter('method ! "post")) { complete("POST") } ~
|
||||
get { complete("GET") }
|
||||
}
|
||||
Get("/?method=post") ~> route ~> check { responseAs[String] shouldEqual "POST" }
|
||||
Post() ~> route ~> check { responseAs[String] shouldEqual "POST" }
|
||||
Get() ~> route ~> check { responseAs[String] shouldEqual "GET" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.server._
|
||||
import org.scalatest.Inside
|
||||
|
||||
class PathDirectivesSpec extends RoutingSpec with Inside {
|
||||
val echoUnmatchedPath = extractUnmatchedPath { echoComplete }
|
||||
def echoCaptureAndUnmatchedPath[T]: T ⇒ Route =
|
||||
capture ⇒ ctx ⇒ ctx.complete(capture.toString + ":" + ctx.unmatchedPath)
|
||||
|
||||
"""path("foo")""" should {
|
||||
val test = testFor(path("foo") { echoUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"reject [/foobar]" in test()
|
||||
"reject [/foo/bar]" in test()
|
||||
"accept [/foo] and clear the unmatchedPath" in test("")
|
||||
"reject [/foo/]" in test()
|
||||
}
|
||||
|
||||
"""path("foo" /)""" should {
|
||||
val test = testFor(path("foo" /) { echoUnmatchedPath })
|
||||
"reject [/foo]" in test()
|
||||
"accept [/foo/] and clear the unmatchedPath" in test("")
|
||||
}
|
||||
|
||||
"""path("")""" should {
|
||||
val test = testFor(path("") { echoUnmatchedPath })
|
||||
"reject [/foo]" in test()
|
||||
"accept [/] and clear the unmatchedPath" in test("")
|
||||
}
|
||||
|
||||
"""pathPrefix("foo")""" should {
|
||||
val test = testFor(pathPrefix("foo") { echoUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"accept [/foobar]" in test("bar")
|
||||
"accept [/foo/bar]" in test("/bar")
|
||||
"accept [/foo] and clear the unmatchedPath" in test("")
|
||||
"accept [/foo/] and clear the unmatchedPath" in test("/")
|
||||
}
|
||||
|
||||
"""pathPrefix("foo" / "bar")""" should {
|
||||
val test = testFor(pathPrefix("foo" / "bar") { echoUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"accept [/foo/bar]" in test("")
|
||||
"accept [/foo/bar/baz]" in test("/baz")
|
||||
}
|
||||
|
||||
"""pathPrefix("ab[cd]+".r)""" should {
|
||||
val test = testFor(pathPrefix("ab[cd]+".r) { echoCaptureAndUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"reject [/ab/cd]" in test()
|
||||
"reject [/abcdef]" in test("abcd:ef")
|
||||
"reject [/abcdd/ef]" in test("abcdd:/ef")
|
||||
}
|
||||
|
||||
"""pathPrefix("ab(cd)".r)""" should {
|
||||
val test = testFor(pathPrefix("ab(cd)+".r) { echoCaptureAndUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"reject [/ab/cd]" in test()
|
||||
"reject [/abcdef]" in test("cd:ef")
|
||||
"reject [/abcde/fg]" in test("cd:e/fg")
|
||||
}
|
||||
|
||||
"pathPrefix(regex)" should {
|
||||
"fail when the regex contains more than one group" in {
|
||||
an[IllegalArgumentException] must be thrownBy path("a(b+)(c+)".r) { echoCaptureAndUnmatchedPath }
|
||||
}
|
||||
}
|
||||
|
||||
"pathPrefix(IntNumber)" should {
|
||||
val test = testFor(pathPrefix(IntNumber) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/23]" in test("23:")
|
||||
"accept [/12345yes]" in test("12345:yes")
|
||||
"reject [/]" in test()
|
||||
"reject [/abc]" in test()
|
||||
"reject [/2147483648]" in test() // > Int.MaxValue
|
||||
}
|
||||
|
||||
"pathPrefix(CustomShortNumber)" should {
|
||||
object CustomShortNumber extends NumberMatcher[Short](Short.MaxValue, 10) {
|
||||
def fromChar(c: Char) = fromDecimalChar(c)
|
||||
}
|
||||
|
||||
val test = testFor(pathPrefix(CustomShortNumber) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/23]" in test("23:")
|
||||
"accept [/12345yes]" in test("12345:yes")
|
||||
"reject [/]" in test()
|
||||
"reject [/abc]" in test()
|
||||
"reject [/33000]" in test() // > Short.MaxValue
|
||||
}
|
||||
|
||||
"pathPrefix(JavaUUID)" should {
|
||||
val test = testFor(pathPrefix(JavaUUID) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/bdea8652-f26c-40ca-8157-0b96a2a8389d]" in test("bdea8652-f26c-40ca-8157-0b96a2a8389d:")
|
||||
"accept [/bdea8652-f26c-40ca-8157-0b96a2a8389dyes]" in test("bdea8652-f26c-40ca-8157-0b96a2a8389d:yes")
|
||||
"reject [/]" in test()
|
||||
"reject [/abc]" in test()
|
||||
}
|
||||
|
||||
"pathPrefix(Map(\"red\" -> 1, \"green\" -> 2, \"blue\" -> 3))" should {
|
||||
val test = testFor(pathPrefix(Map("red" -> 1, "green" -> 2, "blue" -> 3)) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/green]" in test("2:")
|
||||
"accept [/redsea]" in test("1:sea")
|
||||
"reject [/black]" in test()
|
||||
}
|
||||
|
||||
"pathPrefix(Map.empty)" should {
|
||||
val test = testFor(pathPrefix(Map[String, Int]()) { echoCaptureAndUnmatchedPath })
|
||||
"reject [/black]" in test()
|
||||
}
|
||||
|
||||
"pathPrefix(Segment)" should {
|
||||
val test = testFor(pathPrefix(Segment) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/abc]" in test("abc:")
|
||||
"accept [/abc/]" in test("abc:/")
|
||||
"accept [/abc/def]" in test("abc:/def")
|
||||
"reject [/]" in test()
|
||||
}
|
||||
|
||||
"pathPrefix(Segments)" should {
|
||||
val test = testFor(pathPrefix(Segments) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/]" in test("List():")
|
||||
"accept [/a/b/c]" in test("List(a, b, c):")
|
||||
"accept [/a/b/c/]" in test("List(a, b, c):/")
|
||||
}
|
||||
|
||||
"""pathPrefix(separateOnSlashes("a/b"))""" should {
|
||||
val test = testFor(pathPrefix(separateOnSlashes("a/b")) { echoUnmatchedPath })
|
||||
"accept [/a/b]" in test("")
|
||||
"accept [/a/b/]" in test("/")
|
||||
"accept [/a/c]" in test()
|
||||
}
|
||||
"""pathPrefix(separateOnSlashes("abc"))""" should {
|
||||
val test = testFor(pathPrefix(separateOnSlashes("abc")) { echoUnmatchedPath })
|
||||
"accept [/abc]" in test("")
|
||||
"accept [/abcdef]" in test("def")
|
||||
"accept [/ab]" in test()
|
||||
}
|
||||
|
||||
"""pathPrefixTest("a" / Segment ~ Slash)""" should {
|
||||
val test = testFor(pathPrefixTest("a" / Segment ~ Slash) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/a/bc/]" in test("bc:/a/bc/")
|
||||
"accept [/a/bc]" in test()
|
||||
"accept [/a/]" in test()
|
||||
}
|
||||
|
||||
"""pathSuffix("edit" / Segment)""" should {
|
||||
val test = testFor(pathSuffix("edit" / Segment) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/orders/123/edit]" in test("123:/orders/")
|
||||
"accept [/orders/123/ed]" in test()
|
||||
"accept [/edit]" in test()
|
||||
}
|
||||
|
||||
"""pathSuffix("foo" / "bar" ~ "baz")""" should {
|
||||
val test = testFor(pathSuffix("foo" / "bar" ~ "baz") { echoUnmatchedPath })
|
||||
"accept [/orders/barbaz/foo]" in test("/orders/")
|
||||
"accept [/orders/bazbar/foo]" in test()
|
||||
}
|
||||
|
||||
"pathSuffixTest(Slash)" should {
|
||||
val test = testFor(pathSuffixTest(Slash) { echoUnmatchedPath })
|
||||
"accept [/]" in test("/")
|
||||
"accept [/foo/]" in test("/foo/")
|
||||
"accept [/foo]" in test()
|
||||
}
|
||||
|
||||
"""pathPrefix("foo" | "bar")""" should {
|
||||
val test = testFor(pathPrefix("foo" | "bar") { echoUnmatchedPath })
|
||||
"accept [/foo]" in test("")
|
||||
"accept [/foops]" in test("ps")
|
||||
"accept [/bar]" in test("")
|
||||
"reject [/baz]" in test()
|
||||
}
|
||||
|
||||
"""pathSuffix(!"foo")""" should {
|
||||
val test = testFor(pathSuffix(!"foo") { echoUnmatchedPath })
|
||||
"accept [/bar]" in test("/bar")
|
||||
"reject [/foo]" in test()
|
||||
}
|
||||
|
||||
"pathPrefix(IntNumber?)" should {
|
||||
val test = testFor(pathPrefix(IntNumber?) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/12]" in test("Some(12):")
|
||||
"accept [/12a]" in test("Some(12):a")
|
||||
"accept [/foo]" in test("None:foo")
|
||||
}
|
||||
|
||||
"""pathPrefix("foo"?)""" should {
|
||||
val test = testFor(pathPrefix("foo"?) { echoUnmatchedPath })
|
||||
"accept [/foo]" in test("")
|
||||
"accept [/fool]" in test("l")
|
||||
"accept [/bar]" in test("bar")
|
||||
}
|
||||
|
||||
"""pathPrefix("foo") & pathEnd""" should {
|
||||
val test = testFor((pathPrefix("foo") & pathEnd) { echoUnmatchedPath })
|
||||
"reject [/foobar]" in test()
|
||||
"reject [/foo/bar]" in test()
|
||||
"accept [/foo] and clear the unmatchedPath" in test("")
|
||||
"reject [/foo/]" in test()
|
||||
}
|
||||
|
||||
"""pathPrefix("foo") & pathEndOrSingleSlash""" should {
|
||||
val test = testFor((pathPrefix("foo") & pathEndOrSingleSlash) { echoUnmatchedPath })
|
||||
"reject [/foobar]" in test()
|
||||
"reject [/foo/bar]" in test()
|
||||
"accept [/foo] and clear the unmatchedPath" in test("")
|
||||
"accept [/foo/] and clear the unmatchedPath" in test("")
|
||||
}
|
||||
|
||||
"""pathPrefix(IntNumber.repeat(separator = "."))""" should {
|
||||
{
|
||||
val test = testFor(pathPrefix(IntNumber.repeat(min = 2, max = 5, separator = ".")) { echoCaptureAndUnmatchedPath })
|
||||
"reject [/foo]" in test()
|
||||
"reject [/1foo]" in test()
|
||||
"reject [/1.foo]" in test()
|
||||
"accept [/1.2foo]" in test("List(1, 2):foo")
|
||||
"accept [/1.2.foo]" in test("List(1, 2):.foo")
|
||||
"accept [/1.2.3foo]" in test("List(1, 2, 3):foo")
|
||||
"accept [/1.2.3.foo]" in test("List(1, 2, 3):.foo")
|
||||
"accept [/1.2.3.4foo]" in test("List(1, 2, 3, 4):foo")
|
||||
"accept [/1.2.3.4.foo]" in test("List(1, 2, 3, 4):.foo")
|
||||
"accept [/1.2.3.4.5foo]" in test("List(1, 2, 3, 4, 5):foo")
|
||||
"accept [/1.2.3.4.5.foo]" in test("List(1, 2, 3, 4, 5):.foo")
|
||||
"accept [/1.2.3.4.5.6foo]" in test("List(1, 2, 3, 4, 5):.6foo")
|
||||
"accept [/1.2.3.]" in test("List(1, 2, 3):.")
|
||||
"accept [/1.2.3/]" in test("List(1, 2, 3):/")
|
||||
"accept [/1.2.3./]" in test("List(1, 2, 3):./")
|
||||
}
|
||||
{
|
||||
val test = testFor(pathPrefix(IntNumber.repeat(2, ".")) { echoCaptureAndUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"reject [/1bar]" in test()
|
||||
"reject [/1.bar]" in test()
|
||||
"accept [/1.2bar]" in test("List(1, 2):bar")
|
||||
"accept [/1.2.bar]" in test("List(1, 2):.bar")
|
||||
"accept [/1.2.3bar]" in test("List(1, 2):.3bar")
|
||||
}
|
||||
}
|
||||
|
||||
"PathMatchers" should {
|
||||
{
|
||||
val test = testFor(path(Rest.tmap { case Tuple1(s) ⇒ Tuple1(s.split('-').toList) }) { echoComplete })
|
||||
"support the hmap modifier in accept [/yes-no]" in test("List(yes, no)")
|
||||
}
|
||||
{
|
||||
val test = testFor(path(Rest.map(_.split('-').toList)) { echoComplete })
|
||||
"support the map modifier in accept [/yes-no]" in test("List(yes, no)")
|
||||
}
|
||||
{
|
||||
val test = testFor(path(Rest.tflatMap { case Tuple1(s) ⇒ Some(s).filter("yes" ==).map(x ⇒ Tuple1(x)) }) { echoComplete })
|
||||
"support the hflatMap modifier in accept [/yes]" in test("yes")
|
||||
"support the hflatMap modifier in reject [/blub]" in test()
|
||||
}
|
||||
{
|
||||
val test = testFor(path(Rest.flatMap(s ⇒ Some(s).filter("yes" ==))) { echoComplete })
|
||||
"support the flatMap modifier in accept [/yes]" in test("yes")
|
||||
"support the flatMap modifier reject [/blub]" in test()
|
||||
}
|
||||
}
|
||||
|
||||
implicit class WithIn(str: String) {
|
||||
def in(f: String ⇒ Unit) = convertToWordSpecStringWrapper(str) in f(str)
|
||||
def in(body: ⇒ Unit) = convertToWordSpecStringWrapper(str) in body
|
||||
}
|
||||
|
||||
case class testFor(route: Route) {
|
||||
def apply(expectedResponse: String = null): String ⇒ Unit = exampleString ⇒
|
||||
"\\[([^\\]]+)\\]".r.findFirstMatchIn(exampleString) match {
|
||||
case Some(uri) ⇒ Get(uri.group(1)) ~> route ~> check {
|
||||
if (expectedResponse eq null) handled shouldEqual false
|
||||
else responseAs[String] shouldEqual expectedResponse
|
||||
}
|
||||
case None ⇒ failTest("Example '" + exampleString + "' doesn't contain a test uri")
|
||||
}
|
||||
}
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes._
|
||||
|
||||
"the `redirectToTrailingSlashIfMissing` directive" should {
|
||||
val route = redirectToTrailingSlashIfMissing(Found) { completeOk }
|
||||
|
||||
"pass if the request path already has a trailing slash" in {
|
||||
Get("/foo/bar/") ~> route ~> check { response shouldEqual Ok }
|
||||
}
|
||||
|
||||
"redirect if the request path doesn't have a trailing slash" in {
|
||||
Get("/foo/bar") ~> route ~> checkRedirectTo("/foo/bar/")
|
||||
}
|
||||
|
||||
"preserves the query and the frag when redirect" in {
|
||||
Get("/foo/bar?query#frag") ~> route ~> checkRedirectTo("/foo/bar/?query#frag")
|
||||
}
|
||||
|
||||
"redirect with the given redirection status code" in {
|
||||
Get("/foo/bar") ~>
|
||||
redirectToTrailingSlashIfMissing(MovedPermanently) { completeOk } ~>
|
||||
check { status shouldEqual MovedPermanently }
|
||||
}
|
||||
}
|
||||
|
||||
"the `redirectToNoTrailingSlashIfPresent` directive" should {
|
||||
val route = redirectToNoTrailingSlashIfPresent(Found) { completeOk }
|
||||
|
||||
"pass if the request path already doesn't have a trailing slash" in {
|
||||
Get("/foo/bar") ~> route ~> check { response shouldEqual Ok }
|
||||
}
|
||||
|
||||
"redirect if the request path has a trailing slash" in {
|
||||
Get("/foo/bar/") ~> route ~> checkRedirectTo("/foo/bar")
|
||||
}
|
||||
|
||||
"preserves the query and the frag when redirect" in {
|
||||
Get("/foo/bar/?query#frag") ~> route ~> checkRedirectTo("/foo/bar?query#frag")
|
||||
}
|
||||
|
||||
"redirect with the given redirection status code" in {
|
||||
Get("/foo/bar/") ~>
|
||||
redirectToNoTrailingSlashIfPresent(MovedPermanently) { completeOk } ~>
|
||||
check { status shouldEqual MovedPermanently }
|
||||
}
|
||||
}
|
||||
|
||||
import akka.http.scaladsl.model.headers.Location
|
||||
import akka.http.scaladsl.model.Uri
|
||||
|
||||
private def checkRedirectTo(expectedUri: Uri) =
|
||||
check {
|
||||
status shouldBe a[Redirection]
|
||||
inside(header[Location]) {
|
||||
case Some(Location(uri)) ⇒
|
||||
(if (expectedUri.isAbsolute) uri else uri.toRelative) shouldEqual expectedUri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
import akka.http.scaladsl.model.StatusCodes._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.impl.util._
|
||||
import akka.stream.scaladsl.{ Sink, Source }
|
||||
import akka.util.ByteString
|
||||
import org.scalatest.{ Inside, Inspectors }
|
||||
|
||||
class RangeDirectivesSpec extends RoutingSpec with Inspectors with Inside {
|
||||
lazy val wrs =
|
||||
mapSettings(_.copy(rangeCountLimit = 10, rangeCoalescingThreshold = 1L)) &
|
||||
withRangeSupport
|
||||
|
||||
def bytes(length: Byte) = Array.tabulate[Byte](length)(_.toByte)
|
||||
|
||||
"The `withRangeSupport` directive" should {
|
||||
def completeWithRangedBytes(length: Byte) = wrs(complete(bytes(length)))
|
||||
|
||||
"return an Accept-Ranges(bytes) header for GET requests" in {
|
||||
Get() ~> { wrs { complete("any") } } ~> check {
|
||||
headers should contain(`Accept-Ranges`(RangeUnits.Bytes))
|
||||
}
|
||||
}
|
||||
|
||||
"not return an Accept-Ranges(bytes) header for non-GET requests" in {
|
||||
Put() ~> { wrs { complete("any") } } ~> check {
|
||||
headers should not contain `Accept-Ranges`(RangeUnits.Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
"return a Content-Range header for a ranged request with a single range" in {
|
||||
Get() ~> addHeader(Range(ByteRange(0, 1))) ~> completeWithRangedBytes(10) ~> check {
|
||||
headers should contain(`Content-Range`(ContentRange(0, 1, 10)))
|
||||
status shouldEqual PartialContent
|
||||
responseAs[Array[Byte]] shouldEqual bytes(2)
|
||||
}
|
||||
}
|
||||
|
||||
"return a partial response for a ranged request with a single range with undefined lastBytePosition" in {
|
||||
Get() ~> addHeader(Range(ByteRange.fromOffset(5))) ~> completeWithRangedBytes(10) ~> check {
|
||||
responseAs[Array[Byte]] shouldEqual Array[Byte](5, 6, 7, 8, 9)
|
||||
}
|
||||
}
|
||||
|
||||
"return a partial response for a ranged request with a single suffix range" in {
|
||||
Get() ~> addHeader(Range(ByteRange.suffix(1))) ~> completeWithRangedBytes(10) ~> check {
|
||||
responseAs[Array[Byte]] shouldEqual Array[Byte](9)
|
||||
}
|
||||
}
|
||||
|
||||
"return a partial response for a ranged request with a overlapping suffix range" in {
|
||||
Get() ~> addHeader(Range(ByteRange.suffix(100))) ~> completeWithRangedBytes(10) ~> check {
|
||||
responseAs[Array[Byte]] shouldEqual bytes(10)
|
||||
}
|
||||
}
|
||||
|
||||
"be transparent to non-GET requests" in {
|
||||
Post() ~> addHeader(Range(ByteRange(1, 2))) ~> completeWithRangedBytes(5) ~> check {
|
||||
responseAs[Array[Byte]] shouldEqual bytes(5)
|
||||
}
|
||||
}
|
||||
|
||||
"be transparent to non-200 responses" in {
|
||||
Get() ~> addHeader(Range(ByteRange(1, 2))) ~> Route.seal(wrs(reject())) ~> check {
|
||||
status == NotFound
|
||||
headers.exists { case `Content-Range`(_, _) ⇒ true; case _ ⇒ false } shouldEqual false
|
||||
}
|
||||
}
|
||||
|
||||
"reject an unsatisfiable single range" in {
|
||||
Get() ~> addHeader(Range(ByteRange(100, 200))) ~> completeWithRangedBytes(10) ~> check {
|
||||
rejection shouldEqual UnsatisfiableRangeRejection(Seq(ByteRange(100, 200)), 10)
|
||||
}
|
||||
}
|
||||
|
||||
"reject an unsatisfiable single suffix range with length 0" in {
|
||||
Get() ~> addHeader(Range(ByteRange.suffix(0))) ~> completeWithRangedBytes(42) ~> check {
|
||||
rejection shouldEqual UnsatisfiableRangeRejection(Seq(ByteRange.suffix(0)), 42)
|
||||
}
|
||||
}
|
||||
|
||||
"return a mediaType of 'multipart/byteranges' for a ranged request with multiple ranges" in {
|
||||
Get() ~> addHeader(Range(ByteRange(0, 10), ByteRange(0, 10))) ~> completeWithRangedBytes(10) ~> check {
|
||||
mediaType.withParams(Map.empty) shouldEqual MediaTypes.`multipart/byteranges`
|
||||
}
|
||||
}
|
||||
|
||||
"return a 'multipart/byteranges' for a ranged request with multiple coalesced ranges and expect ranges in ascending order" in {
|
||||
Get() ~> addHeader(Range(ByteRange(5, 10), ByteRange(0, 1), ByteRange(1, 2))) ~> {
|
||||
wrs { complete("Some random and not super short entity.") }
|
||||
} ~> check {
|
||||
header[`Content-Range`] should be(None)
|
||||
val parts = Await.result(responseAs[Multipart.ByteRanges].parts.grouped(1000).runWith(Sink.head), 1.second)
|
||||
parts.size shouldEqual 2
|
||||
inside(parts(0)) {
|
||||
case Multipart.ByteRanges.BodyPart(range, entity, unit, headers) ⇒
|
||||
range shouldEqual ContentRange.Default(0, 2, Some(39))
|
||||
unit shouldEqual RangeUnits.Bytes
|
||||
Await.result(entity.dataBytes.utf8String, 100.millis) shouldEqual "Som"
|
||||
}
|
||||
inside(parts(1)) {
|
||||
case Multipart.ByteRanges.BodyPart(range, entity, unit, headers) ⇒
|
||||
range shouldEqual ContentRange.Default(5, 10, Some(39))
|
||||
unit shouldEqual RangeUnits.Bytes
|
||||
Await.result(entity.dataBytes.utf8String, 100.millis) shouldEqual "random"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"return a 'multipart/byteranges' for a ranged request with multiple ranges if entity data source isn't reusable" in {
|
||||
val content = "Some random and not super short entity."
|
||||
def entityData() = StreamUtils.oneTimeSource(Source.single(ByteString(content)))
|
||||
|
||||
Get() ~> addHeader(Range(ByteRange(5, 10), ByteRange(0, 1), ByteRange(1, 2))) ~> {
|
||||
wrs { complete(HttpEntity.Default(MediaTypes.`text/plain`, content.length, entityData())) }
|
||||
} ~> check {
|
||||
header[`Content-Range`] should be(None)
|
||||
val parts = Await.result(responseAs[Multipart.ByteRanges].parts.grouped(1000).runWith(Sink.head), 1.second)
|
||||
parts.size shouldEqual 2
|
||||
}
|
||||
}
|
||||
|
||||
"reject a request with too many requested ranges" in {
|
||||
val ranges = (1 to 20).map(a ⇒ ByteRange.fromOffset(a))
|
||||
Get() ~> addHeader(Range(ranges)) ~> completeWithRangedBytes(100) ~> check {
|
||||
rejection shouldEqual TooManyRangesRejection(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import MediaTypes._
|
||||
import headers._
|
||||
import StatusCodes._
|
||||
|
||||
import akka.http.scaladsl.server._
|
||||
|
||||
class RespondWithDirectivesSpec extends RoutingSpec {
|
||||
|
||||
"overrideStatusCode" should {
|
||||
"set the given status on successful responses" in {
|
||||
Get() ~> {
|
||||
overrideStatusCode(Created) { completeOk }
|
||||
} ~> check { response shouldEqual HttpResponse(Created) }
|
||||
}
|
||||
"leave rejections unaffected" in {
|
||||
Get() ~> {
|
||||
overrideStatusCode(Created) { reject }
|
||||
} ~> check { rejections shouldEqual Nil }
|
||||
}
|
||||
}
|
||||
|
||||
val customHeader = RawHeader("custom", "custom")
|
||||
val customHeader2 = RawHeader("custom2", "custom2")
|
||||
val existingHeader = RawHeader("custom", "existing")
|
||||
|
||||
"respondWithHeader" should {
|
||||
val customHeader = RawHeader("custom", "custom")
|
||||
"add the given header to successful responses" in {
|
||||
Get() ~> {
|
||||
respondWithHeader(customHeader) { completeOk }
|
||||
} ~> check { response shouldEqual HttpResponse(headers = customHeader :: Nil) }
|
||||
}
|
||||
}
|
||||
"respondWithHeaders" should {
|
||||
"add the given headers to successful responses" in {
|
||||
Get() ~> {
|
||||
respondWithHeaders(customHeader, customHeader2) { completeOk }
|
||||
} ~> check { response shouldEqual HttpResponse(headers = customHeader :: customHeader2 :: Nil) }
|
||||
}
|
||||
}
|
||||
"respondWithDefaultHeader" should {
|
||||
def route(extraHeaders: HttpHeader*) = respondWithDefaultHeader(customHeader) {
|
||||
respondWithHeaders(extraHeaders: _*) {
|
||||
completeOk
|
||||
}
|
||||
}
|
||||
|
||||
"add the given header to a response if the header was missing before" in {
|
||||
Get() ~> route() ~> check { response shouldEqual HttpResponse(headers = customHeader :: Nil) }
|
||||
}
|
||||
"not change a response if the header already existed" in {
|
||||
Get() ~> route(existingHeader) ~> check { response shouldEqual HttpResponse(headers = existingHeader :: Nil) }
|
||||
}
|
||||
}
|
||||
"respondWithDefaultHeaders" should {
|
||||
def route(extraHeaders: HttpHeader*) = respondWithDefaultHeaders(customHeader, customHeader2) {
|
||||
respondWithHeaders(extraHeaders: _*) {
|
||||
completeOk
|
||||
}
|
||||
}
|
||||
|
||||
"add the given headers to a response if the header was missing before" in {
|
||||
Get() ~> route() ~> check { response shouldEqual HttpResponse(headers = customHeader :: customHeader2 :: Nil) }
|
||||
}
|
||||
"not update an existing header" in {
|
||||
Get() ~> route(existingHeader) ~> check {
|
||||
response shouldEqual HttpResponse(headers = List(customHeader2, existingHeader))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import org.scalatest.FreeSpec
|
||||
|
||||
import scala.concurrent.Promise
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
|
||||
import akka.http.scaladsl.marshalling._
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.impl.util._
|
||||
import headers._
|
||||
import StatusCodes._
|
||||
import MediaTypes._
|
||||
|
||||
class RouteDirectivesSpec extends FreeSpec with GenericRoutingSpec {
|
||||
|
||||
"The `complete` directive should" - {
|
||||
"by chainable with the `&` operator" in {
|
||||
Get() ~> (get & complete("yeah")) ~> check { responseAs[String] shouldEqual "yeah" }
|
||||
}
|
||||
"be lazy in its argument evaluation, independently of application style" in {
|
||||
var i = 0
|
||||
Put() ~> {
|
||||
get { complete { i += 1; "get" } } ~
|
||||
put { complete { i += 1; "put" } } ~
|
||||
(post & complete { i += 1; "post" })
|
||||
} ~> check {
|
||||
responseAs[String] shouldEqual "put"
|
||||
i shouldEqual 1
|
||||
}
|
||||
}
|
||||
"support completion from response futures" - {
|
||||
"simple case without marshaller" in {
|
||||
Get() ~> {
|
||||
get & complete(Promise.successful(HttpResponse(entity = "yup")).future)
|
||||
} ~> check { responseAs[String] shouldEqual "yup" }
|
||||
}
|
||||
"for successful futures and marshalling" in {
|
||||
Get() ~> complete(Promise.successful("yes").future) ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"for failed futures and marshalling" in {
|
||||
object TestException extends RuntimeException
|
||||
Get() ~> complete(Promise.failed[String](TestException).future) ~>
|
||||
check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
"for futures failed with a RejectionError" in {
|
||||
Get() ~> complete(Promise.failed[String](RejectionError(AuthorizationFailedRejection)).future) ~>
|
||||
check {
|
||||
rejection shouldEqual AuthorizationFailedRejection
|
||||
}
|
||||
}
|
||||
}
|
||||
"allow easy handling of futured ToResponseMarshallers" in pending /*{
|
||||
trait RegistrationStatus
|
||||
case class Registered(name: String) extends RegistrationStatus
|
||||
case object AlreadyRegistered extends RegistrationStatus
|
||||
|
||||
val route =
|
||||
get {
|
||||
path("register" / Segment) { name ⇒
|
||||
def registerUser(name: String): Future[RegistrationStatus] = Future.successful {
|
||||
name match {
|
||||
case "otto" ⇒ AlreadyRegistered
|
||||
case _ ⇒ Registered(name)
|
||||
}
|
||||
}
|
||||
complete {
|
||||
registerUser(name).map[ToResponseMarshallable] {
|
||||
case Registered(_) ⇒ HttpEntity.Empty
|
||||
case AlreadyRegistered ⇒
|
||||
import spray.json.DefaultJsonProtocol._
|
||||
import spray.httpx.SprayJsonSupport._
|
||||
(StatusCodes.BadRequest, Map("error" -> "User already Registered"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Get("/register/otto") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
}
|
||||
Get("/register/karl") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
entity shouldEqual HttpEntity.Empty
|
||||
}
|
||||
}*/
|
||||
"do Content-Type negotiation for multi-marshallers" in pendingUntilFixed {
|
||||
val route = get & complete(Data("Ida", 83))
|
||||
|
||||
import akka.http.scaladsl.model.headers.Accept
|
||||
Get().withHeaders(Accept(MediaTypes.`application/json`)) ~> route ~> check {
|
||||
responseAs[String] shouldEqual
|
||||
"""{
|
||||
| "name": "Ida",
|
||||
| "age": 83
|
||||
|}""".stripMarginWithNewline("\n")
|
||||
}
|
||||
Get().withHeaders(Accept(MediaTypes.`text/xml`)) ~> route ~> check {
|
||||
responseAs[xml.NodeSeq] shouldEqual <data><name>Ida</name><age>83</age></data>
|
||||
}
|
||||
pending
|
||||
/*Get().withHeaders(Accept(MediaTypes.`text/plain`)) ~> HttpService.sealRoute(route) ~> check {
|
||||
status shouldEqual StatusCodes.NotAcceptable
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
"the redirect directive should" - {
|
||||
"produce proper 'Found' redirections" in {
|
||||
Get() ~> {
|
||||
redirect("/foo", Found)
|
||||
} ~> check {
|
||||
response shouldEqual HttpResponse(
|
||||
status = 302,
|
||||
entity = HttpEntity(`text/html`, "The requested resource temporarily resides under <a href=\"/foo\">this URI</a>."),
|
||||
headers = Location("/foo") :: Nil)
|
||||
}
|
||||
}
|
||||
|
||||
"produce proper 'NotModified' redirections" in {
|
||||
Get() ~> {
|
||||
redirect("/foo", NotModified)
|
||||
} ~> check { response shouldEqual HttpResponse(304, headers = Location("/foo") :: Nil) }
|
||||
}
|
||||
}
|
||||
|
||||
case class Data(name: String, age: Int)
|
||||
object Data {
|
||||
//import spray.json.DefaultJsonProtocol._
|
||||
//import spray.httpx.SprayJsonSupport._
|
||||
|
||||
val jsonMarshaller: ToEntityMarshaller[Data] = FIXME // jsonFormat2(Data.apply)
|
||||
val xmlMarshaller: ToEntityMarshaller[Data] = FIXME
|
||||
/*Marshaller.delegate[Data, xml.NodeSeq](MediaTypes.`text/xml`) { (data: Data) ⇒
|
||||
<data><name>{ data.name }</name><age>{ data.age }</age></data>
|
||||
}*/
|
||||
|
||||
implicit val dataMarshaller: ToResponseMarshaller[Data] = FIXME
|
||||
//ToResponseMarshaller.oneOf(MediaTypes.`application/json`, MediaTypes.`text/xml`)(jsonMarshaller, xmlMarshaller)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes._
|
||||
|
||||
class SchemeDirectivesSpec extends RoutingSpec {
|
||||
"the extractScheme directive" should {
|
||||
"extract the Uri scheme" in {
|
||||
Put("http://localhost/", "Hello") ~> extractScheme { echoComplete } ~> check { responseAs[String] shouldEqual "http" }
|
||||
}
|
||||
}
|
||||
|
||||
"""the scheme("http") directive""" should {
|
||||
"let requests with an http Uri scheme pass" in {
|
||||
Put("http://localhost/", "Hello") ~> scheme("http") { completeOk } ~> check { response shouldEqual Ok }
|
||||
}
|
||||
"reject requests with an https Uri scheme" in {
|
||||
Get("https://localhost/") ~> scheme("http") { completeOk } ~> check { rejections shouldEqual List(SchemeRejection("http")) }
|
||||
}
|
||||
"cancel SchemeRejection if other scheme passed" in {
|
||||
val route =
|
||||
scheme("https") { completeOk } ~
|
||||
scheme("http") { reject }
|
||||
|
||||
Put("http://localhost/", "Hello") ~> route ~> check {
|
||||
rejections should be(Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"""the scheme("https") directive""" should {
|
||||
"let requests with an https Uri scheme pass" in {
|
||||
Put("https://localhost/", "Hello") ~> scheme("https") { completeOk } ~> check { response shouldEqual Ok }
|
||||
}
|
||||
"reject requests with an http Uri scheme" in {
|
||||
Get("http://localhost/") ~> scheme("https") { completeOk } ~> check { rejections shouldEqual List(SchemeRejection("https")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.concurrent.Future
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.server.AuthenticationFailedRejection.{ CredentialsRejected, CredentialsMissing }
|
||||
|
||||
class SecurityDirectivesSpec extends RoutingSpec {
|
||||
val dontAuth = authenticateBasicAsync[String]("MyRealm", _ ⇒ Future.successful(None))
|
||||
val doAuth = authenticateBasicPF("MyRealm", { case UserCredentials.Provided(name) ⇒ name })
|
||||
val authWithAnonymous = doAuth.withAnonymousUser("We are Legion")
|
||||
|
||||
val challenge = HttpChallenge("Basic", "MyRealm")
|
||||
|
||||
"basic authentication" should {
|
||||
"reject requests without Authorization header with an AuthenticationFailedRejection" in {
|
||||
Get() ~> {
|
||||
dontAuth { echoComplete }
|
||||
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsMissing, challenge) }
|
||||
}
|
||||
"reject unauthenticated requests with Authorization header with an AuthenticationFailedRejection" in {
|
||||
Get() ~> Authorization(BasicHttpCredentials("Bob", "")) ~> {
|
||||
dontAuth { echoComplete }
|
||||
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsRejected, challenge) }
|
||||
}
|
||||
"reject requests with illegal Authorization header with 401" in {
|
||||
Get() ~> RawHeader("Authorization", "bob alice") ~> Route.seal {
|
||||
dontAuth { echoComplete }
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The resource requires authentication, which was not supplied with the request"
|
||||
header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(challenge))
|
||||
}
|
||||
}
|
||||
"extract the object representing the user identity created by successful authentication" in {
|
||||
Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> {
|
||||
doAuth { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Alice" }
|
||||
}
|
||||
"extract the object representing the user identity created for the anonymous user" in {
|
||||
Get() ~> {
|
||||
authWithAnonymous { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "We are Legion" }
|
||||
}
|
||||
"properly handle exceptions thrown in its inner route" in {
|
||||
object TestException extends RuntimeException
|
||||
Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> {
|
||||
Route.seal {
|
||||
doAuth { _ ⇒ throw TestException }
|
||||
}
|
||||
} ~> check { status shouldEqual StatusCodes.InternalServerError }
|
||||
}
|
||||
}
|
||||
"authentication directives" should {
|
||||
"properly stack" in {
|
||||
val otherChallenge = HttpChallenge("MyAuth", "MyRealm2")
|
||||
val otherAuth: Directive1[String] = authenticateOrRejectWithChallenge { (cred: Option[HttpCredentials]) ⇒
|
||||
Future.successful(Left(otherChallenge))
|
||||
}
|
||||
val bothAuth = dontAuth | otherAuth
|
||||
|
||||
Get() ~> Route.seal(bothAuth { echoComplete }) ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
headers.collect {
|
||||
case `WWW-Authenticate`(challenge +: Nil) ⇒ challenge
|
||||
} shouldEqual Seq(challenge, otherChallenge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import scala.collection.immutable.Seq
|
||||
import akka.http.impl.engine.ws.InternalCustomHeader
|
||||
import akka.http.scaladsl.model.headers.{ UpgradeProtocol, Upgrade }
|
||||
import akka.http.scaladsl.model.{ HttpRequest, StatusCodes, HttpResponse }
|
||||
import akka.http.scaladsl.model.ws.{ Message, UpgradeToWebsocket }
|
||||
import akka.http.scaladsl.server.{ Route, RoutingSpec }
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.stream.scaladsl.Flow
|
||||
|
||||
class WebsocketDirectivesSpec extends RoutingSpec {
|
||||
"the handleWebsocketMessages directive" should {
|
||||
"handle websocket requests" in {
|
||||
Get("http://localhost/") ~> Upgrade(List(UpgradeProtocol("websocket"))) ~>
|
||||
emulateHttpCore ~> Route.seal(handleWebsocketMessages(Flow[Message])) ~>
|
||||
check {
|
||||
status shouldEqual StatusCodes.SwitchingProtocols
|
||||
}
|
||||
}
|
||||
"reject non-websocket requests" in {
|
||||
Get("http://localhost/") ~> emulateHttpCore ~> Route.seal(handleWebsocketMessages(Flow[Message])) ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
responseAs[String] shouldEqual "Expected Websocket Upgrade request"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Only checks for upgrade header and then adds UpgradeToWebsocket mock header */
|
||||
def emulateHttpCore(req: HttpRequest): HttpRequest =
|
||||
req.header[Upgrade] match {
|
||||
case Some(upgrade) if upgrade.hasWebsocket ⇒ req.copy(headers = req.headers :+ upgradeToWebsocketHeaderMock)
|
||||
case _ ⇒ req
|
||||
}
|
||||
def upgradeToWebsocketHeaderMock: UpgradeToWebsocket =
|
||||
new InternalCustomHeader("UpgradeToWebsocketMock") with UpgradeToWebsocket {
|
||||
def requestedProtocols: Seq[String] = Nil
|
||||
|
||||
def handleMessages(handlerFlow: Flow[Message, Message, Any], subprotocol: Option[String])(implicit mat: FlowMaterializer): HttpResponse =
|
||||
HttpResponse(StatusCodes.SwitchingProtocols)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
class TupleOpsSpec extends WordSpec with Matchers {
|
||||
import TupleOps._
|
||||
|
||||
"The TupleOps" should {
|
||||
|
||||
"support folding over tuples using a binary poly-function" in {
|
||||
object Funky extends BinaryPolyFunc {
|
||||
implicit def step1 = at[Double, Int](_ + _)
|
||||
implicit def step2 = at[Double, Symbol]((d, s) ⇒ (d + s.name.tail.toInt).toByte)
|
||||
implicit def step3 = at[Byte, String]((byte, s) ⇒ byte + s.toLong)
|
||||
}
|
||||
(1, 'X2, "3").foldLeft(0.0)(Funky) shouldEqual 6L
|
||||
}
|
||||
|
||||
"support joining tuples" in {
|
||||
(1, 'X2, "3") join () shouldEqual (1, 'X2, "3")
|
||||
() join (1, 'X2, "3") shouldEqual (1, 'X2, "3")
|
||||
(1, 'X2, "3") join (4.0, 5L) shouldEqual (1, 'X2, "3", 4.0, 5L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.unmarshalling
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ Future, Await }
|
||||
import org.scalatest.matchers.Matcher
|
||||
import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers }
|
||||
import akka.http.scaladsl.testkit.ScalatestUtils
|
||||
import akka.util.ByteString
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
import akka.http.impl.util._
|
||||
import headers._
|
||||
import MediaTypes._
|
||||
|
||||
class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with ScalatestUtils {
|
||||
implicit val system = ActorSystem(getClass.getSimpleName)
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
"The PredefinedFromEntityUnmarshallers." - {
|
||||
"stringUnmarshaller should unmarshal `text/plain` content in UTF-8 to Strings" in {
|
||||
Unmarshal(HttpEntity("Hällö")).to[String] should evaluateTo("Hällö")
|
||||
}
|
||||
"charArrayUnmarshaller should unmarshal `text/plain` content in UTF-8 to char arrays" in {
|
||||
Unmarshal(HttpEntity("árvíztűrő ütvefúrógép")).to[Array[Char]] should evaluateTo("árvíztűrő ütvefúrógép".toCharArray)
|
||||
}
|
||||
}
|
||||
|
||||
"The MultipartUnmarshallers." - {
|
||||
|
||||
"multipartGeneralUnmarshaller should correctly unmarshal 'multipart/*' content with" - {
|
||||
"an empty part" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
|
||||
}
|
||||
"two empty parts" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|--XYZABC
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)),
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
|
||||
}
|
||||
"a part without entity and missing header separation CRLF" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|Content-type: text/xml
|
||||
|Age: 12
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(MediaTypes.`text/xml`), List(Age(12))))
|
||||
}
|
||||
"an implicitly typed part (without headers)" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|
|
||||
|Perfectly fine part content.
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "Perfectly fine part content.")))
|
||||
}
|
||||
"one non-empty form-data part" in {
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
|
||||
"""---
|
||||
|Content-type: text/plain; charset=UTF8
|
||||
|content-disposition: form-data; name="email"
|
||||
|
|
||||
|test@there.com
|
||||
|-----""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "test@there.com"),
|
||||
List(`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")))))
|
||||
}
|
||||
"two different parts" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345",
|
||||
"""--12345
|
||||
|
|
||||
|first part, with a trailing newline
|
||||
|
|
||||
|--12345
|
||||
|Content-Type: application/octet-stream
|
||||
|Content-Transfer-Encoding: binary
|
||||
|
|
||||
|filecontent
|
||||
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, with a trailing newline\r\n")),
|
||||
Multipart.General.BodyPart.Strict(
|
||||
HttpEntity(`application/octet-stream`, "filecontent"),
|
||||
List(RawHeader("Content-Transfer-Encoding", "binary"))))
|
||||
}
|
||||
"illegal headers" in (
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|Date: unknown
|
||||
|content-disposition: form-data; name=email
|
||||
|
|
||||
|test@there.com
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "test@there.com"),
|
||||
List(`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")),
|
||||
RawHeader("date", "unknown")))))
|
||||
"a full example (Strict)" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345",
|
||||
"""preamble and
|
||||
|more preamble
|
||||
|--12345
|
||||
|
|
||||
|first part, implicitly typed
|
||||
|--12345
|
||||
|Content-Type: application/octet-stream
|
||||
|
|
||||
|second part, explicitly typed
|
||||
|--12345--
|
||||
|epilogue and
|
||||
|more epilogue""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")),
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "second part, explicitly typed")))
|
||||
}
|
||||
"a full example (Default)" in {
|
||||
val content = """preamble and
|
||||
|more preamble
|
||||
|--12345
|
||||
|
|
||||
|first part, implicitly typed
|
||||
|--12345
|
||||
|Content-Type: application/octet-stream
|
||||
|
|
||||
|second part, explicitly typed
|
||||
|--12345--
|
||||
|epilogue and
|
||||
|more epilogue""".stripMarginWithNewline("\r\n")
|
||||
val byteStrings = content.map(c ⇒ ByteString(c.toString)) // one-char ByteStrings
|
||||
Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "12345", content.length, Source(byteStrings)))
|
||||
.to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")),
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "second part, explicitly typed")))
|
||||
}
|
||||
}
|
||||
|
||||
"multipartGeneralUnmarshaller should reject illegal multipart content with" - {
|
||||
"an empty entity" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", ByteString.empty))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity"
|
||||
}
|
||||
"an entity without initial boundary" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
"""this is
|
||||
|just preamble text""".stripMarginWithNewline("\r\n")))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity"
|
||||
}
|
||||
"a stray boundary" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "ABC",
|
||||
"""--ABC
|
||||
|Content-type: text/plain; charset=UTF8
|
||||
|--ABCContent-type: application/json
|
||||
|content-disposition: form-data; name="email"
|
||||
|-----""".stripMarginWithNewline("\r\n")))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Illegal multipart boundary in message content"
|
||||
}
|
||||
"duplicate Content-Type header" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
|
||||
"""---
|
||||
|Content-type: text/plain; charset=UTF8
|
||||
|Content-type: application/json
|
||||
|content-disposition: form-data; name="email"
|
||||
|
|
||||
|test@there.com
|
||||
|-----""".stripMarginWithNewline("\r\n")))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual
|
||||
"multipart part must not contain more than one Content-Type header"
|
||||
}
|
||||
"a missing header-separating CRLF (in Strict entity)" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
|
||||
"""---
|
||||
|not good here
|
||||
|-----""".stripMarginWithNewline("\r\n")))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Illegal character ' ' in header name"
|
||||
}
|
||||
"a missing header-separating CRLF (in Default entity)" in {
|
||||
val content = """---
|
||||
|
|
||||
|ok
|
||||
|---
|
||||
|not ok
|
||||
|-----""".stripMarginWithNewline("\r\n")
|
||||
val byteStrings = content.map(c ⇒ ByteString(c.toString)) // one-char ByteStrings
|
||||
val contentType = `multipart/form-data` withBoundary "-"
|
||||
Await.result(Unmarshal(HttpEntity.Default(contentType, content.length, Source(byteStrings)))
|
||||
.to[Multipart.General]
|
||||
.flatMap(_ toStrict 1.second).failed, 1.second).getMessage shouldEqual "Illegal character ' ' in header name"
|
||||
}
|
||||
}
|
||||
|
||||
"multipartByteRangesUnmarshaller should correctly unmarshal multipart/byteranges content with two different parts" in {
|
||||
Unmarshal(HttpEntity(`multipart/byteranges` withBoundary "12345",
|
||||
"""--12345
|
||||
|Content-Range: bytes 0-2/26
|
||||
|Content-Type: text/plain
|
||||
|
|
||||
|ABC
|
||||
|--12345
|
||||
|Content-Range: bytes 23-25/26
|
||||
|Content-Type: text/plain
|
||||
|
|
||||
|XYZ
|
||||
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.ByteRanges] should haveParts(
|
||||
Multipart.ByteRanges.BodyPart.Strict(ContentRange(0, 2, 26), HttpEntity(ContentTypes.`text/plain`, "ABC")),
|
||||
Multipart.ByteRanges.BodyPart.Strict(ContentRange(23, 25, 26), HttpEntity(ContentTypes.`text/plain`, "XYZ")))
|
||||
}
|
||||
|
||||
"multipartFormDataUnmarshaller should correctly unmarshal 'multipart/form-data' content" - {
|
||||
"with one element" in {
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|content-disposition: form-data; name=email
|
||||
|
|
||||
|test@there.com
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.FormData] should haveParts(
|
||||
Multipart.FormData.BodyPart.Strict("email", HttpEntity(ContentTypes.`application/octet-stream`, "test@there.com")))
|
||||
}
|
||||
"with a file" in {
|
||||
Unmarshal {
|
||||
HttpEntity.Default(
|
||||
contentType = `multipart/form-data` withBoundary "XYZABC",
|
||||
contentLength = 1, // not verified during unmarshalling
|
||||
data = Source {
|
||||
List(
|
||||
ByteString {
|
||||
"""--XYZABC
|
||||
|Content-Disposition: form-data; name="email"
|
||||
|
|
||||
|test@there.com
|
||||
|--XYZABC
|
||||
|Content-Dispo""".stripMarginWithNewline("\r\n")
|
||||
},
|
||||
ByteString {
|
||||
"""sition: form-data; name="userfile"; filename="test.dat"
|
||||
|Content-Type: application/pdf
|
||||
|Content-Transfer-Encoding: binary
|
||||
|
|
||||
|filecontent
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n")
|
||||
})
|
||||
})
|
||||
}.to[Multipart.FormData].flatMap(_.toStrict(1.second)) should haveParts(
|
||||
Multipart.FormData.BodyPart.Strict("email", HttpEntity(ContentTypes.`application/octet-stream`, "test@there.com")),
|
||||
Multipart.FormData.BodyPart.Strict("userfile", HttpEntity(MediaTypes.`application/pdf`, "filecontent"),
|
||||
Map("filename" -> "test.dat"), List(RawHeader("Content-Transfer-Encoding", "binary"))))
|
||||
}
|
||||
// TODO: reactivate after multipart/form-data unmarshalling integrity verification is implemented
|
||||
//
|
||||
// "reject illegal multipart content" in {
|
||||
// val Left(MalformedContent(msg, _)) = HttpEntity(`multipart/form-data` withBoundary "XYZABC", "--noboundary--").as[MultipartFormData]
|
||||
// msg shouldEqual "Missing start boundary"
|
||||
// }
|
||||
// "reject illegal form-data content" in {
|
||||
// val Left(MalformedContent(msg, _)) = HttpEntity(`multipart/form-data` withBoundary "XYZABC",
|
||||
// """|--XYZABC
|
||||
// |content-disposition: form-data; named="email"
|
||||
// |
|
||||
// |test@there.com
|
||||
// |--XYZABC--""".stripMargin).as[MultipartFormData]
|
||||
// msg shouldEqual "Illegal multipart/form-data content: unnamed body part (no Content-Disposition header or no 'name' parameter)"
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
override def afterAll() = system.shutdown()
|
||||
|
||||
def haveParts[T <: Multipart](parts: Multipart.BodyPart.Strict*): Matcher[Future[T]] =
|
||||
equal(parts).matcher[Seq[Multipart.BodyPart.Strict]] compose { x ⇒
|
||||
Await.result(x
|
||||
.fast.flatMap {
|
||||
_.parts
|
||||
.mapAsync(1)(_ toStrict 1.second)
|
||||
.grouped(100)
|
||||
.runWith(Sink.head)
|
||||
}
|
||||
.fast.recover { case _: NoSuchElementException ⇒ Nil }, 1.second)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
import akka.http.scaladsl.server.Route
|
||||
|
||||
private[util] abstract class ApplyConverterInstances {
|
||||
[#implicit def hac1[[#T1#]] = new ApplyConverter[Tuple1[[#T1#]]] {
|
||||
type In = ([#T1#]) ⇒ Route
|
||||
def apply(fn: In): (Tuple1[[#T1#]]) ⇒ Route = {
|
||||
case Tuple1([#t1#]) ⇒ fn([#t1#])
|
||||
}
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
private[util] abstract class ConstructFromTupleInstances {
|
||||
[#implicit def instance1[[#T1#], R](construct: ([#T1#]) => R): ConstructFromTuple[Tuple1[[#T1#]], R] =
|
||||
new ConstructFromTuple[Tuple1[[#T1#]], R] {
|
||||
def apply(tup: Tuple1[[#T1#]]): R = construct([#tup._1#])
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
import TupleOps.AppendOne
|
||||
|
||||
private[util] abstract class TupleAppendOneInstances {
|
||||
type Aux[P, S, Out0] = AppendOne[P, S] { type Out = Out0 }
|
||||
|
||||
implicit def append0[T1]: Aux[Unit, T1, Tuple1[T1]] =
|
||||
new AppendOne[Unit, T1] {
|
||||
type Out = Tuple1[T1]
|
||||
def apply(prefix: Unit, last: T1): Tuple1[T1] = Tuple1(last)
|
||||
}
|
||||
|
||||
[1..21#implicit def append1[[#T1#], L]: Aux[Tuple1[[#T1#]], L, Tuple2[[#T1#], L]] =
|
||||
new AppendOne[Tuple1[[#T1#]], L] {
|
||||
type Out = Tuple2[[#T1#], L]
|
||||
def apply(prefix: Tuple1[[#T1#]], last: L): Tuple2[[#T1#], L] = Tuple2([#prefix._1#], last)
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
import TupleOps.FoldLeft
|
||||
import BinaryPolyFunc.Case
|
||||
|
||||
private[util] abstract class TupleFoldInstances {
|
||||
|
||||
type Aux[In, T, Op, Out0] = FoldLeft[In, T, Op] { type Out = Out0 }
|
||||
|
||||
implicit def t0[In, Op]: Aux[In, Unit, Op, In] =
|
||||
new FoldLeft[In, Unit, Op] {
|
||||
type Out = In
|
||||
def apply(zero: In, tuple: Unit) = zero
|
||||
}
|
||||
|
||||
implicit def t1[In, A, Op](implicit f: Case[In, A, Op]): Aux[In, Tuple1[A], Op, f.Out] =
|
||||
new FoldLeft[In, Tuple1[A], Op] {
|
||||
type Out = f.Out
|
||||
def apply(zero: In, tuple: Tuple1[A]) = f(zero, tuple._1)
|
||||
}
|
||||
|
||||
[2..22#implicit def t1[In, [2..#T0#], X, T1, Op](implicit fold: Aux[In, Tuple0[[2..#T0#]], Op, X], f: Case[X, T1, Op]): Aux[In, Tuple1[[#T1#]], Op, f.Out] =
|
||||
new FoldLeft[In, Tuple1[[#T1#]], Op] {
|
||||
type Out = f.Out
|
||||
def apply(zero: In, t: Tuple1[[#T1#]]) =
|
||||
f(fold(zero, Tuple0([2..#t._0#])), t._1)
|
||||
}#
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
/**
|
||||
* Helper class to steer around SI-9013.
|
||||
*
|
||||
* It's currently impossible to implement a trait containing @varargs methods
|
||||
* if the trait is written in Scala. Therefore, derive from this class and
|
||||
* implement the method without varargs.
|
||||
* FIXME: remove once SI-9013 is fixed.
|
||||
*
|
||||
* See https://issues.scala-lang.org/browse/SI-9013
|
||||
*/
|
||||
abstract class AbstractDirective implements Directive {
|
||||
@Override
|
||||
public Route route(Route first, Route... others) {
|
||||
return createRoute(first, others);
|
||||
}
|
||||
|
||||
protected abstract Route createRoute(Route first, Route[] others);
|
||||
}
|
||||
35
akka-http/src/main/java/akka/http/javadsl/server/Coder.java
Normal file
35
akka-http/src/main/java/akka/http/javadsl/server/Coder.java
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import akka.http.scaladsl.coding.Deflate$;
|
||||
import akka.http.scaladsl.coding.Gzip$;
|
||||
import akka.http.scaladsl.coding.NoCoding$;
|
||||
import akka.stream.FlowMaterializer;
|
||||
import akka.util.ByteString;
|
||||
import scala.concurrent.Future;
|
||||
|
||||
/**
|
||||
* A coder is an implementation of the predefined encoders/decoders defined for HTTP.
|
||||
*/
|
||||
public enum Coder {
|
||||
NoCoding(NoCoding$.MODULE$), Deflate(Deflate$.MODULE$), Gzip(Gzip$.MODULE$);
|
||||
|
||||
private akka.http.scaladsl.coding.Coder underlying;
|
||||
|
||||
Coder(akka.http.scaladsl.coding.Coder underlying) {
|
||||
this.underlying = underlying;
|
||||
}
|
||||
|
||||
public ByteString encode(ByteString input) {
|
||||
return underlying.encode(input);
|
||||
}
|
||||
public Future<ByteString> decode(ByteString input, FlowMaterializer mat) {
|
||||
return underlying.decode(input, mat);
|
||||
}
|
||||
public akka.http.scaladsl.coding.Coder _underlyingScalaCoder() {
|
||||
return underlying;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
|
||||
/**
|
||||
* A directive is the basic building block for building routes by composing
|
||||
* any kind of request or response processing into the main route a request
|
||||
* flows through. It is a factory that creates a route when given a sequence of
|
||||
* route alternatives to be augmented with the function the directive
|
||||
* represents.
|
||||
*
|
||||
* The `path`-Directive, for example, filters incoming requests by checking if
|
||||
* the URI of the incoming request matches the pattern and only invokes its inner
|
||||
* routes for those requests.
|
||||
*/
|
||||
public interface Directive {
|
||||
/**
|
||||
* Creates the Route given a sequence of inner route alternatives.
|
||||
*/
|
||||
Route route(Route first, Route... others);
|
||||
}
|
||||
43
akka-http/src/main/resources/reference.conf
Normal file
43
akka-http/src/main/resources/reference.conf
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
#######################################
|
||||
# akka-http Reference Config File #
|
||||
#######################################
|
||||
|
||||
# This is the reference config file that contains all the default settings.
|
||||
# Make your edits/overrides in your application.conf.
|
||||
|
||||
akka.http.routing {
|
||||
# Enables/disables the returning of more detailed error messages to the
|
||||
# client in the error response
|
||||
# Should be disabled for browser-facing APIs due to the risk of XSS attacks
|
||||
# and (probably) enabled for internal or non-browser APIs
|
||||
# (Note that akka-http will always produce log messages containing the full error details)
|
||||
verbose-error-messages = off
|
||||
|
||||
# Enables/disables ETag and `If-Modified-Since` support for FileAndResourceDirectives
|
||||
file-get-conditional = on
|
||||
|
||||
# Enables/disables the rendering of the "rendered by" footer in directory listings
|
||||
render-vanity-footer = yes
|
||||
|
||||
# The maximum size between two requested ranges. Ranges with less space in between will be coalesced.
|
||||
#
|
||||
# When multiple ranges are requested, a server may coalesce any of the ranges that overlap or that are separated
|
||||
# by a gap that is smaller than the overhead of sending multiple parts, regardless of the order in which the
|
||||
# corresponding byte-range-spec appeared in the received Range header field. Since the typical overhead between
|
||||
# parts of a multipart/byteranges payload is around 80 bytes, depending on the selected representation's
|
||||
# media type and the chosen boundary parameter length, it can be less efficient to transfer many small
|
||||
# disjoint parts than it is to transfer the entire selected representation.
|
||||
range-coalescing-threshold = 80
|
||||
|
||||
# The maximum number of allowed ranges per request.
|
||||
# Requests with more ranges will be rejected due to DOS suspicion.
|
||||
range-count-limit = 16
|
||||
|
||||
# The maximum number of bytes per ByteString a decoding directive will produce
|
||||
# for an entity data stream.
|
||||
decode-max-bytes-per-chunk = 1m
|
||||
|
||||
# Fully qualified config path which holds the dispatcher configuration
|
||||
# to be used by FlowMaterialiser when creating Actors for IO operations.
|
||||
file-io-dispatcher = ${akka.stream.file-io-dispatcher}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.reflect.ClassTag
|
||||
import akka.http.javadsl.server.{ RequestContext, RequestVal }
|
||||
import akka.http.impl.util.JavaMapping.Implicits._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] trait ExtractionImplBase[T] extends RequestVal[T] {
|
||||
protected[http] implicit def classTag: ClassTag[T]
|
||||
def resultClass: Class[T] = classTag.runtimeClass.asInstanceOf[Class[T]]
|
||||
|
||||
def get(ctx: RequestContext): T =
|
||||
ctx.request.asScala.header[ExtractionMap].flatMap(_.get(this))
|
||||
.getOrElse(throw new RuntimeException(s"Value wasn't extracted! $this"))
|
||||
}
|
||||
|
||||
private[http] abstract class ExtractionImpl[T](implicit val classTag: ClassTag[T]) extends ExtractionImplBase[T]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import akka.http.javadsl.server.Marshaller
|
||||
import akka.http.scaladsl.marshalling
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
// FIXME: too lenient visibility, currently used to implement Java marshallers, needs proper API, see #16439
|
||||
case class MarshallerImpl[T](scalaMarshaller: ExecutionContext ⇒ marshalling.ToResponseMarshaller[T]) extends Marshaller[T]
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.reflect.ClassTag
|
||||
import akka.http.javadsl.server.Parameter
|
||||
import akka.http.scaladsl.server.directives.{ ParameterDirectives, BasicDirectives }
|
||||
import akka.http.scaladsl.server.Directive1
|
||||
import akka.http.scaladsl.server.directives.ParameterDirectives.ParamMagnet
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] class ParameterImpl[T: ClassTag](val underlying: ExecutionContext ⇒ ParamMagnet { type Out = Directive1[T] })
|
||||
extends StandaloneExtractionImpl[T] with Parameter[T] {
|
||||
|
||||
//def extract(ctx: RequestContext): Future[T] =
|
||||
def directive: Directive1[T] =
|
||||
BasicDirectives.extractExecutionContext.flatMap { implicit ec ⇒
|
||||
ParameterDirectives.parameter(underlying(ec))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.reflect.ClassTag
|
||||
import akka.http.javadsl.server.PathMatcher
|
||||
import akka.http.scaladsl.server.{ PathMatcher ⇒ ScalaPathMatcher }
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] class PathMatcherImpl[T: ClassTag](val matcher: ScalaPathMatcher[Tuple1[T]])
|
||||
extends ExtractionImpl[T] with PathMatcher[T]
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.concurrent.Future
|
||||
import akka.http.javadsl.{ model ⇒ jm }
|
||||
import akka.http.impl.util.JavaMapping.Implicits._
|
||||
import akka.http.scaladsl.server.{ RequestContext ⇒ ScalaRequestContext }
|
||||
import akka.http.javadsl.server._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] final case class RequestContextImpl(underlying: ScalaRequestContext) extends RequestContext {
|
||||
import underlying.executionContext
|
||||
|
||||
// provides auto-conversion to japi.RouteResult
|
||||
import RouteResultImpl._
|
||||
|
||||
def request: jm.HttpRequest = underlying.request
|
||||
def unmatchedPath: String = underlying.unmatchedPath.toString
|
||||
|
||||
def completeWith(futureResult: Future[RouteResult]): RouteResult =
|
||||
futureResult.flatMap {
|
||||
case r: RouteResultImpl ⇒ r.underlying
|
||||
}
|
||||
def complete(text: String): RouteResult = underlying.complete(text)
|
||||
def completeWithStatus(statusCode: Int): RouteResult =
|
||||
completeWithStatus(jm.StatusCodes.get(statusCode))
|
||||
def completeWithStatus(statusCode: jm.StatusCode): RouteResult =
|
||||
underlying.complete(statusCode.asScala)
|
||||
def completeAs[T](marshaller: Marshaller[T], value: T): RouteResult = marshaller match {
|
||||
case MarshallerImpl(m) ⇒
|
||||
implicit val marshaller = m(underlying.executionContext)
|
||||
underlying.complete(value)
|
||||
case _ ⇒ throw new IllegalArgumentException("Unsupported marshaller: $marshaller")
|
||||
}
|
||||
def complete(response: jm.HttpResponse): RouteResult = underlying.complete(response.asScala)
|
||||
|
||||
def notFound(): RouteResult = underlying.reject()
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.language.implicitConversions
|
||||
import scala.annotation.tailrec
|
||||
import scala.collection.immutable
|
||||
import akka.http.javadsl.model.ContentType
|
||||
import akka.http.scaladsl.server.directives.{ UserCredentials, ContentTypeResolver }
|
||||
import akka.http.scaladsl.server.directives.FileAndResourceDirectives.DirectoryRenderer
|
||||
import akka.http.scaladsl.model.HttpHeader
|
||||
import akka.http.scaladsl.model.headers.CustomHeader
|
||||
import akka.http.scaladsl.server.{ Route ⇒ ScalaRoute, Directive0, Directives }
|
||||
import akka.http.impl.util.JavaMapping.Implicits._
|
||||
import akka.http.scaladsl.server
|
||||
import akka.http.javadsl.server._
|
||||
import RouteStructure._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] trait ExtractionMap extends CustomHeader {
|
||||
def get[T](key: RequestVal[T]): Option[T]
|
||||
def set[T](key: RequestVal[T], value: T): ExtractionMap
|
||||
}
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] object ExtractionMap {
|
||||
implicit def apply(map: Map[RequestVal[_], Any]): ExtractionMap =
|
||||
new ExtractionMap {
|
||||
def get[T](key: RequestVal[T]): Option[T] =
|
||||
map.get(key).asInstanceOf[Option[T]]
|
||||
|
||||
def set[T](key: RequestVal[T], value: T): ExtractionMap =
|
||||
ExtractionMap(map.updated(key, value))
|
||||
|
||||
// CustomHeader methods
|
||||
override def suppressRendering: Boolean = true
|
||||
def name(): String = "ExtractedValues"
|
||||
def value(): String = "<empty>"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] object RouteImplementation extends Directives with server.RouteConcatenation {
|
||||
def apply(route: Route): ScalaRoute = route match {
|
||||
case RouteAlternatives(children) ⇒
|
||||
val converted = children.map(RouteImplementation.apply)
|
||||
converted.reduce(_ ~ _)
|
||||
case RawPathPrefix(elements, children) ⇒
|
||||
val inner = apply(RouteAlternatives(children))
|
||||
|
||||
def one[T](matcher: PathMatcher[T]): Directive0 =
|
||||
rawPathPrefix(matcher.asInstanceOf[PathMatcherImpl[T]].matcher) flatMap { value ⇒
|
||||
addExtraction(matcher, value)
|
||||
}
|
||||
elements.map(one(_)).reduce(_ & _).apply(inner)
|
||||
|
||||
case GetFromResource(path, contentType, classLoader) ⇒
|
||||
getFromResource(path, contentType.asScala, classLoader)
|
||||
case GetFromResourceDirectory(path, classLoader, resolver) ⇒
|
||||
getFromResourceDirectory(path, classLoader)(scalaResolver(resolver))
|
||||
case GetFromFile(file, contentType) ⇒
|
||||
getFromFile(file, contentType.asScala)
|
||||
case GetFromDirectory(directory, true, resolver) ⇒
|
||||
extractExecutionContext { implicit ec ⇒
|
||||
getFromBrowseableDirectory(directory.getPath)(DirectoryRenderer.defaultDirectoryRenderer, scalaResolver(resolver))
|
||||
}
|
||||
case FileAndResourceRouteWithDefaultResolver(constructor) ⇒
|
||||
RouteImplementation(constructor(new directives.ContentTypeResolver {
|
||||
def resolve(fileName: String): ContentType = ContentTypeResolver.Default(fileName)
|
||||
}))
|
||||
|
||||
case MethodFilter(m, children) ⇒
|
||||
val inner = apply(RouteAlternatives(children))
|
||||
method(m.asScala).apply(inner)
|
||||
|
||||
case Extract(extractions, children) ⇒
|
||||
val inner = apply(RouteAlternatives(children))
|
||||
extractRequestContext.flatMap { ctx ⇒
|
||||
extractions.map { e ⇒
|
||||
e.directive.flatMap(addExtraction(e.asInstanceOf[RequestVal[Any]], _))
|
||||
}.reduce(_ & _)
|
||||
}.apply(inner)
|
||||
|
||||
case BasicAuthentication(authenticator, children) ⇒
|
||||
val inner = apply(RouteAlternatives(children))
|
||||
authenticateBasicAsync(authenticator.realm, { creds ⇒
|
||||
val javaCreds =
|
||||
creds match {
|
||||
case UserCredentials.Missing ⇒
|
||||
new BasicUserCredentials {
|
||||
def available: Boolean = false
|
||||
def userName: String = throw new IllegalStateException("Credentials missing")
|
||||
def verifySecret(secret: String): Boolean = throw new IllegalStateException("Credentials missing")
|
||||
}
|
||||
case p @ UserCredentials.Provided(name) ⇒
|
||||
new BasicUserCredentials {
|
||||
def available: Boolean = true
|
||||
def userName: String = name
|
||||
def verifySecret(secret: String): Boolean = p.verifySecret(secret)
|
||||
}
|
||||
}
|
||||
|
||||
authenticator.authenticate(javaCreds)
|
||||
}).flatMap { user ⇒
|
||||
addExtraction(authenticator.asInstanceOf[RequestVal[Any]], user)
|
||||
}.apply(inner)
|
||||
|
||||
case EncodeResponse(coders, children) ⇒
|
||||
val scalaCoders = coders.map(_._underlyingScalaCoder())
|
||||
encodeResponseWith(scalaCoders.head, scalaCoders.tail: _*).apply(apply(RouteAlternatives(children)))
|
||||
|
||||
case Conditional(eTag, lastModified, children) ⇒
|
||||
conditional(eTag.asScala, lastModified.asScala).apply(apply(RouteAlternatives(children)))
|
||||
|
||||
case o: OpaqueRoute ⇒
|
||||
(ctx ⇒ o.handle(new RequestContextImpl(ctx)).asInstanceOf[RouteResultImpl].underlying)
|
||||
|
||||
case p: Product ⇒ extractExecutionContext { implicit ec ⇒ complete(500, s"Not implemented: ${p.productPrefix}") }
|
||||
}
|
||||
|
||||
def addExtraction[T](key: RequestVal[T], value: T): Directive0 = {
|
||||
@tailrec def addToExtractionMap(headers: immutable.Seq[HttpHeader], prefix: Vector[HttpHeader] = Vector.empty): immutable.Seq[HttpHeader] =
|
||||
headers match {
|
||||
case (m: ExtractionMap) +: rest ⇒ m.set(key, value) +: (prefix ++ rest)
|
||||
case other +: rest ⇒ addToExtractionMap(rest, prefix :+ other)
|
||||
case Nil ⇒ ExtractionMap(Map(key -> value)) +: prefix
|
||||
}
|
||||
mapRequest(_.mapHeaders(addToExtractionMap(_)))
|
||||
}
|
||||
|
||||
private def scalaResolver(resolver: directives.ContentTypeResolver): ContentTypeResolver =
|
||||
ContentTypeResolver(f ⇒ resolver.resolve(f).asScala)
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.language.implicitConversions
|
||||
import scala.concurrent.Future
|
||||
import akka.http.javadsl.{ server ⇒ js }
|
||||
import akka.http.scaladsl.{ server ⇒ ss }
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] class RouteResultImpl(val underlying: Future[ss.RouteResult]) extends js.RouteResult
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] object RouteResultImpl {
|
||||
implicit def autoConvert(result: Future[ss.RouteResult]): js.RouteResult =
|
||||
new RouteResultImpl(result)
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import java.io.File
|
||||
import scala.language.existentials
|
||||
import scala.collection.immutable
|
||||
import akka.http.javadsl.model.{ DateTime, ContentType, HttpMethod }
|
||||
import akka.http.javadsl.model.headers.EntityTag
|
||||
import akka.http.javadsl.server.directives.ContentTypeResolver
|
||||
import akka.http.javadsl.server._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] object RouteStructure {
|
||||
trait DirectiveRoute extends Route {
|
||||
def children: immutable.Seq[Route]
|
||||
|
||||
require(children.nonEmpty)
|
||||
}
|
||||
case class RouteAlternatives(children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
|
||||
case class MethodFilter(method: HttpMethod, children: immutable.Seq[Route]) extends DirectiveRoute {
|
||||
def filter(ctx: RequestContext): Boolean = ctx.request.method == method
|
||||
}
|
||||
|
||||
abstract case class FileAndResourceRouteWithDefaultResolver(routeConstructor: ContentTypeResolver ⇒ Route) extends Route
|
||||
case class GetFromResource(resourcePath: String, contentType: ContentType, classLoader: ClassLoader) extends Route
|
||||
case class GetFromResourceDirectory(resourceDirectory: String, classLoader: ClassLoader, resolver: ContentTypeResolver) extends Route
|
||||
case class GetFromFile(file: File, contentType: ContentType) extends Route
|
||||
case class GetFromDirectory(directory: File, browseable: Boolean, resolver: ContentTypeResolver) extends Route
|
||||
|
||||
case class RawPathPrefix(pathElements: immutable.Seq[PathMatcher[_]], children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
case class Extract(extractions: Seq[StandaloneExtractionImpl[_]], children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
case class BasicAuthentication(authenticator: HttpBasicAuthenticator[_], children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
case class EncodeResponse(coders: immutable.Seq[Coder], children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
|
||||
case class Conditional(entityTag: EntityTag, lastModified: DateTime, children: immutable.Seq[Route]) extends DirectiveRoute
|
||||
|
||||
abstract class OpaqueRoute(extractions: RequestVal[_]*) extends Route {
|
||||
def handle(ctx: RequestContext): RouteResult
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.concurrent.Future
|
||||
import scala.reflect.ClassTag
|
||||
import akka.http.javadsl.server.RequestVal
|
||||
import akka.http.scaladsl.server._
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] abstract class StandaloneExtractionImpl[T: ClassTag] extends ExtractionImpl[T] with RequestVal[T] {
|
||||
def directive: Directive1[T]
|
||||
}
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] abstract class ExtractingStandaloneExtractionImpl[T: ClassTag] extends StandaloneExtractionImpl[T] {
|
||||
def directive: Directive1[T] = Directives.extract(extract).flatMap(Directives.onSuccess(_))
|
||||
|
||||
def extract(ctx: RequestContext): Future[T]
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.impl.server
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.reflect.ClassTag
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.http.javadsl.server.Unmarshaller
|
||||
import akka.http.scaladsl.unmarshalling.FromMessageUnmarshaller
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*
|
||||
*/
|
||||
// FIXME: too lenient visibility, currently used to implement Java marshallers, needs proper API, see #16439
|
||||
case class UnmarshallerImpl[T](scalaUnmarshaller: (ExecutionContext, FlowMaterializer) ⇒ FromMessageUnmarshaller[T])(implicit val classTag: ClassTag[T])
|
||||
extends Unmarshaller[T]
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import akka.http.javadsl.server.directives._
|
||||
import scala.collection.immutable
|
||||
import scala.annotation.varargs
|
||||
import akka.http.javadsl.model.HttpMethods
|
||||
|
||||
// FIXME: add support for the remaining directives, see #16436
|
||||
abstract class AllDirectives extends PathDirectives
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
object Directives extends AllDirectives {
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
private[http] def custom(f: immutable.Seq[Route] ⇒ Route): Directive =
|
||||
new AbstractDirective {
|
||||
def createRoute(first: Route, others: Array[Route]): Route = f(first +: others.toVector)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
/**
|
||||
* A route Handler that handles a request (that is encapsulated in a [[RequestContext]])
|
||||
* and returns a [[RouteResult]] with the response (or the rejection).
|
||||
*
|
||||
* Use the methods in [[RequestContext]] to create a [[RouteResult]]. A handler mustn't
|
||||
* return [[null]] as the result.
|
||||
*/
|
||||
trait Handler {
|
||||
def handle(ctx: RequestContext): RouteResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A route handler with one additional argument.
|
||||
*/
|
||||
trait Handler1[T1] {
|
||||
def handle(ctx: RequestContext, t1: T1): RouteResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A route handler with two additional arguments.
|
||||
*/
|
||||
trait Handler2[T1, T2] {
|
||||
def handle(ctx: RequestContext, t1: T1, t2: T2): RouteResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A route handler with three additional arguments.
|
||||
*/
|
||||
trait Handler3[T1, T2, T3] {
|
||||
def handle(ctx: RequestContext, t1: T1, t2: T2, t3: T3): RouteResult
|
||||
}
|
||||
|
||||
/**
|
||||
* A route handler with four additional arguments.
|
||||
*/
|
||||
trait Handler4[T1, T2, T3, T4] {
|
||||
def handle(ctx: RequestContext, t1: T1, t2: T2, t3: T3, t4: T4): RouteResult
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import scala.concurrent.Future
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.Http.ServerBinding
|
||||
|
||||
/**
|
||||
* A convenience class to derive from to get everything from HttpService and Directives into scope.
|
||||
* Implement the [[HttpApp.createRoute]] method to provide the Route and then call [[HttpApp.bindRoute]]
|
||||
* to start the server on the specified interface.
|
||||
*/
|
||||
abstract class HttpApp
|
||||
extends AllDirectives
|
||||
with HttpServiceBase {
|
||||
protected def createRoute(): Route
|
||||
|
||||
/**
|
||||
* Starts an HTTP server on the given interface and port. Creates the route by calling the
|
||||
* user-implemented [[createRoute]] method and uses the route to handle requests of the server.
|
||||
*/
|
||||
def bindRoute(interface: String, port: Int, system: ActorSystem): Future[ServerBinding] =
|
||||
bindRoute(interface, port, createRoute(), system)
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import akka.http.impl.server.{ ExtractionImplBase, ExtractionImpl, RouteStructure }
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
|
||||
import scala.annotation.varargs
|
||||
import scala.concurrent.Future
|
||||
import scala.reflect
|
||||
import reflect.ClassTag
|
||||
|
||||
/**
|
||||
* Represents existing or missing HTTP Basic authentication credentials.
|
||||
*/
|
||||
trait BasicUserCredentials {
|
||||
/**
|
||||
* Were credentials provided in the request?
|
||||
*/
|
||||
def available: Boolean
|
||||
|
||||
/**
|
||||
* The username as sent in the request.
|
||||
*/
|
||||
def userName: String
|
||||
/**
|
||||
* Verifies the given secret against the one sent in the request.
|
||||
*/
|
||||
def verifySecret(secret: String): Boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Implement this class to provide an HTTP Basic authentication check. The [[authenticate]] method needs to be implemented
|
||||
* to check if the supplied or missing credentials are authenticated and provide a domain level object representing
|
||||
* the user as a [[RequestVal]].
|
||||
*/
|
||||
abstract class HttpBasicAuthenticator[T](val realm: String) extends AbstractDirective with ExtractionImplBase[T] with RequestVal[T] {
|
||||
protected[http] implicit def classTag: ClassTag[T] = reflect.classTag[AnyRef].asInstanceOf[ClassTag[T]]
|
||||
def authenticate(credentials: BasicUserCredentials): Future[Option[T]]
|
||||
|
||||
/**
|
||||
* Creates a return value for use in [[authenticate]] that successfully authenticates the requests and provides
|
||||
* the given user.
|
||||
*/
|
||||
def authenticateAs(user: T): Future[Option[T]] = FastFuture.successful(Some(user))
|
||||
|
||||
/**
|
||||
* Refuses access for this user.
|
||||
*/
|
||||
def refuseAccess(): Future[Option[T]] = FastFuture.successful(None)
|
||||
|
||||
/**
|
||||
* INTERNAL API
|
||||
*/
|
||||
protected[http] final def createRoute(first: Route, others: Array[Route]): Route =
|
||||
RouteStructure.BasicAuthentication(this, (first +: others).toVector)
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import scala.concurrent.Future
|
||||
import akka.actor.ActorSystem
|
||||
import akka.http.scaladsl.{ server, Http }
|
||||
import akka.http.scaladsl.Http.ServerBinding
|
||||
import akka.http.impl.server.RouteImplementation
|
||||
import akka.stream.{ ActorFlowMaterializer, FlowMaterializer }
|
||||
import akka.stream.scaladsl.{ Keep, Sink }
|
||||
|
||||
trait HttpServiceBase {
|
||||
/**
|
||||
* Starts a server on the given interface and port and uses the route to handle incoming requests.
|
||||
*/
|
||||
def bindRoute(interface: String, port: Int, route: Route, system: ActorSystem): Future[ServerBinding] = {
|
||||
implicit val sys = system
|
||||
implicit val mat = ActorFlowMaterializer()
|
||||
handleConnectionsWithRoute(interface, port, route, system, mat)
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a server on the given interface and port and uses the route to handle incoming requests.
|
||||
*/
|
||||
def bindRoute(interface: String, port: Int, route: Route, system: ActorSystem, flowMaterializer: FlowMaterializer): Future[ServerBinding] =
|
||||
handleConnectionsWithRoute(interface, port, route, system, flowMaterializer)
|
||||
|
||||
/**
|
||||
* Uses the route to handle incoming connections and requests for the ServerBinding.
|
||||
*/
|
||||
def handleConnectionsWithRoute(interface: String, port: Int, route: Route, system: ActorSystem, flowMaterializer: FlowMaterializer): Future[ServerBinding] = {
|
||||
implicit val sys = system
|
||||
implicit val mat = flowMaterializer
|
||||
|
||||
import system.dispatcher
|
||||
val r: server.Route = RouteImplementation(route)
|
||||
Http(system).bind(interface, port).toMat(Sink.foreach(_.handleWith(r)))(Keep.left).run()(flowMaterializer)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides the entrypoints to create an Http server from a route.
|
||||
*/
|
||||
object HttpService extends HttpServiceBase
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
/**
|
||||
* A marker trait for a marshaller that converts a value of type [[T]] to an
|
||||
* HttpResponse.
|
||||
*/
|
||||
trait Marshaller[T]
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import akka.http.scaladsl.marshalling.ToResponseMarshaller
|
||||
import akka.http.impl.server.MarshallerImpl
|
||||
|
||||
/**
|
||||
* A collection of predefined marshallers.
|
||||
*/
|
||||
object Marshallers {
|
||||
def STRING: Marshaller[String] = MarshallerImpl(implicit ctx ⇒ implicitly[ToResponseMarshaller[String]])
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import java.{ lang ⇒ jl }
|
||||
|
||||
import scala.concurrent.ExecutionContext
|
||||
import scala.reflect.ClassTag
|
||||
|
||||
import akka.http.scaladsl.server.Directive1
|
||||
import akka.http.scaladsl.server.directives.ParameterDirectives.ParamMagnet
|
||||
import akka.http.scaladsl.common.ToNameReceptacleEnhancements
|
||||
import akka.http.impl.server.ParameterImpl
|
||||
|
||||
/**
|
||||
* A RequestVal representing a query parameter of type T.
|
||||
*/
|
||||
trait Parameter[T] extends RequestVal[T]
|
||||
|
||||
/**
|
||||
* A collection of predefined parameters.
|
||||
* FIXME: add tests, see #16437
|
||||
*/
|
||||
object Parameters {
|
||||
import ToNameReceptacleEnhancements._
|
||||
|
||||
/**
|
||||
* A string query parameter.
|
||||
*/
|
||||
def string(name: String): Parameter[String] =
|
||||
fromScalaParam(implicit ec ⇒ ParamMagnet(name))
|
||||
|
||||
/**
|
||||
* An integer query parameter.
|
||||
*/
|
||||
def integer(name: String): Parameter[jl.Integer] =
|
||||
fromScalaParam[jl.Integer](implicit ec ⇒
|
||||
ParamMagnet(name.as[Int]).asInstanceOf[ParamMagnet { type Out = Directive1[jl.Integer] }])
|
||||
|
||||
private def fromScalaParam[T: ClassTag](underlying: ExecutionContext ⇒ ParamMagnet { type Out = Directive1[T] }): Parameter[T] =
|
||||
new ParameterImpl[T](underlying)
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import java.{ lang ⇒ jl, util ⇒ ju }
|
||||
import scala.reflect.ClassTag
|
||||
import scala.collection.JavaConverters._
|
||||
import akka.http.impl.server.PathMatcherImpl
|
||||
import akka.http.scaladsl.server.{ PathMatchers ⇒ ScalaPathMatchers, PathMatcher0, PathMatcher1 }
|
||||
|
||||
/**
|
||||
* A PathMatcher is used to match the (yet unmatched) URI path of incoming requests.
|
||||
* It is also a RequestVal that allows to access dynamic parts of the part in a
|
||||
* handler.
|
||||
*
|
||||
* Using a PathMatcher with the [[Directives.path]] or [[Directives.pathPrefix]] directives
|
||||
* "consumes" a part of the path which is recorded in [[RequestContext.unmatchedPath]].
|
||||
*/
|
||||
trait PathMatcher[T] extends RequestVal[T]
|
||||
|
||||
/**
|
||||
* A collection of predefined path matchers.
|
||||
*/
|
||||
object PathMatchers {
|
||||
val NEUTRAL: PathMatcher[Void] = matcher0(_.Neutral)
|
||||
val SLASH: PathMatcher[Void] = matcher0(_.Slash)
|
||||
val END: PathMatcher[Void] = matcher0(_.PathEnd)
|
||||
|
||||
def segment(name: String): PathMatcher[String] = matcher(_ ⇒ name -> name)
|
||||
|
||||
def integerNumber: PathMatcher[jl.Integer] = matcher(_.IntNumber.asInstanceOf[PathMatcher1[jl.Integer]])
|
||||
def hexIntegerNumber: PathMatcher[jl.Integer] = matcher(_.HexIntNumber.asInstanceOf[PathMatcher1[jl.Integer]])
|
||||
|
||||
def longNumber: PathMatcher[jl.Long] = matcher(_.LongNumber.asInstanceOf[PathMatcher1[jl.Long]])
|
||||
def hexLongNumber: PathMatcher[jl.Long] = matcher(_.HexLongNumber.asInstanceOf[PathMatcher1[jl.Long]])
|
||||
|
||||
def uuid: PathMatcher[ju.UUID] = matcher(_.JavaUUID)
|
||||
|
||||
def segment: PathMatcher[String] = matcher(_.Segment)
|
||||
def segments: PathMatcher[ju.List[String]] = matcher(_.Segments.map(_.asJava))
|
||||
def segments(maxNumber: Int): PathMatcher[ju.List[String]] = matcher(_.Segments(maxNumber).map(_.asJava))
|
||||
|
||||
def rest: PathMatcher[String] = matcher(_.Rest)
|
||||
|
||||
private def matcher[T: ClassTag](scalaMatcher: ScalaPathMatchers.type ⇒ PathMatcher1[T]): PathMatcher[T] =
|
||||
new PathMatcherImpl[T](scalaMatcher(ScalaPathMatchers))
|
||||
private def matcher0(scalaMatcher: ScalaPathMatchers.type ⇒ PathMatcher0): PathMatcher[Void] =
|
||||
new PathMatcherImpl[Void](scalaMatcher(ScalaPathMatchers).tmap(_ ⇒ Tuple1(null)))
|
||||
}
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import akka.http.javadsl.model._
|
||||
import akka.util.ByteString
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
/**
|
||||
* The RequestContext represents the state of the request while it is routed through
|
||||
* the route structure.
|
||||
*/
|
||||
trait RequestContext {
|
||||
/**
|
||||
* The incoming request.
|
||||
*/
|
||||
def request: HttpRequest
|
||||
|
||||
/**
|
||||
* The still unmatched path of the request.
|
||||
*/
|
||||
def unmatchedPath: String
|
||||
|
||||
/**
|
||||
* Completes the request with a value of type T and marshals it using the given
|
||||
* marshaller.
|
||||
*/
|
||||
def completeAs[T](marshaller: Marshaller[T], value: T): RouteResult
|
||||
|
||||
/**
|
||||
* Completes the request with the given response.
|
||||
*/
|
||||
def complete(response: HttpResponse): RouteResult
|
||||
|
||||
/**
|
||||
* Completes the request with the given string as an entity of type `text/plain`.
|
||||
*/
|
||||
def complete(text: String): RouteResult
|
||||
|
||||
/**
|
||||
* Completes the request with the given status code and no entity.
|
||||
*/
|
||||
def completeWithStatus(statusCode: StatusCode): RouteResult
|
||||
|
||||
/**
|
||||
* Completes the request with the given status code and no entity.
|
||||
*/
|
||||
def completeWithStatus(statusCode: Int): RouteResult
|
||||
|
||||
/**
|
||||
* Defers completion of the request
|
||||
*/
|
||||
def completeWith(futureResult: Future[RouteResult]): RouteResult
|
||||
|
||||
/**
|
||||
* Explicitly rejects the request as not found. Other route alternatives
|
||||
* may still be able provide a response.
|
||||
*/
|
||||
def notFound(): RouteResult
|
||||
|
||||
// FIXME: provide proper support for rejections, see #16438
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
/**
|
||||
* Represents a value that can be extracted from a request.
|
||||
*/
|
||||
trait RequestVal[T] { outer ⇒
|
||||
/**
|
||||
* An accessor for the value given the [[RequestContext]].
|
||||
*
|
||||
* Note, that some RequestVals need to be actively specified in the route structure to
|
||||
* be extracted at a particular point during routing. One example is a [[PathMatcher]]
|
||||
* that needs to used with a [[directives.PathDirectives]] to specify which part of the
|
||||
* path should actually be extracted. Another example is an [[HttpBasicAuthenticator]]
|
||||
* that needs to be used in the route explicitly to be activated.
|
||||
*/
|
||||
def get(ctx: RequestContext): T
|
||||
|
||||
/**
|
||||
* The runtime type of the extracted value.
|
||||
*/
|
||||
def resultClass: Class[T]
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server
|
||||
|
||||
import java.{ util ⇒ ju }
|
||||
import scala.concurrent.Future
|
||||
import scala.reflect.ClassTag
|
||||
import akka.http.javadsl.model.HttpMethod
|
||||
import akka.http.scaladsl.server
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.server.directives.{ RouteDirectives, BasicDirectives }
|
||||
import akka.http.impl.server.{ UnmarshallerImpl, ExtractingStandaloneExtractionImpl, RequestContextImpl, StandaloneExtractionImpl }
|
||||
import akka.http.scaladsl.util.FastFuture
|
||||
import akka.http.impl.util.JavaMapping.Implicits._
|
||||
|
||||
/**
|
||||
* A collection of predefined [[RequestVals]].
|
||||
*/
|
||||
object RequestVals {
|
||||
/**
|
||||
* Creates an extraction that extracts the request body using the supplied Unmarshaller.
|
||||
*/
|
||||
def entityAs[T](unmarshaller: Unmarshaller[T]): RequestVal[T] =
|
||||
new ExtractingStandaloneExtractionImpl[T]()(unmarshaller.classTag) {
|
||||
def extract(ctx: server.RequestContext): Future[T] = {
|
||||
val u = unmarshaller.asInstanceOf[UnmarshallerImpl[T]].scalaUnmarshaller(ctx.executionContext, ctx.flowMaterializer)
|
||||
u(ctx.request)(ctx.executionContext)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the request method.
|
||||
*/
|
||||
def requestMethod: RequestVal[HttpMethod] =
|
||||
new ExtractingStandaloneExtractionImpl[HttpMethod] {
|
||||
def extract(ctx: server.RequestContext): Future[HttpMethod] = FastFuture.successful(ctx.request.method.asJava)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new [[RequestVal]] given a [[ju.Map]] and a [[RequestVal]] that represents the key.
|
||||
* The new RequestVal represents the existing value as looked up in the map. If the key doesn't
|
||||
* exist the request is rejected.
|
||||
*/
|
||||
def lookupInMap[T, U](key: RequestVal[T], clazz: Class[U], map: ju.Map[T, U]): RequestVal[U] =
|
||||
new StandaloneExtractionImpl[U]()(ClassTag(clazz)) {
|
||||
import BasicDirectives._
|
||||
import RouteDirectives._
|
||||
|
||||
def directive: Directive1[U] =
|
||||
extract(ctx ⇒ key.get(RequestContextImpl(ctx))).flatMap {
|
||||
case key if map.containsKey(key) ⇒ provide(map.get(key))
|
||||
case _ ⇒ reject()
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue