+str #17361: Unified http java/scala projects except marshallers

This commit is contained in:
Endre Sándor Varga 2015-05-21 17:17:55 +02:00
parent 454a393af1
commit be82e85ffc
182 changed files with 13693 additions and 0 deletions

View file

@ -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");
}
}

View file

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

View file

@ -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");
}
}

View file

@ -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");
}
}

View file

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

View file

@ -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;
}
}

View file

@ -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");
}
}

View file

@ -0,0 +1 @@
<p>Lorem ipsum!</p>

View file

@ -0,0 +1 @@
XyZ

View file

@ -0,0 +1 @@
123

View file

@ -0,0 +1 @@
123

View file

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

View file

@ -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

View file

@ -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"))
}
}
}

View file

@ -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])
}
}
}

View file

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

View file

@ -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")
}
}
}

View file

@ -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
}

View file

@ -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"
}
}
}

View file

@ -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]
}

View file

@ -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>Hallo</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: _*))
}
}
}

View file

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

View file

@ -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
}

View file

@ -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 }
}
}
}

View file

@ -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

View file

@ -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 '&lt;username&gt;-password' as credentials</li>
<li><a href="/crash">/crash</a></li>
</ul>
</body>
</html>
}

View file

@ -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" }
}
}
}

View file

@ -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"
}
}
}
}

View file

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

View file

@ -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"))
}
}
}
}

View file

@ -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")
}
}
}
}

View file

@ -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!!!"
}
}

View file

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

View file

@ -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 }
}
}
}

View file

@ -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"
}
}
}
}

View file

@ -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."
}
}
}
}

View file

@ -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" }
}
}
}
}

View file

@ -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`)))
}
}
}
}

View file

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

View file

@ -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" }
}
}
}

View file

@ -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" }
}
}
}

View file

@ -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
}
}
}

View file

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

View file

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

View file

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

View file

@ -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")) }
}
}
}

View file

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

View file

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

View file

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

View file

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