+str #17361: Unified http java/scala projects except marshallers
This commit is contained in:
parent
454a393af1
commit
be82e85ffc
182 changed files with 13693 additions and 0 deletions
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import org.junit.Test;
|
||||
import scala.Option;
|
||||
import scala.concurrent.Future;
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.model.headers.Authorization;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
|
||||
public class AuthenticationDirectivesTest extends JUnitRouteTest {
|
||||
HttpBasicAuthenticator<String> authenticatedUser =
|
||||
new HttpBasicAuthenticator<String>("test-realm") {
|
||||
@Override
|
||||
public Future<Option<String>> authenticate(BasicUserCredentials credentials) {
|
||||
if (credentials.available() && // no anonymous access
|
||||
credentials.userName().equals("sina") &&
|
||||
credentials.verifySecret("1234"))
|
||||
return authenticateAs("Sina");
|
||||
else return refuseAccess();
|
||||
}
|
||||
};
|
||||
|
||||
Handler1<String> helloWorldHandler =
|
||||
new Handler1<String>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, String user) {
|
||||
return ctx.complete("Hello "+user+"!");
|
||||
}
|
||||
};
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("secure").route(
|
||||
authenticatedUser.route(
|
||||
handleWith(authenticatedUser, helloWorldHandler)
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
@Test
|
||||
public void testCorrectUser() {
|
||||
HttpRequest authenticatedRequest =
|
||||
HttpRequest.GET("/secure")
|
||||
.addHeader(Authorization.basic("sina", "1234"));
|
||||
|
||||
route.run(authenticatedRequest)
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Hello Sina!");
|
||||
}
|
||||
@Test
|
||||
public void testRejectAnonymousAccess() {
|
||||
route.run(HttpRequest.GET("/secure"))
|
||||
.assertStatusCode(401)
|
||||
.assertEntity("The resource requires authentication, which was not supplied with the request")
|
||||
.assertHeaderExists("WWW-Authenticate", "Basic realm=\"test-realm\"");
|
||||
}
|
||||
@Test
|
||||
public void testRejectUnknownUser() {
|
||||
HttpRequest authenticatedRequest =
|
||||
HttpRequest.GET("/secure")
|
||||
.addHeader(Authorization.basic("joe", "0000"));
|
||||
|
||||
route.run(authenticatedRequest)
|
||||
.assertStatusCode(401)
|
||||
.assertEntity("The supplied authentication is invalid");
|
||||
}
|
||||
@Test
|
||||
public void testRejectWrongPassword() {
|
||||
HttpRequest authenticatedRequest =
|
||||
HttpRequest.GET("/secure")
|
||||
.addHeader(Authorization.basic("sina", "1235"));
|
||||
|
||||
route.run(authenticatedRequest)
|
||||
.assertStatusCode(401)
|
||||
.assertEntity("The supplied authentication is invalid");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
|
||||
import akka.actor.ActorSystem;
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.model.headers.AcceptEncoding;
|
||||
import akka.http.javadsl.model.headers.ContentEncoding;
|
||||
import akka.http.javadsl.model.headers.HttpEncodings;
|
||||
import akka.stream.ActorFlowMaterializer;
|
||||
import akka.util.ByteString;
|
||||
import org.junit.*;
|
||||
import scala.concurrent.Await;
|
||||
import scala.concurrent.duration.Duration;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class CodingDirectivesTest extends JUnitRouteTest {
|
||||
|
||||
static ActorSystem system;
|
||||
|
||||
@BeforeClass
|
||||
public static void setup() {
|
||||
system = ActorSystem.create("FlowGraphDocTest");
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
public static void tearDown() {
|
||||
system.shutdown();
|
||||
system.awaitTermination();
|
||||
system = null;
|
||||
}
|
||||
|
||||
final ActorFlowMaterializer mat = ActorFlowMaterializer.create(system);
|
||||
|
||||
@Test
|
||||
public void testAutomaticEncodingWhenNoEncodingRequested() throws Exception {
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
encodeResponse(
|
||||
complete("TestString")
|
||||
)
|
||||
);
|
||||
|
||||
TestResponse response = route.run(HttpRequest.create());
|
||||
response
|
||||
.assertStatusCode(200);
|
||||
|
||||
Assert.assertEquals("TestString", response.entityBytes().utf8String());
|
||||
}
|
||||
@Test
|
||||
public void testAutomaticEncodingWhenDeflateRequested() throws Exception {
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
encodeResponse(
|
||||
complete("tester")
|
||||
)
|
||||
);
|
||||
|
||||
HttpRequest request = HttpRequest.create().addHeader(AcceptEncoding.create(HttpEncodings.DEFLATE));
|
||||
TestResponse response = route.run(request);
|
||||
response
|
||||
.assertStatusCode(200)
|
||||
.assertHeaderExists(ContentEncoding.create(HttpEncodings.DEFLATE));
|
||||
|
||||
ByteString decompressed =
|
||||
Await.result(Coder.Deflate.decode(response.entityBytes(), mat), Duration.apply(3, TimeUnit.SECONDS));
|
||||
Assert.assertEquals("tester", decompressed.utf8String());
|
||||
}
|
||||
@Test
|
||||
public void testEncodingWhenDeflateRequestedAndGzipSupported() {
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
encodeResponse(Coder.Gzip).route(
|
||||
complete("tester")
|
||||
)
|
||||
);
|
||||
|
||||
HttpRequest request = HttpRequest.create().addHeader(AcceptEncoding.create(HttpEncodings.DEFLATE));
|
||||
route.run(request)
|
||||
.assertStatusCode(406)
|
||||
.assertEntity("Resource representation is only available with these Content-Encodings:\ngzip");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testAutomaticDecoding() {}
|
||||
@Test
|
||||
public void testGzipDecoding() {}
|
||||
}
|
||||
|
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import akka.dispatch.Futures;
|
||||
import akka.http.javadsl.marshallers.jackson.Jackson;
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.model.MediaTypes;
|
||||
import org.junit.Test;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import java.util.concurrent.Callable;
|
||||
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
|
||||
public class CompleteTest extends JUnitRouteTest {
|
||||
@Test
|
||||
public void completeWithString() {
|
||||
Route route = complete("Everything OK!");
|
||||
|
||||
HttpRequest request = HttpRequest.create();
|
||||
|
||||
runRoute(route, request)
|
||||
.assertStatusCode(200)
|
||||
.assertMediaType(MediaTypes.TEXT_PLAIN)
|
||||
.assertEntity("Everything OK!");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void completeAsJacksonJson() {
|
||||
class Person {
|
||||
public String getFirstName() { return "Peter"; }
|
||||
public String getLastName() { return "Parker"; }
|
||||
public int getAge() { return 138; }
|
||||
}
|
||||
Route route = completeAs(Jackson.json(), new Person());
|
||||
|
||||
HttpRequest request = HttpRequest.create();
|
||||
|
||||
runRoute(route, request)
|
||||
.assertStatusCode(200)
|
||||
.assertMediaType("application/json")
|
||||
.assertEntity("{\"age\":138,\"firstName\":\"Peter\",\"lastName\":\"Parker\"}");
|
||||
}
|
||||
@Test
|
||||
public void completeWithFuture() {
|
||||
Parameter<Integer> x = Parameters.integer("x");
|
||||
Parameter<Integer> y = Parameters.integer("y");
|
||||
|
||||
Handler2<Integer, Integer> slowCalc = new Handler2<Integer, Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(final RequestContext ctx, final Integer x, final Integer y) {
|
||||
return ctx.completeWith(Futures.future(new Callable<RouteResult>() {
|
||||
@Override
|
||||
public RouteResult call() throws Exception {
|
||||
int result = x + y;
|
||||
return ctx.complete(String.format("%d + %d = %d",x, y, result));
|
||||
}
|
||||
}, executionContext()));
|
||||
}
|
||||
};
|
||||
|
||||
Route route = handleWith(x, y, slowCalc);
|
||||
runRoute(route, HttpRequest.GET("add?x=42&y=23"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("42 + 23 = 65");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import akka.http.scaladsl.model.HttpRequest;
|
||||
import org.junit.Test;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
|
||||
public class HandlerBindingTest extends JUnitRouteTest {
|
||||
@Test
|
||||
public void testHandlerWithoutExtractions() {
|
||||
Route route = handleWith(
|
||||
new Handler() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx) {
|
||||
return ctx.complete("Ok");
|
||||
}
|
||||
}
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("/"))
|
||||
.assertEntity("Ok");
|
||||
}
|
||||
@Test
|
||||
public void testHandlerWithSomeExtractions() {
|
||||
final Parameter<Integer> a = Parameters.integer("a");
|
||||
final Parameter<Integer> b = Parameters.integer("b");
|
||||
|
||||
Route route = handleWith(
|
||||
new Handler() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx) {
|
||||
return ctx.complete("Ok a:" + a.get(ctx) +" b:" + b.get(ctx));
|
||||
}
|
||||
}, a, b
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("?a=23&b=42"))
|
||||
.assertEntity("Ok a:23 b:42");
|
||||
}
|
||||
@Test
|
||||
public void testHandlerIfExtractionFails() {
|
||||
final Parameter<Integer> a = Parameters.integer("a");
|
||||
|
||||
Route route = handleWith(
|
||||
new Handler() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx) {
|
||||
return ctx.complete("Ok " + a.get(ctx));
|
||||
}
|
||||
}, a
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("/"))
|
||||
.assertStatusCode(404)
|
||||
.assertEntity("Request is missing required query parameter 'a'");
|
||||
}
|
||||
@Test
|
||||
public void testHandler1() {
|
||||
final Parameter<Integer> a = Parameters.integer("a");
|
||||
|
||||
Route route = handleWith(
|
||||
a,
|
||||
new Handler1<Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, Integer a) {
|
||||
return ctx.complete("Ok " + a);
|
||||
}
|
||||
}
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("?a=23"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Ok 23");
|
||||
}
|
||||
@Test
|
||||
public void testHandler2() {
|
||||
Route route = handleWith(
|
||||
Parameters.integer("a"),
|
||||
Parameters.integer("b"),
|
||||
new Handler2<Integer, Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, Integer a, Integer b) {
|
||||
return ctx.complete("Sum: " + (a + b));
|
||||
}
|
||||
}
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("?a=23&b=42"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Sum: 65");
|
||||
}
|
||||
@Test
|
||||
public void testHandler3() {
|
||||
Route route = handleWith(
|
||||
Parameters.integer("a"),
|
||||
Parameters.integer("b"),
|
||||
Parameters.integer("c"),
|
||||
new Handler3<Integer, Integer, Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, Integer a, Integer b, Integer c) {
|
||||
return ctx.complete("Sum: " + (a + b + c));
|
||||
}
|
||||
}
|
||||
);
|
||||
TestResponse response = runRoute(route, HttpRequest.GET("?a=23&b=42&c=30"));
|
||||
response.assertStatusCode(200);
|
||||
response.assertEntity("Sum: 95");
|
||||
}
|
||||
@Test
|
||||
public void testHandler4() {
|
||||
Route route = handleWith(
|
||||
Parameters.integer("a"),
|
||||
Parameters.integer("b"),
|
||||
Parameters.integer("c"),
|
||||
Parameters.integer("d"),
|
||||
new Handler4<Integer, Integer, Integer, Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, Integer a, Integer b, Integer c, Integer d) {
|
||||
return ctx.complete("Sum: " + (a + b + c + d));
|
||||
}
|
||||
}
|
||||
);
|
||||
runRoute(route, HttpRequest.GET("?a=23&b=42&c=30&d=45"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Sum: 140");
|
||||
}
|
||||
@Test
|
||||
public void testReflectiveInstanceHandler() {
|
||||
class Test {
|
||||
public RouteResult negate(RequestContext ctx, int a) {
|
||||
return ctx.complete("Negated: " + (- a));
|
||||
}
|
||||
}
|
||||
Route route = handleWith(new Test(), "negate", Parameters.integer("a"));
|
||||
runRoute(route, HttpRequest.GET("?a=23"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Negated: -23");
|
||||
}
|
||||
|
||||
public static RouteResult squared(RequestContext ctx, int a) {
|
||||
return ctx.complete("Squared: " + (a * a));
|
||||
}
|
||||
@Test
|
||||
public void testStaticReflectiveHandler() {
|
||||
Route route = handleWith(HandlerBindingTest.class, "squared", Parameters.integer("a"));
|
||||
runRoute(route, HttpRequest.GET("?a=23"))
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("Squared: 529");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,222 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server;
|
||||
|
||||
import static akka.http.javadsl.server.Directives.*;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import akka.http.scaladsl.model.HttpRequest;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PathDirectivesTest extends JUnitRouteTest {
|
||||
@Test
|
||||
public void testPathPrefixAndPath() {
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
pathPrefix("pet").route(
|
||||
path("cat").route(complete("The cat!")),
|
||||
path("dog").route(complete("The dog!")),
|
||||
pathSingleSlash().route(complete("Here are only pets."))
|
||||
)
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/pet/"))
|
||||
.assertEntity("Here are only pets.");
|
||||
|
||||
route.run(HttpRequest.GET("/pet")) // missing trailing slash
|
||||
.assertStatusCode(404);
|
||||
|
||||
route.run(HttpRequest.GET("/pet/cat"))
|
||||
.assertEntity("The cat!");
|
||||
|
||||
route.run(HttpRequest.GET("/pet/dog"))
|
||||
.assertEntity("The dog!");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRawPathPrefix() {
|
||||
TestRoute route1 =
|
||||
testRoute(
|
||||
rawPathPrefix(PathMatchers.SLASH(), "pet", PathMatchers.SLASH(), "", PathMatchers.SLASH(), "cat").route(
|
||||
complete("The cat!")
|
||||
)
|
||||
);
|
||||
|
||||
route1.run(HttpRequest.GET("/pet//cat"))
|
||||
.assertEntity("The cat!");
|
||||
|
||||
// any suffix allowed
|
||||
route1.run(HttpRequest.GET("/pet//cat/abcdefg"))
|
||||
.assertEntity("The cat!");
|
||||
|
||||
TestRoute route2 =
|
||||
testRoute(
|
||||
rawPathPrefix(PathMatchers.SLASH(), "pet", PathMatchers.SLASH(), "", PathMatchers.SLASH(), "cat", PathMatchers.END()).route(
|
||||
complete("The cat!")
|
||||
)
|
||||
);
|
||||
|
||||
route2.run(HttpRequest.GET("/pet//cat"))
|
||||
.assertEntity("The cat!");
|
||||
|
||||
route2.run(HttpRequest.GET("/pet//cat/abcdefg"))
|
||||
.assertStatusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSegment() {
|
||||
PathMatcher<String> name = PathMatchers.segment();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("hey", name).route(toStringEcho(name))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/hey/jude"))
|
||||
.assertEntity("jude");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleSlash() {
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
pathSingleSlash().route(complete("Ok"))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/"))
|
||||
.assertEntity("Ok");
|
||||
|
||||
route.run(HttpRequest.GET("/abc"))
|
||||
.assertStatusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testIntegerMatcher() {
|
||||
PathMatcher<Integer> age = PathMatchers.integerNumber();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("age", age).route(toStringEcho(age))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/age/38"))
|
||||
.assertEntity("38");
|
||||
|
||||
route.run(HttpRequest.GET("/age/abc"))
|
||||
.assertStatusCode(404);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testTwoVals() {
|
||||
// tests that `x` and `y` have different identities which is important for
|
||||
// retrieving the values
|
||||
PathMatcher<Integer> x = PathMatchers.integerNumber();
|
||||
PathMatcher<Integer> y = PathMatchers.integerNumber();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("multiply", x, y).route(
|
||||
handleWith(x, y, new Handler2<Integer, Integer>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, Integer x, Integer y) {
|
||||
return ctx.complete(String.format("%d * %d = %d", x, y, x * y));
|
||||
}
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/multiply/3/6"))
|
||||
.assertEntity("3 * 6 = 18");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHexIntegerMatcher() {
|
||||
PathMatcher<Integer> color = PathMatchers.hexIntegerNumber();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("color", color).route(toStringEcho(color))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/color/a0c2ef"))
|
||||
.assertEntity(Integer.toString(0xa0c2ef));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testLongMatcher() {
|
||||
PathMatcher<Long> bigAge = PathMatchers.longNumber();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("bigage", bigAge).route(toStringEcho(bigAge))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/bigage/12345678901"))
|
||||
.assertEntity("12345678901");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testHexLongMatcher() {
|
||||
PathMatcher<Long> code = PathMatchers.hexLongNumber();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("code", code).route(toStringEcho(code))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/code/a0b1c2d3e4f5"))
|
||||
.assertEntity(Long.toString(0xa0b1c2d3e4f5L));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestMatcher() {
|
||||
PathMatcher<String> rest = PathMatchers.rest();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("pets", rest).route(toStringEcho(rest))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/pets/afdaoisd/asda/sfasfasf/asf"))
|
||||
.assertEntity("afdaoisd/asda/sfasfasf/asf");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUUIDMatcher() {
|
||||
PathMatcher<UUID> uuid = PathMatchers.uuid();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("by-uuid", uuid).route(toStringEcho(uuid))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/by-uuid/6ba7b811-9dad-11d1-80b4-00c04fd430c8"))
|
||||
.assertEntity("6ba7b811-9dad-11d1-80b4-00c04fd430c8");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSegmentsMatcher() {
|
||||
PathMatcher<List<String>> segments = PathMatchers.segments();
|
||||
|
||||
TestRoute route =
|
||||
testRoute(
|
||||
path("pets", segments).route(toStringEcho(segments))
|
||||
);
|
||||
|
||||
route.run(HttpRequest.GET("/pets/cat/dog"))
|
||||
.assertEntity("[cat, dog]");
|
||||
}
|
||||
|
||||
private <T> Route toStringEcho(RequestVal<T> value) {
|
||||
return handleWith(value, new Handler1<T>() {
|
||||
@Override
|
||||
public RouteResult handle(RequestContext ctx, T t) {
|
||||
return ctx.complete(t.toString());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,79 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.examples.petstore;
|
||||
|
||||
import akka.http.javadsl.marshallers.jackson.Jackson;
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.model.MediaTypes;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import static org.junit.Assert.*;
|
||||
|
||||
import akka.http.javadsl.testkit.TestRoute;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class PetStoreAPITest extends JUnitRouteTest {
|
||||
@Test
|
||||
public void testGetPet() {
|
||||
TestResponse response = createRoute().run(HttpRequest.GET("/pet/1"));
|
||||
|
||||
response
|
||||
.assertStatusCode(200)
|
||||
.assertMediaType("application/json");
|
||||
|
||||
Pet pet = response.entityAs(Jackson.jsonAs(Pet.class));
|
||||
assertEquals("cat", pet.getName());
|
||||
assertEquals(1, pet.getId());
|
||||
}
|
||||
@Test
|
||||
public void testGetMissingPet() {
|
||||
createRoute().run(HttpRequest.GET("/pet/999"))
|
||||
.assertStatusCode(404);
|
||||
}
|
||||
@Test
|
||||
public void testPutPet() {
|
||||
HttpRequest request =
|
||||
HttpRequest.PUT("/pet/1")
|
||||
.withEntity(MediaTypes.APPLICATION_JSON.toContentType(), "{\"id\": 1, \"name\": \"giraffe\"}");
|
||||
|
||||
TestResponse response = createRoute().run(request);
|
||||
|
||||
response.assertStatusCode(200);
|
||||
|
||||
Pet pet = response.entityAs(Jackson.jsonAs(Pet.class));
|
||||
assertEquals("giraffe", pet.getName());
|
||||
assertEquals(1, pet.getId());
|
||||
}
|
||||
@Test
|
||||
public void testDeletePet() {
|
||||
Map<Integer, Pet> data = createData();
|
||||
|
||||
HttpRequest request = HttpRequest.DELETE("/pet/0");
|
||||
|
||||
createRoute(data).run(request)
|
||||
.assertStatusCode(200);
|
||||
|
||||
// test actual deletion from data store
|
||||
assertFalse(data.containsKey(0));
|
||||
}
|
||||
|
||||
private TestRoute createRoute() {
|
||||
return createRoute(createData());
|
||||
}
|
||||
private TestRoute createRoute(Map<Integer, Pet> pets) {
|
||||
return testRoute(PetStoreExample.appRoute(pets));
|
||||
}
|
||||
private Map<Integer, Pet> createData() {
|
||||
Map<Integer, Pet> pets = new HashMap<Integer, Pet>();
|
||||
Pet dog = new Pet(0, "dog");
|
||||
Pet cat = new Pet(1, "cat");
|
||||
pets.put(0, dog);
|
||||
pets.put(1, cat);
|
||||
|
||||
return pets;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.javadsl.server.examples.simple;
|
||||
|
||||
import akka.http.javadsl.model.HttpRequest;
|
||||
import akka.http.javadsl.testkit.*;
|
||||
import org.junit.Test;
|
||||
|
||||
public class SimpleServerTest extends JUnitRouteTest {
|
||||
TestRoute route = testRoute(new SimpleServerApp().createRoute());
|
||||
|
||||
@Test
|
||||
public void testAdd() {
|
||||
TestResponse response = route.run(HttpRequest.GET("/add?x=42&y=23"));
|
||||
|
||||
response
|
||||
.assertStatusCode(200)
|
||||
.assertEntity("42 + 23 = 65");
|
||||
}
|
||||
}
|
||||
1
akka-http-tests/src/test/resources/sample.html
Normal file
1
akka-http-tests/src/test/resources/sample.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
<p>Lorem ipsum!</p>
|
||||
1
akka-http-tests/src/test/resources/sample.xyz
Normal file
1
akka-http-tests/src/test/resources/sample.xyz
Normal file
|
|
@ -0,0 +1 @@
|
|||
XyZ
|
||||
1
akka-http-tests/src/test/resources/someDir/fileA.txt
Normal file
1
akka-http-tests/src/test/resources/someDir/fileA.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
123
|
||||
0
akka-http-tests/src/test/resources/someDir/fileB.xml
Normal file
0
akka-http-tests/src/test/resources/someDir/fileB.xml
Normal file
0
akka-http-tests/src/test/resources/someDir/sub/file.html
Normal file
0
akka-http-tests/src/test/resources/someDir/sub/file.html
Normal file
|
|
@ -0,0 +1 @@
|
|||
123
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.{ BeforeAndAfterAll, Matchers, WordSpec }
|
||||
import org.scalatest.concurrent.ScalaFutures
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshal
|
||||
import akka.http.scaladsl.marshalling.Marshal
|
||||
import akka.http.scaladsl.model._
|
||||
|
||||
class FormDataSpec extends WordSpec with Matchers with ScalaFutures with BeforeAndAfterAll {
|
||||
implicit val system = ActorSystem(getClass.getSimpleName)
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
val formData = FormData(Map("surname" -> "Smith", "age" -> "42"))
|
||||
|
||||
"The FormData infrastructure" should {
|
||||
"properly round-trip the fields of www-urlencoded forms" in {
|
||||
Marshal(formData).to[HttpEntity]
|
||||
.flatMap(Unmarshal(_).to[FormData]).futureValue shouldEqual formData
|
||||
}
|
||||
|
||||
"properly marshal www-urlencoded forms containing special chars" in {
|
||||
Marshal(FormData(Map("name" -> "Smith&Wesson"))).to[HttpEntity]
|
||||
.flatMap(Unmarshal(_).to[String]).futureValue shouldEqual "name=Smith%26Wesson"
|
||||
|
||||
Marshal(FormData(Map("name" -> "Smith+Wesson; hopefully!"))).to[HttpEntity]
|
||||
.flatMap(Unmarshal(_).to[String]).futureValue shouldEqual "name=Smith%2BWesson%3B+hopefully%21"
|
||||
}
|
||||
}
|
||||
|
||||
override def afterAll() = {
|
||||
system.shutdown()
|
||||
system.awaitTermination(10.seconds)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.{ Suite, BeforeAndAfterAll, Matchers }
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.util.ByteString
|
||||
|
||||
trait CodecSpecSupport extends Matchers with BeforeAndAfterAll { self: Suite ⇒
|
||||
|
||||
def readAs(string: String, charset: String = "UTF8") = equal(string).matcher[String] compose { (_: ByteString).decodeString(charset) }
|
||||
def hexDump(bytes: ByteString) = bytes.map("%02x".format(_)).mkString
|
||||
def fromHexDump(dump: String) = dump.grouped(2).toArray.map(chars ⇒ Integer.parseInt(new String(chars), 16).toByte)
|
||||
|
||||
def printBytes(i: Int, id: String) = {
|
||||
def byte(i: Int) = (i & 0xFF).toHexString
|
||||
println(id + ": " + byte(i) + ":" + byte(i >> 8) + ":" + byte(i >> 16) + ":" + byte(i >> 24))
|
||||
i
|
||||
}
|
||||
|
||||
lazy val smallTextBytes = ByteString(smallText, "UTF8")
|
||||
lazy val largeTextBytes = ByteString(largeText, "UTF8")
|
||||
|
||||
val smallText = "Yeah!"
|
||||
val largeText =
|
||||
"""Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore
|
||||
magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
|
||||
gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing
|
||||
elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos
|
||||
et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor
|
||||
sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et
|
||||
dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd
|
||||
gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.
|
||||
|
||||
Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat
|
||||
nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit augue duis
|
||||
dolore te feugait nulla facilisi. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh
|
||||
euismod tincidunt ut laoreet dolore magna aliquam erat volutpat.
|
||||
|
||||
Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit lobortis nisl ut aliquip ex ea commodo
|
||||
consequat. Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu
|
||||
feugiat nulla facilisis at vero eros et accumsan et iusto odio dignissim qui blandit praesent luptatum zzril delenit
|
||||
augue duis dolore te feugait nulla facilisi.
|
||||
|
||||
Nam liber tempor cum soluta nobis eleifend option congue nihil imperdiet doming id quod mazim placerat facer possim
|
||||
assum. Lorem ipsum dolor sit amet, consectetuer adipiscing elit, sed diam nonummy nibh euismod tincidunt ut laoreet
|
||||
dolore magna aliquam erat volutpat. Ut wisi enim ad minim veniam, quis nostrud exerci tation ullamcorper suscipit
|
||||
lobortis nisl ut aliquip ex ea commodo consequat.
|
||||
|
||||
Duis autem vel eum iriure dolor in hendrerit in vulputate velit esse molestie consequat, vel illum dolore eu feugiat
|
||||
nulla facilisis.
|
||||
|
||||
At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem
|
||||
ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt
|
||||
ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum.
|
||||
Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet,
|
||||
consetetur sadipscing elitr, At accusam aliquyam diam diam dolore dolores duo eirmod eos erat, et nonumy sed tempor et
|
||||
et invidunt justo labore Stet clita ea et gubergren, kasd magna no rebum. sanctus sea sed takimata ut vero voluptua.
|
||||
est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor
|
||||
invidunt ut labore et dolore magna aliquyam erat.
|
||||
|
||||
Consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam
|
||||
voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus
|
||||
est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy e""".replace("\r\n", "\n")
|
||||
|
||||
implicit val system = ActorSystem(getClass.getSimpleName)
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
|
||||
override def afterAll() = {
|
||||
system.shutdown()
|
||||
system.awaitTermination(10.seconds)
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.WordSpec
|
||||
import akka.util.ByteString
|
||||
import akka.stream.stage.{ SyncDirective, Context, PushStage, Stage }
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.impl.util._
|
||||
import headers._
|
||||
import HttpMethods.POST
|
||||
|
||||
class DecoderSpec extends WordSpec with CodecSpecSupport {
|
||||
|
||||
"A Decoder" should {
|
||||
"not transform the message if it doesn't contain a Content-Encoding header" in {
|
||||
val request = HttpRequest(POST, entity = HttpEntity(smallText))
|
||||
DummyDecoder.decode(request) shouldEqual request
|
||||
}
|
||||
"correctly transform the message if it contains a Content-Encoding header" in {
|
||||
val request = HttpRequest(POST, entity = HttpEntity(smallText), headers = List(`Content-Encoding`(DummyDecoder.encoding)))
|
||||
val decoded = DummyDecoder.decode(request)
|
||||
decoded.headers shouldEqual Nil
|
||||
decoded.entity.toStrict(1.second).awaitResult(1.second) shouldEqual HttpEntity(dummyDecompress(smallText))
|
||||
}
|
||||
}
|
||||
|
||||
def dummyDecompress(s: String): String = dummyDecompress(ByteString(s, "UTF8")).decodeString("UTF8")
|
||||
def dummyDecompress(bytes: ByteString): ByteString = DummyDecoder.decode(bytes).awaitResult(1.second)
|
||||
|
||||
case object DummyDecoder extends StreamDecoder {
|
||||
val encoding = HttpEncodings.compress
|
||||
|
||||
def newDecompressorStage(maxBytesPerChunk: Int): () ⇒ Stage[ByteString, ByteString] =
|
||||
() ⇒ new PushStage[ByteString, ByteString] {
|
||||
def onPush(elem: ByteString, ctx: Context[ByteString]): SyncDirective =
|
||||
ctx.push(elem ++ ByteString("compressed"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.util.ByteString
|
||||
|
||||
import java.io.{ InputStream, OutputStream }
|
||||
import java.util.zip._
|
||||
|
||||
class DeflateSpec extends CoderSpec {
|
||||
protected def Coder: Coder with StreamDecoder = Deflate
|
||||
|
||||
protected def newDecodedInputStream(underlying: InputStream): InputStream =
|
||||
new InflaterInputStream(underlying)
|
||||
|
||||
protected def newEncodedOutputStream(underlying: OutputStream): OutputStream =
|
||||
new DeflaterOutputStream(underlying)
|
||||
|
||||
override def extraTests(): Unit = {
|
||||
"throw early if header is corrupt" in {
|
||||
(the[RuntimeException] thrownBy {
|
||||
ourDecode(ByteString(0, 1, 2, 3, 4))
|
||||
}).getCause should be(a[DataFormatException])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.util.ByteString
|
||||
import org.scalatest.WordSpec
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
import HttpMethods.POST
|
||||
import scala.concurrent.duration._
|
||||
import akka.http.impl.util._
|
||||
|
||||
class EncoderSpec extends WordSpec with CodecSpecSupport {
|
||||
|
||||
"An Encoder" should {
|
||||
"not transform the message if messageFilter returns false" in {
|
||||
val request = HttpRequest(POST, entity = HttpEntity(smallText.getBytes("UTF8")))
|
||||
DummyEncoder.encode(request) shouldEqual request
|
||||
}
|
||||
"correctly transform the HttpMessage if messageFilter returns true" in {
|
||||
val request = HttpRequest(POST, entity = HttpEntity(smallText))
|
||||
val encoded = DummyEncoder.encode(request)
|
||||
encoded.headers shouldEqual List(`Content-Encoding`(DummyEncoder.encoding))
|
||||
encoded.entity.toStrict(1.second).awaitResult(1.second) shouldEqual HttpEntity(dummyCompress(smallText))
|
||||
}
|
||||
}
|
||||
|
||||
def dummyCompress(s: String): String = dummyCompress(ByteString(s, "UTF8")).utf8String
|
||||
def dummyCompress(bytes: ByteString): ByteString = DummyCompressor.compressAndFinish(bytes)
|
||||
|
||||
case object DummyEncoder extends Encoder {
|
||||
val messageFilter = Encoder.DefaultFilter
|
||||
val encoding = HttpEncodings.compress
|
||||
def newCompressor = DummyCompressor
|
||||
}
|
||||
|
||||
case object DummyCompressor extends Compressor {
|
||||
def compress(input: ByteString) = input ++ ByteString("compressed")
|
||||
def flush() = ByteString.empty
|
||||
def finish() = ByteString.empty
|
||||
|
||||
def compressAndFlush(input: ByteString): ByteString = compress(input)
|
||||
def compressAndFinish(input: ByteString): ByteString = compress(input)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import akka.http.impl.util._
|
||||
|
||||
import java.io.{ InputStream, OutputStream }
|
||||
import java.util.zip.{ ZipException, GZIPInputStream, GZIPOutputStream }
|
||||
|
||||
import akka.util.ByteString
|
||||
|
||||
class GzipSpec extends CoderSpec {
|
||||
protected def Coder: Coder with StreamDecoder = Gzip
|
||||
|
||||
protected def newDecodedInputStream(underlying: InputStream): InputStream =
|
||||
new GZIPInputStream(underlying)
|
||||
|
||||
protected def newEncodedOutputStream(underlying: OutputStream): OutputStream =
|
||||
new GZIPOutputStream(underlying)
|
||||
|
||||
override def extraTests(): Unit = {
|
||||
"decode concatenated compressions" in {
|
||||
pending // FIXME: unbreak
|
||||
ourDecode(Seq(encode("Hello, "), encode("dear "), encode("User!")).join) should readAs("Hello, dear User!")
|
||||
}
|
||||
"provide a better compression ratio than the standard Gzip/Gunzip streams" in {
|
||||
ourEncode(largeTextBytes).length should be < streamEncode(largeTextBytes).length
|
||||
}
|
||||
"throw an error on truncated input" in {
|
||||
pending // FIXME: unbreak
|
||||
val ex = the[RuntimeException] thrownBy ourDecode(streamEncode(smallTextBytes).dropRight(5))
|
||||
ex.getCause.getMessage should equal("Truncated GZIP stream")
|
||||
}
|
||||
"throw an error if compressed data is just missing the trailer at the end" in {
|
||||
def brokenCompress(payload: String) = Gzip.newCompressor.compress(ByteString(payload, "UTF-8"))
|
||||
|
||||
val ex = the[RuntimeException] thrownBy ourDecode(brokenCompress("abcdefghijkl"))
|
||||
ex.getCause.getMessage should equal("Truncated GZIP stream")
|
||||
}
|
||||
"throw early if header is corrupt" in {
|
||||
val cause = (the[RuntimeException] thrownBy ourDecode(ByteString(0, 1, 2, 3, 4))).getCause
|
||||
cause should (be(a[ZipException]) and have message "Not in GZIP format")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.coding
|
||||
|
||||
import java.io.{ OutputStream, InputStream }
|
||||
|
||||
class NoCodingSpec extends CoderSpec {
|
||||
protected def Coder: Coder with StreamDecoder = NoCoding
|
||||
|
||||
override protected def corruptInputCheck = false
|
||||
|
||||
protected def newEncodedOutputStream(underlying: OutputStream): OutputStream = underlying
|
||||
protected def newDecodedInputStream(underlying: InputStream): InputStream = underlying
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshallers
|
||||
|
||||
import akka.http.scaladsl.marshalling.ToEntityMarshaller
|
||||
import akka.http.scaladsl.model.{ HttpCharsets, HttpEntity, MediaTypes }
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller
|
||||
import akka.http.impl.util._
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
case class Employee(fname: String, name: String, age: Int, id: Long, boardMember: Boolean) {
|
||||
require(!boardMember || age > 40, "Board members must be older than 40")
|
||||
}
|
||||
|
||||
object Employee {
|
||||
val simple = Employee("Frank", "Smith", 42, 12345, false)
|
||||
val json = """{"fname":"Frank","name":"Smith","age":42,"id":12345,"boardMember":false}"""
|
||||
|
||||
val utf8 = Employee("Fränk", "Smi√", 42, 12345, false)
|
||||
val utf8json =
|
||||
"""{
|
||||
| "fname": "Fränk",
|
||||
| "name": "Smi√",
|
||||
| "age": 42,
|
||||
| "id": 12345,
|
||||
| "boardMember": false
|
||||
|}""".stripMargin.getBytes(HttpCharsets.`UTF-8`.nioCharset)
|
||||
|
||||
val illegalEmployeeJson = """{"fname":"Little Boy","name":"Smith","age":7,"id":12345,"boardMember":true}"""
|
||||
}
|
||||
|
||||
/** Common infrastructure needed for several json support subprojects */
|
||||
abstract class JsonSupportSpec extends WordSpec with Matchers with ScalatestRouteTest {
|
||||
require(getClass.getSimpleName.endsWith("Spec"))
|
||||
// assuming that the classname ends with "Spec"
|
||||
def name: String = getClass.getSimpleName.dropRight(4)
|
||||
implicit def marshaller: ToEntityMarshaller[Employee]
|
||||
implicit def unmarshaller: FromEntityUnmarshaller[Employee]
|
||||
|
||||
"The " + name should {
|
||||
"provide unmarshalling support for a case class" in {
|
||||
HttpEntity(MediaTypes.`application/json`, Employee.json) should unmarshalToValue(Employee.simple)
|
||||
}
|
||||
"provide marshalling support for a case class" in {
|
||||
val marshalled = marshal(Employee.simple)
|
||||
|
||||
marshalled.data.utf8String shouldEqual
|
||||
"""{
|
||||
| "age": 42,
|
||||
| "boardMember": false,
|
||||
| "fname": "Frank",
|
||||
| "id": 12345,
|
||||
| "name": "Smith"
|
||||
|}""".stripMarginWithNewline("\n")
|
||||
}
|
||||
"use UTF-8 as the default charset for JSON source decoding" in {
|
||||
HttpEntity(MediaTypes.`application/json`, Employee.utf8json) should unmarshalToValue(Employee.utf8)
|
||||
}
|
||||
"provide proper error messages for requirement errors" in {
|
||||
val result = unmarshal(HttpEntity(MediaTypes.`application/json`, Employee.illegalEmployeeJson))
|
||||
|
||||
result.isFailure shouldEqual true
|
||||
val ex = result.failed.get
|
||||
ex.getMessage shouldEqual "requirement failed: Board members must be older than 40"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshallers.sprayjson
|
||||
|
||||
import java.lang.StringBuilder
|
||||
|
||||
import akka.http.scaladsl.marshallers.{ JsonSupportSpec, Employee }
|
||||
import akka.http.scaladsl.marshalling.ToEntityMarshaller
|
||||
import akka.http.scaladsl.unmarshalling.FromEntityUnmarshaller
|
||||
import spray.json.{ JsValue, PrettyPrinter, JsonPrinter, DefaultJsonProtocol }
|
||||
|
||||
import scala.collection.immutable.ListMap
|
||||
|
||||
class SprayJsonSupportSpec extends JsonSupportSpec {
|
||||
object EmployeeJsonProtocol extends DefaultJsonProtocol {
|
||||
implicit val employeeFormat = jsonFormat5(Employee.apply)
|
||||
}
|
||||
import EmployeeJsonProtocol._
|
||||
|
||||
implicit val orderedFieldPrint: JsonPrinter = new PrettyPrinter {
|
||||
override protected def printObject(members: Map[String, JsValue], sb: StringBuilder, indent: Int): Unit =
|
||||
super.printObject(ListMap(members.toSeq.sortBy(_._1): _*), sb, indent)
|
||||
}
|
||||
|
||||
implicit def marshaller: ToEntityMarshaller[Employee] = SprayJsonSupport.sprayJsonMarshaller[Employee]
|
||||
implicit def unmarshaller: FromEntityUnmarshaller[Employee] = SprayJsonSupport.sprayJsonUnmarshaller[Employee]
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshallers.xml
|
||||
|
||||
import scala.xml.NodeSeq
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
import akka.http.scaladsl.unmarshalling.{ Unmarshaller, Unmarshal }
|
||||
import akka.http.scaladsl.model._
|
||||
import HttpCharsets._
|
||||
import MediaTypes._
|
||||
|
||||
class ScalaXmlSupportSpec extends WordSpec with Matchers with ScalatestRouteTest {
|
||||
import ScalaXmlSupport._
|
||||
|
||||
"ScalaXmlSupport" should {
|
||||
"NodeSeqMarshaller should marshal xml snippets to `text/xml` content in UTF-8" in {
|
||||
marshal(<employee><nr>Ha“llo</nr></employee>) shouldEqual
|
||||
HttpEntity(ContentType(`text/xml`, `UTF-8`), "<employee><nr>Ha“llo</nr></employee>")
|
||||
}
|
||||
"nodeSeqUnmarshaller should unmarshal `text/xml` content in UTF-8 to NodeSeqs" in {
|
||||
Unmarshal(HttpEntity(`text/xml`, "<int>Hällö</int>")).to[NodeSeq].map(_.text) should evaluateTo("Hällö")
|
||||
}
|
||||
"nodeSeqUnmarshaller should reject `application/octet-stream`" in {
|
||||
Unmarshal(HttpEntity(`application/octet-stream`, "<int>Hällö</int>")).to[NodeSeq].map(_.text) should
|
||||
haveFailedWith(Unmarshaller.UnsupportedContentTypeException(nodeSeqContentTypeRanges: _*))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,138 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
import org.scalatest.{ Matchers, FreeSpec }
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
import akka.http.scaladsl.model._
|
||||
import MediaTypes._
|
||||
import HttpCharsets._
|
||||
|
||||
class ContentNegotiationSpec extends FreeSpec with Matchers {
|
||||
|
||||
"Content Negotiation should work properly for requests with header(s)" - {
|
||||
|
||||
"(without headers)" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept: */*" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept: */*;q=.8" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept: text/*" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`)
|
||||
accept(`audio/ogg`) should reject
|
||||
}
|
||||
|
||||
"Accept: text/*;q=.8" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/xml` withCharset `UTF-16`) should select(`text/xml`, `UTF-16`)
|
||||
accept(`audio/ogg`) should reject
|
||||
}
|
||||
|
||||
"Accept: text/*;q=0" in test { accept ⇒
|
||||
accept(`text/plain`) should reject
|
||||
accept(`text/xml` withCharset `UTF-16`) should reject
|
||||
accept(`audio/ogg`) should reject
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-16" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should reject
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-16, UTF-8" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-8;q=.2, UTF-16" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-16`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
|
||||
}
|
||||
|
||||
"Accept-Charset: UTF-8;q=.2" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `ISO-8859-1`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
|
||||
}
|
||||
|
||||
"Accept-Charset: latin1;q=.1, UTF-8;q=.2" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should select(`text/plain`, `UTF-8`)
|
||||
}
|
||||
|
||||
"Accept-Charset: *" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `UTF-8`)
|
||||
accept(`text/plain` withCharset `UTF-16`) should select(`text/plain`, `UTF-16`)
|
||||
}
|
||||
|
||||
"Accept-Charset: *;q=0" in test { accept ⇒
|
||||
accept(`text/plain`) should reject
|
||||
accept(`text/plain` withCharset `UTF-16`) should reject
|
||||
}
|
||||
|
||||
"Accept-Charset: us;q=0.1,*;q=0" in test { accept ⇒
|
||||
accept(`text/plain`) should select(`text/plain`, `US-ASCII`)
|
||||
accept(`text/plain` withCharset `UTF-8`) should reject
|
||||
}
|
||||
|
||||
"Accept: text/xml, text/html;q=.5" in test { accept ⇒
|
||||
accept(`text/plain`) should reject
|
||||
accept(`text/xml`) should select(`text/xml`, `UTF-8`)
|
||||
accept(`text/html`) should select(`text/html`, `UTF-8`)
|
||||
accept(`text/html`, `text/xml`) should select(`text/xml`, `UTF-8`)
|
||||
accept(`text/xml`, `text/html`) should select(`text/xml`, `UTF-8`)
|
||||
accept(`text/plain`, `text/xml`) should select(`text/xml`, `UTF-8`)
|
||||
accept(`text/plain`, `text/html`) should select(`text/html`, `UTF-8`)
|
||||
}
|
||||
|
||||
"""Accept: text/html, text/plain;q=0.8, application/*;q=.5, *;q= .2
|
||||
Accept-Charset: UTF-16""" in test { accept ⇒
|
||||
accept(`text/plain`, `text/html`, `audio/ogg`) should select(`text/html`, `UTF-16`)
|
||||
accept(`text/plain`, `text/html` withCharset `UTF-8`, `audio/ogg`) should select(`text/plain`, `UTF-16`)
|
||||
accept(`audio/ogg`, `application/javascript`, `text/plain` withCharset `UTF-8`) should select(`application/javascript`, `UTF-16`)
|
||||
accept(`image/gif`, `application/javascript`) should select(`application/javascript`, `UTF-16`)
|
||||
accept(`image/gif`, `audio/ogg`) should select(`image/gif`, `UTF-16`)
|
||||
}
|
||||
}
|
||||
|
||||
def test[U](body: ((ContentType*) ⇒ Option[ContentType]) ⇒ U): String ⇒ U = { example ⇒
|
||||
val headers =
|
||||
if (example != "(without headers)") {
|
||||
example.split('\n').toList map { rawHeader ⇒
|
||||
val Array(name, value) = rawHeader.split(':')
|
||||
HttpHeader.parse(name.trim, value) match {
|
||||
case HttpHeader.ParsingResult.Ok(header, Nil) ⇒ header
|
||||
case result ⇒ fail(result.errors.head.formatPretty)
|
||||
}
|
||||
}
|
||||
} else Nil
|
||||
val request = HttpRequest(headers = headers)
|
||||
body { contentTypes ⇒
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
implicit val marshallers = contentTypes map {
|
||||
case ct @ ContentType(mt, Some(cs)) ⇒ Marshaller.withFixedCharset(mt, cs)((s: String) ⇒ HttpEntity(ct, s))
|
||||
case ContentType(mt, None) ⇒ Marshaller.withOpenCharset(mt)((s: String, cs) ⇒ HttpEntity(ContentType(mt, cs), s))
|
||||
}
|
||||
Await.result(Marshal("foo").toResponseFor(request)
|
||||
.fast.map(response ⇒ Some(response.entity.contentType))
|
||||
.fast.recover { case _: Marshal.UnacceptableResponseContentTypeException ⇒ None }, 1.second)
|
||||
}
|
||||
}
|
||||
|
||||
def reject = equal(None)
|
||||
def select(mediaType: MediaType, charset: HttpCharset) = equal(Some(ContentType(mediaType, charset)))
|
||||
}
|
||||
|
|
@ -0,0 +1,149 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.marshalling
|
||||
|
||||
import akka.http.scaladsl.testkit.MarshallingTestUtils
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
|
||||
|
||||
import scala.collection.immutable.ListMap
|
||||
import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers }
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.stream.scaladsl.Source
|
||||
import akka.http.impl.util._
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
import HttpCharsets._
|
||||
import MediaTypes._
|
||||
|
||||
class MarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with MultipartMarshallers with MarshallingTestUtils {
|
||||
implicit val system = ActorSystem(getClass.getSimpleName)
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
"The PredefinedToEntityMarshallers." - {
|
||||
"StringMarshaller should marshal strings to `text/plain` content in UTF-8" in {
|
||||
marshal("Ha“llo") shouldEqual HttpEntity("Ha“llo")
|
||||
}
|
||||
"CharArrayMarshaller should marshal char arrays to `text/plain` content in UTF-8" in {
|
||||
marshal("Ha“llo".toCharArray) shouldEqual HttpEntity("Ha“llo")
|
||||
}
|
||||
"FormDataMarshaller should marshal FormData instances to application/x-www-form-urlencoded content" in {
|
||||
marshal(FormData(Map("name" -> "Bob", "pass" -> "hällo", "admin" -> ""))) shouldEqual
|
||||
HttpEntity(ContentType(`application/x-www-form-urlencoded`, `UTF-8`), "name=Bob&pass=h%C3%A4llo&admin=")
|
||||
}
|
||||
}
|
||||
|
||||
"The GenericMarshallers." - {
|
||||
"optionMarshaller should enable marshalling of Option[T]" in {
|
||||
|
||||
marshal(Some("Ha“llo")) shouldEqual HttpEntity("Ha“llo")
|
||||
marshal(None: Option[String]) shouldEqual HttpEntity.Empty
|
||||
}
|
||||
"eitherMarshaller should enable marshalling of Either[A, B]" in {
|
||||
marshal[Either[Array[Char], String]](Right("right")) shouldEqual HttpEntity("right")
|
||||
marshal[Either[Array[Char], String]](Left("left".toCharArray)) shouldEqual HttpEntity("left")
|
||||
}
|
||||
}
|
||||
|
||||
"The MultipartMarshallers." - {
|
||||
"multipartMarshaller should correctly marshal multipart content with" - {
|
||||
"one empty part" in {
|
||||
marshal(Multipart.General(`multipart/mixed`, Multipart.General.BodyPart.Strict(""))) shouldEqual HttpEntity(
|
||||
contentType = ContentType(`multipart/mixed` withBoundary randomBoundary),
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=UTF-8
|
||||
|
|
||||
|
|
||||
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
|
||||
}
|
||||
"one non-empty part" in {
|
||||
marshal(Multipart.General(`multipart/alternative`, Multipart.General.BodyPart.Strict(
|
||||
entity = HttpEntity(ContentType(`text/plain`, `UTF-8`), "test@there.com"),
|
||||
headers = `Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")) :: Nil))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/alternative` withBoundary randomBoundary),
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=UTF-8
|
||||
|Content-Disposition: form-data; name=email
|
||||
|
|
||||
|test@there.com
|
||||
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
|
||||
}
|
||||
"two different parts" in {
|
||||
marshal(Multipart.General(`multipart/related`,
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentType(`text/plain`, Some(`US-ASCII`)), "first part, with a trailing linebreak\r\n")),
|
||||
Multipart.General.BodyPart.Strict(
|
||||
HttpEntity(ContentType(`application/octet-stream`), "filecontent"),
|
||||
RawHeader("Content-Transfer-Encoding", "binary") :: Nil))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/related` withBoundary randomBoundary),
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=US-ASCII
|
||||
|
|
||||
|first part, with a trailing linebreak
|
||||
|
|
||||
|--$randomBoundary
|
||||
|Content-Type: application/octet-stream
|
||||
|Content-Transfer-Encoding: binary
|
||||
|
|
||||
|filecontent
|
||||
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
|
||||
}
|
||||
}
|
||||
|
||||
"multipartFormDataMarshaller should correctly marshal 'multipart/form-data' content with" - {
|
||||
"two fields" in {
|
||||
marshal(Multipart.FormData(ListMap(
|
||||
"surname" -> HttpEntity("Mike"),
|
||||
"age" -> marshal(<int>42</int>)))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/form-data` withBoundary randomBoundary),
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/plain; charset=UTF-8
|
||||
|Content-Disposition: form-data; name=surname
|
||||
|
|
||||
|Mike
|
||||
|--$randomBoundary
|
||||
|Content-Type: text/xml; charset=UTF-8
|
||||
|Content-Disposition: form-data; name=age
|
||||
|
|
||||
|<int>42</int>
|
||||
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
|
||||
}
|
||||
|
||||
"two fields having a custom `Content-Disposition`" in {
|
||||
marshal(Multipart.FormData(Source(List(
|
||||
Multipart.FormData.BodyPart("attachment[0]", HttpEntity(`text/csv`, "name,age\r\n\"John Doe\",20\r\n"),
|
||||
Map("filename" -> "attachment.csv")),
|
||||
Multipart.FormData.BodyPart("attachment[1]", HttpEntity("naice!".getBytes),
|
||||
Map("filename" -> "attachment2.csv"), List(RawHeader("Content-Transfer-Encoding", "binary"))))))) shouldEqual
|
||||
HttpEntity(
|
||||
contentType = ContentType(`multipart/form-data` withBoundary randomBoundary),
|
||||
string = s"""--$randomBoundary
|
||||
|Content-Type: text/csv
|
||||
|Content-Disposition: form-data; filename=attachment.csv; name="attachment[0]"
|
||||
|
|
||||
|name,age
|
||||
|"John Doe",20
|
||||
|
|
||||
|--$randomBoundary
|
||||
|Content-Type: application/octet-stream
|
||||
|Content-Disposition: form-data; filename=attachment2.csv; name="attachment[1]"
|
||||
|Content-Transfer-Encoding: binary
|
||||
|
|
||||
|naice!
|
||||
|--$randomBoundary--""".stripMarginWithNewline("\r\n"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override def afterAll() = system.shutdown()
|
||||
|
||||
protected class FixedRandom extends java.util.Random {
|
||||
override def nextBytes(array: Array[Byte]): Unit = "my-stable-boundary".getBytes("UTF-8").copyToArray(array)
|
||||
}
|
||||
override protected val multipartBoundaryRandom = new FixedRandom // fix for stable value
|
||||
}
|
||||
|
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.model
|
||||
import model.HttpMethods._
|
||||
import model.StatusCodes
|
||||
|
||||
class BasicRouteSpecs extends RoutingSpec {
|
||||
|
||||
"routes created by the concatenation operator '~'" should {
|
||||
"yield the first sub route if it succeeded" in {
|
||||
Get() ~> {
|
||||
get { complete("first") } ~ get { complete("second") }
|
||||
} ~> check { responseAs[String] shouldEqual "first" }
|
||||
}
|
||||
"yield the second sub route if the first did not succeed" in {
|
||||
Get() ~> {
|
||||
post { complete("first") } ~ get { complete("second") }
|
||||
} ~> check { responseAs[String] shouldEqual "second" }
|
||||
}
|
||||
"collect rejections from both sub routes" in {
|
||||
Delete() ~> {
|
||||
get { completeOk } ~ put { completeOk }
|
||||
} ~> check { rejections shouldEqual Seq(MethodRejection(GET), MethodRejection(PUT)) }
|
||||
}
|
||||
"clear rejections that have already been 'overcome' by previous directives" in {
|
||||
pending
|
||||
/*Put() ~> {
|
||||
put { parameter('yeah) { echoComplete } } ~
|
||||
get { completeOk }
|
||||
} ~> check { rejection shouldEqual MissingQueryParamRejection("yeah") }*/
|
||||
}
|
||||
}
|
||||
|
||||
"Route conjunction" should {
|
||||
val stringDirective = provide("The cat")
|
||||
val intDirective = provide(42)
|
||||
val doubleDirective = provide(23.0)
|
||||
|
||||
val dirStringInt = stringDirective & intDirective
|
||||
val dirStringIntDouble = dirStringInt & doubleDirective
|
||||
val dirDoubleStringInt = doubleDirective & dirStringInt
|
||||
val dirStringIntStringInt = dirStringInt & dirStringInt
|
||||
|
||||
"work for two elements" in {
|
||||
Get("/abc") ~> {
|
||||
dirStringInt { (str, i) ⇒
|
||||
complete(s"$str ${i + 1}")
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "The cat 43" }
|
||||
}
|
||||
"work for 2 + 1" in {
|
||||
Get("/abc") ~> {
|
||||
dirStringIntDouble { (str, i, d) ⇒
|
||||
complete(s"$str ${i + 1} ${d + 0.1}")
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "The cat 43 23.1" }
|
||||
}
|
||||
"work for 1 + 2" in {
|
||||
Get("/abc") ~> {
|
||||
dirDoubleStringInt { (d, str, i) ⇒
|
||||
complete(s"$str ${i + 1} ${d + 0.1}")
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "The cat 43 23.1" }
|
||||
}
|
||||
"work for 2 + 2" in {
|
||||
Get("/abc") ~> {
|
||||
dirStringIntStringInt { (str, i, str2, i2) ⇒
|
||||
complete(s"$str ${i + i2} $str2")
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "The cat 84 The cat" }
|
||||
}
|
||||
}
|
||||
"Route disjunction" should {
|
||||
"work in the happy case" in {
|
||||
val route = Route.seal((path("abc") | path("def")) {
|
||||
completeOk
|
||||
})
|
||||
|
||||
Get("/abc") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
Get("/def") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
}
|
||||
Get("/ghi") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.NotFound
|
||||
}
|
||||
}
|
||||
"don't apply alternative if inner route rejects" in {
|
||||
object MyRejection extends Rejection
|
||||
val route = (path("abc") | post) {
|
||||
reject(MyRejection)
|
||||
}
|
||||
Get("/abc") ~> route ~> check {
|
||||
rejection shouldEqual MyRejection
|
||||
}
|
||||
}
|
||||
}
|
||||
"Case class extraction with Directive.as" should {
|
||||
"extract one argument" in {
|
||||
case class MyNumber(i: Int)
|
||||
|
||||
val abcPath = path("abc" / IntNumber).as(MyNumber)(echoComplete)
|
||||
|
||||
Get("/abc/5") ~> abcPath ~> check {
|
||||
responseAs[String] shouldEqual "MyNumber(5)"
|
||||
}
|
||||
}
|
||||
"extract two arguments" in {
|
||||
case class Person(name: String, age: Int)
|
||||
|
||||
val personPath = path("person" / Segment / IntNumber).as(Person)(echoComplete)
|
||||
|
||||
Get("/person/john/38") ~> personPath ~> check {
|
||||
responseAs[String] shouldEqual "Person(john,38)"
|
||||
}
|
||||
}
|
||||
}
|
||||
"Dynamic execution of inner routes of Directive0" should {
|
||||
"re-execute inner routes every time" in {
|
||||
var a = ""
|
||||
val dynamicRoute = get { a += "x"; complete(a) }
|
||||
def expect(route: Route, s: String) = Get() ~> route ~> check { responseAs[String] shouldEqual s }
|
||||
|
||||
expect(dynamicRoute, "x")
|
||||
expect(dynamicRoute, "xx")
|
||||
expect(dynamicRoute, "xxx")
|
||||
expect(dynamicRoute, "xxxx")
|
||||
}
|
||||
}
|
||||
|
||||
case object MyException extends RuntimeException
|
||||
"Route sealing" should {
|
||||
"catch route execution exceptions" in {
|
||||
Get("/abc") ~> Route.seal {
|
||||
get { ctx ⇒
|
||||
throw MyException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
}
|
||||
}
|
||||
"catch route building exceptions" in {
|
||||
Get("/abc") ~> Route.seal {
|
||||
get {
|
||||
throw MyException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
}
|
||||
}
|
||||
"convert all rejections to responses" in {
|
||||
object MyRejection extends Rejection
|
||||
Get("/abc") ~> Route.seal {
|
||||
get {
|
||||
reject(MyRejection)
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
}
|
||||
}
|
||||
"always prioritize MethodRejections over AuthorizationFailedRejections" in {
|
||||
Get("/abc") ~> Route.seal {
|
||||
post { completeOk } ~
|
||||
authorize(false) { completeOk }
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.MethodNotAllowed
|
||||
responseAs[String] shouldEqual "HTTP method not allowed, supported methods: POST"
|
||||
}
|
||||
|
||||
Get("/abc") ~> Route.seal {
|
||||
authorize(false) { completeOk } ~
|
||||
post { completeOk }
|
||||
} ~> check { status shouldEqual StatusCodes.MethodNotAllowed }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import org.scalatest.{ WordSpec, Suite, Matchers }
|
||||
import akka.http.scaladsl.model.HttpResponse
|
||||
import akka.http.scaladsl.testkit.ScalatestRouteTest
|
||||
|
||||
trait GenericRoutingSpec extends Matchers with Directives with ScalatestRouteTest { this: Suite ⇒
|
||||
val Ok = HttpResponse()
|
||||
val completeOk = complete(Ok)
|
||||
|
||||
def echoComplete[T]: T ⇒ Route = { x ⇒ complete(x.toString) }
|
||||
def echoComplete2[T, U]: (T, U) ⇒ Route = { (x, y) ⇒ complete(s"$x $y") }
|
||||
}
|
||||
|
||||
abstract class RoutingSpec extends WordSpec with GenericRoutingSpec
|
||||
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport
|
||||
import akka.http.scaladsl.server.directives.UserCredentials
|
||||
import com.typesafe.config.{ ConfigFactory, Config }
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.http.scaladsl.Http
|
||||
|
||||
object TestServer extends App {
|
||||
val testConf: Config = ConfigFactory.parseString("""
|
||||
akka.loglevel = INFO
|
||||
akka.log-dead-letters = off""")
|
||||
implicit val system = ActorSystem("ServerTest", testConf)
|
||||
import system.dispatcher
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
|
||||
import ScalaXmlSupport._
|
||||
import Directives._
|
||||
|
||||
def auth: AuthenticatorPF[String] = {
|
||||
case p @ UserCredentials.Provided(name) if p.verifySecret(name + "-password") ⇒ name
|
||||
}
|
||||
|
||||
val bindingFuture = Http().bindAndHandle({
|
||||
get {
|
||||
path("") {
|
||||
complete(index)
|
||||
} ~
|
||||
path("secure") {
|
||||
authenticateBasicPF("My very secure site", auth) { user ⇒
|
||||
complete(<html><body>Hello <b>{ user }</b>. Access has been granted!</body></html>)
|
||||
}
|
||||
} ~
|
||||
path("ping") {
|
||||
complete("PONG!")
|
||||
} ~
|
||||
path("crash") {
|
||||
complete(sys.error("BOOM!"))
|
||||
}
|
||||
}
|
||||
}, interface = "localhost", port = 8080)
|
||||
|
||||
println(s"Server online at http://localhost:8080/\nPress RETURN to stop...")
|
||||
Console.readLine()
|
||||
|
||||
bindingFuture.flatMap(_.unbind()).onComplete(_ ⇒ system.shutdown())
|
||||
|
||||
lazy val index =
|
||||
<html>
|
||||
<body>
|
||||
<h1>Say hello to <i>akka-http-core</i>!</h1>
|
||||
<p>Defined resources:</p>
|
||||
<ul>
|
||||
<li><a href="/ping">/ping</a></li>
|
||||
<li><a href="/secure">/secure</a> Use any username and '<username>-password' as credentials</li>
|
||||
<li><a href="/crash">/crash</a></li>
|
||||
</ul>
|
||||
</body>
|
||||
</html>
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
class BasicDirectivesSpec extends RoutingSpec {
|
||||
|
||||
"The `mapUnmatchedPath` directive" should {
|
||||
"map the unmatched path" in {
|
||||
Get("/abc") ~> {
|
||||
mapUnmatchedPath(_ / "def") {
|
||||
path("abc" / "def") { completeOk }
|
||||
}
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"The `extract` directive" should {
|
||||
"extract from the RequestContext" in {
|
||||
Get("/abc") ~> {
|
||||
extract(_.request.method.value) {
|
||||
echoComplete
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "GET" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,184 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import StatusCodes._
|
||||
import headers._
|
||||
|
||||
class CacheConditionDirectivesSpec extends RoutingSpec {
|
||||
|
||||
"the `conditional` directive" should {
|
||||
val timestamp = DateTime.now - 2000
|
||||
val ifUnmodifiedSince = `If-Unmodified-Since`(timestamp)
|
||||
val ifModifiedSince = `If-Modified-Since`(timestamp)
|
||||
val tag = EntityTag("fresh")
|
||||
val responseHeaders = List(ETag(tag), `Last-Modified`(timestamp))
|
||||
|
||||
def taggedAndTimestamped = conditional(tag, timestamp) { completeOk }
|
||||
def weak = conditional(tag.copy(weak = true), timestamp) { completeOk }
|
||||
|
||||
"return OK for new resources" in {
|
||||
Get() ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
"return OK for non-matching resources" in {
|
||||
Get() ~> `If-None-Match`(EntityTag("old")) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-Modified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(EntityTag("old")) ~> `If-Modified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
"ignore If-Modified-Since if If-None-Match is defined" in {
|
||||
Get() ~> `If-None-Match`(tag) ~> `If-Modified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
}
|
||||
Get() ~> `If-None-Match`(EntityTag("old")) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
}
|
||||
}
|
||||
|
||||
"return PreconditionFailed for matched but unsafe resources" in {
|
||||
Put() ~> `If-None-Match`(tag) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual PreconditionFailed
|
||||
headers shouldEqual Nil
|
||||
}
|
||||
}
|
||||
|
||||
"return NotModified for matching resources" in {
|
||||
Get() ~> `If-None-Match`.`*` ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(tag) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(tag) ~> `If-Modified-Since`(timestamp + 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(tag.copy(weak = true)) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(tag, EntityTag("some"), EntityTag("other")) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
"return NotModified when only one matching header is set" in {
|
||||
Get() ~> `If-None-Match`.`*` ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> `If-None-Match`(tag) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Get() ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
"return NotModified for matching weak resources" in {
|
||||
val weakTag = tag.copy(weak = true)
|
||||
Get() ~> `If-None-Match`(tag) ~> weak ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (List(ETag(weakTag), `Last-Modified`(timestamp)))
|
||||
}
|
||||
Get() ~> `If-None-Match`(weakTag) ~> weak ~> check {
|
||||
status shouldEqual NotModified
|
||||
headers should contain theSameElementsAs (List(ETag(weakTag), `Last-Modified`(timestamp)))
|
||||
}
|
||||
}
|
||||
|
||||
"return normally for matching If-Match/If-Unmodified" in {
|
||||
Put() ~> `If-Match`.`*` ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Put() ~> `If-Match`(tag) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
Put() ~> ifUnmodifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
headers should contain theSameElementsAs (responseHeaders)
|
||||
}
|
||||
}
|
||||
|
||||
"return PreconditionFailed for non-matching If-Match/If-Unmodified" in {
|
||||
Put() ~> `If-Match`(EntityTag("old")) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual PreconditionFailed
|
||||
headers shouldEqual Nil
|
||||
}
|
||||
Put() ~> `If-Unmodified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual PreconditionFailed
|
||||
headers shouldEqual Nil
|
||||
}
|
||||
}
|
||||
|
||||
"ignore If-Unmodified-Since if If-Match is defined" in {
|
||||
Put() ~> `If-Match`(tag) ~> `If-Unmodified-Since`(timestamp - 1000) ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual OK
|
||||
}
|
||||
Put() ~> `If-Match`(EntityTag("old")) ~> ifModifiedSince ~> taggedAndTimestamped ~> check {
|
||||
status shouldEqual PreconditionFailed
|
||||
}
|
||||
}
|
||||
|
||||
"not filter out a `Range` header if `If-Range` does match the timestamp" in {
|
||||
Get() ~> `If-Range`(timestamp) ~> Range(ByteRange(0, 10)) ~> {
|
||||
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] should startWith("Some")
|
||||
}
|
||||
}
|
||||
|
||||
"filter out a `Range` header if `If-Range` doesn't match the timestamp" in {
|
||||
Get() ~> `If-Range`(timestamp - 1000) ~> Range(ByteRange(0, 10)) ~> {
|
||||
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] shouldEqual "None"
|
||||
}
|
||||
}
|
||||
|
||||
"not filter out a `Range` header if `If-Range` does match the ETag" in {
|
||||
Get() ~> `If-Range`(tag) ~> Range(ByteRange(0, 10)) ~> {
|
||||
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] should startWith("Some")
|
||||
}
|
||||
}
|
||||
|
||||
"filter out a `Range` header if `If-Range` doesn't match the ETag" in {
|
||||
Get() ~> `If-Range`(EntityTag("other")) ~> Range(ByteRange(0, 10)) ~> {
|
||||
(conditional(tag, timestamp) & optionalHeaderValueByType[Range]()) { echoComplete }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
responseAs[String] shouldEqual "None"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,413 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import org.scalatest.matchers.Matcher
|
||||
import akka.util.ByteString
|
||||
import akka.stream.scaladsl.Source
|
||||
import akka.http.impl.util._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.coding._
|
||||
import headers._
|
||||
import HttpEntity.{ ChunkStreamPart, Chunk }
|
||||
import HttpCharsets._
|
||||
import HttpEncodings._
|
||||
import MediaTypes._
|
||||
import StatusCodes._
|
||||
|
||||
import scala.concurrent.duration._
|
||||
|
||||
class CodingDirectivesSpec extends RoutingSpec {
|
||||
|
||||
val echoRequestContent: Route = { ctx ⇒ ctx.complete(ctx.request.entity.dataBytes.utf8String) }
|
||||
|
||||
val yeah = complete("Yeah!")
|
||||
lazy val yeahGzipped = compress("Yeah!", Gzip)
|
||||
lazy val yeahDeflated = compress("Yeah!", Deflate)
|
||||
|
||||
lazy val helloGzipped = compress("Hello", Gzip)
|
||||
lazy val helloDeflated = compress("Hello", Deflate)
|
||||
|
||||
"the NoEncoding decoder" should {
|
||||
"decode the request content if it has encoding 'identity'" in {
|
||||
Post("/", "yes") ~> `Content-Encoding`(identity) ~> {
|
||||
decodeRequestWith(NoCoding) { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"reject requests with content encoded with 'deflate'" in {
|
||||
Post("/", "yes") ~> `Content-Encoding`(deflate) ~> {
|
||||
decodeRequestWith(NoCoding) { echoRequestContent }
|
||||
} ~> check { rejection shouldEqual UnsupportedRequestEncodingRejection(identity) }
|
||||
}
|
||||
"decode the request content if no Content-Encoding header is present" in {
|
||||
Post("/", "yes") ~> decodeRequestWith(NoCoding) { echoRequestContent } ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"leave request without content unchanged" in {
|
||||
Post() ~> decodeRequestWith(Gzip) { completeOk } ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"the Gzip decoder" should {
|
||||
"decode the request content if it has encoding 'gzip'" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeRequestWith(Gzip) { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "Hello" }
|
||||
}
|
||||
"reject the request content if it has encoding 'gzip' but is corrupt" in {
|
||||
Post("/", fromHexDump("000102")) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeRequestWith(Gzip) { echoRequestContent }
|
||||
} ~> check {
|
||||
status shouldEqual BadRequest
|
||||
responseAs[String] shouldEqual "The request's encoding is corrupt"
|
||||
}
|
||||
}
|
||||
"reject truncated gzip request content" in {
|
||||
Post("/", helloGzipped.dropRight(2)) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeRequestWith(Gzip) { echoRequestContent }
|
||||
} ~> check {
|
||||
status shouldEqual BadRequest
|
||||
responseAs[String] shouldEqual "The request's encoding is corrupt"
|
||||
}
|
||||
}
|
||||
"reject requests with content encoded with 'deflate'" in {
|
||||
Post("/", "Hello") ~> `Content-Encoding`(deflate) ~> {
|
||||
decodeRequestWith(Gzip) { completeOk }
|
||||
} ~> check { rejection shouldEqual UnsupportedRequestEncodingRejection(gzip) }
|
||||
}
|
||||
"reject requests without Content-Encoding header" in {
|
||||
Post("/", "Hello") ~> {
|
||||
decodeRequestWith(Gzip) { completeOk }
|
||||
} ~> check { rejection shouldEqual UnsupportedRequestEncodingRejection(gzip) }
|
||||
}
|
||||
"leave request without content unchanged" in {
|
||||
Post() ~> {
|
||||
decodeRequestWith(Gzip) { completeOk }
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"a (decodeRequestWith(Gzip) | decodeRequestWith(NoEncoding)) compound directive" should {
|
||||
lazy val decodeWithGzipOrNoEncoding = decodeRequestWith(Gzip) | decodeRequestWith(NoCoding)
|
||||
"decode the request content if it has encoding 'gzip'" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeWithGzipOrNoEncoding { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "Hello" }
|
||||
}
|
||||
"decode the request content if it has encoding 'identity'" in {
|
||||
Post("/", "yes") ~> `Content-Encoding`(identity) ~> {
|
||||
decodeWithGzipOrNoEncoding { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"decode the request content if no Content-Encoding header is present" in {
|
||||
Post("/", "yes") ~> decodeWithGzipOrNoEncoding { echoRequestContent } ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"reject requests with content encoded with 'deflate'" in {
|
||||
Post("/", "yes") ~> `Content-Encoding`(deflate) ~> {
|
||||
decodeWithGzipOrNoEncoding { echoRequestContent }
|
||||
} ~> check {
|
||||
rejections shouldEqual Seq(
|
||||
UnsupportedRequestEncodingRejection(gzip),
|
||||
UnsupportedRequestEncodingRejection(identity))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the Gzip encoder" should {
|
||||
"encode the response content with GZIP if the client accepts it with a dedicated Accept-Encoding header" in {
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"encode the response content with GZIP if the request has no Accept-Encoding header" in {
|
||||
Post() ~> {
|
||||
encodeResponseWith(Gzip) { yeah }
|
||||
} ~> check { strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped) }
|
||||
}
|
||||
"reject the request if the client does not accept GZIP encoding" in {
|
||||
Post() ~> `Accept-Encoding`(identity) ~> {
|
||||
encodeResponseWith(Gzip) { completeOk }
|
||||
} ~> check { rejection shouldEqual UnacceptedResponseEncodingRejection(gzip) }
|
||||
}
|
||||
"leave responses without content unchanged" in {
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"leave responses with an already set Content-Encoding header unchanged" in {
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) {
|
||||
RespondWithDirectives.respondWithHeader(`Content-Encoding`(identity)) { completeOk }
|
||||
}
|
||||
} ~> check { response shouldEqual Ok.withHeaders(`Content-Encoding`(identity)) }
|
||||
}
|
||||
"correctly encode the chunk stream produced by a chunked response" in {
|
||||
val text = "This is a somewhat lengthy text that is being chunked by the autochunk directive!"
|
||||
val textChunks =
|
||||
() ⇒ text.grouped(8).map { chars ⇒
|
||||
Chunk(chars.mkString): ChunkStreamPart
|
||||
}
|
||||
val chunkedTextEntity = HttpEntity.Chunked(MediaTypes.`text/plain`, Source(textChunks))
|
||||
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) {
|
||||
complete(chunkedTextEntity)
|
||||
}
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
chunks.size shouldEqual (11 + 1) // 11 regular + the last one
|
||||
val bytes = chunks.foldLeft(ByteString.empty)(_ ++ _.data)
|
||||
Gzip.decode(bytes).awaitResult(1.second) should readAs(text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the encodeResponseWith(NoEncoding) directive" should {
|
||||
"produce a response if no Accept-Encoding is present in the request" in {
|
||||
Post() ~> encodeResponseWith(NoCoding) { completeOk } ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"produce a not encoded response if the client only accepts non matching encodings" in {
|
||||
Post() ~> `Accept-Encoding`(gzip, identity) ~> {
|
||||
encodeResponseWith(NoCoding) { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
|
||||
Post() ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Deflate, NoCoding) { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"reject the request if the request has an 'Accept-Encoding: identity; q=0' header" in {
|
||||
Post() ~> `Accept-Encoding`(identity.withQValue(0f)) ~> {
|
||||
encodeResponseWith(NoCoding) { completeOk }
|
||||
} ~> check { rejection shouldEqual UnacceptedResponseEncodingRejection(identity) }
|
||||
}
|
||||
}
|
||||
|
||||
"a (encodeResponse(Gzip) | encodeResponse(NoEncoding)) compound directive" should {
|
||||
lazy val encodeGzipOrIdentity = encodeResponseWith(Gzip) | encodeResponseWith(NoCoding)
|
||||
"produce a not encoded response if the request has no Accept-Encoding header" in {
|
||||
Post() ~> {
|
||||
encodeGzipOrIdentity { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"produce a GZIP encoded response if the request has an `Accept-Encoding: deflate;q=0.5, gzip` header" in {
|
||||
Post() ~> `Accept-Encoding`(deflate.withQValue(.5f), gzip) ~> {
|
||||
encodeGzipOrIdentity { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"produce a non-encoded response if the request has an `Accept-Encoding: identity` header" in {
|
||||
Post() ~> `Accept-Encoding`(identity) ~> {
|
||||
encodeGzipOrIdentity { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"produce a non-encoded response if the request has an `Accept-Encoding: deflate` header" in {
|
||||
Post() ~> `Accept-Encoding`(deflate) ~> {
|
||||
encodeGzipOrIdentity { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the encodeResponse directive" should {
|
||||
"produce a non-encoded response if the request has no Accept-Encoding header" in {
|
||||
Get("/") ~> {
|
||||
encodeResponse { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"produce a GZIP encoded response if the request has an `Accept-Encoding: gzip, deflate` header" in {
|
||||
Get("/") ~> `Accept-Encoding`(gzip, deflate) ~> {
|
||||
encodeResponse { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"produce a Deflate encoded response if the request has an `Accept-Encoding: deflate` header" in {
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> {
|
||||
encodeResponse { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(deflate)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahDeflated)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the encodeResponseWith directive" should {
|
||||
"produce a response encoded with the specified Encoder if the request has a matching Accept-Encoding header" in {
|
||||
Get("/") ~> `Accept-Encoding`(gzip) ~> {
|
||||
encodeResponseWith(Gzip) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"produce a response encoded with one of the specified Encoders if the request has a matching Accept-Encoding header" in {
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> {
|
||||
encodeResponseWith(Gzip, Deflate) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(deflate)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahDeflated)
|
||||
}
|
||||
}
|
||||
"produce a response encoded with the first of the specified Encoders if the request has no Accept-Encoding header" in {
|
||||
Get("/") ~> {
|
||||
encodeResponseWith(Gzip, Deflate) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"produce a response with no encoding if the request has an empty Accept-Encoding header" in {
|
||||
Get("/") ~> `Accept-Encoding`() ~> {
|
||||
encodeResponseWith(Gzip, Deflate, NoCoding) { completeOk }
|
||||
} ~> check {
|
||||
response shouldEqual Ok
|
||||
response should haveNoContentEncoding
|
||||
}
|
||||
}
|
||||
"negotiate the correct content encoding" in {
|
||||
Get("/") ~> `Accept-Encoding`(identity.withQValue(.5f), deflate.withQValue(0f), gzip) ~> {
|
||||
encodeResponseWith(NoCoding, Deflate, Gzip) { yeah }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), yeahGzipped)
|
||||
}
|
||||
}
|
||||
"reject the request if it has an Accept-Encoding header with an encoding that doesn't match" in {
|
||||
Get("/") ~> `Accept-Encoding`(deflate) ~> {
|
||||
encodeResponseWith(Gzip) { yeah }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
"reject the request if it has an Accept-Encoding header with an encoding that matches but is blacklisted" in {
|
||||
Get("/") ~> `Accept-Encoding`(gzip.withQValue(0f)) ~> {
|
||||
encodeResponseWith(Gzip) { yeah }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnacceptedResponseEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the decodeRequest directive" should {
|
||||
"decode the request content if it has a `Content-Encoding: gzip` header and the content is gzip encoded" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeRequest { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "Hello" }
|
||||
}
|
||||
"decode the request content if it has a `Content-Encoding: deflate` header and the content is deflate encoded" in {
|
||||
Post("/", helloDeflated) ~> `Content-Encoding`(deflate) ~> {
|
||||
decodeRequest { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "Hello" }
|
||||
}
|
||||
"decode the request content if it has a `Content-Encoding: identity` header and the content is not encoded" in {
|
||||
Post("/", "yes") ~> `Content-Encoding`(identity) ~> {
|
||||
decodeRequest { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"decode the request content using NoEncoding if no Content-Encoding header is present" in {
|
||||
Post("/", "yes") ~> decodeRequest { echoRequestContent } ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"reject the request if it has a `Content-Encoding: deflate` header but the request is encoded with Gzip" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(deflate) ~>
|
||||
decodeRequest { echoRequestContent } ~> check {
|
||||
status shouldEqual BadRequest
|
||||
responseAs[String] shouldEqual "The request's encoding is corrupt"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the decodeRequestWith directive" should {
|
||||
"decode the request content if its `Content-Encoding` header matches the specified encoder" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeRequestWith(Gzip) { echoRequestContent }
|
||||
} ~> check { responseAs[String] shouldEqual "Hello" }
|
||||
}
|
||||
"reject the request if its `Content-Encoding` header doesn't match the specified encoder" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(deflate) ~> {
|
||||
decodeRequestWith(Gzip) { echoRequestContent }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
"reject the request when decodeing with GZIP and no Content-Encoding header is present" in {
|
||||
Post("/", "yes") ~> decodeRequestWith(Gzip) { echoRequestContent } ~> check {
|
||||
rejection shouldEqual UnsupportedRequestEncodingRejection(gzip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"the (decodeRequest & encodeResponse) compound directive" should {
|
||||
lazy val decodeEncode = decodeRequest & encodeResponse
|
||||
"decode a GZIP encoded request and produce a none encoded response if the request has no Accept-Encoding header" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> {
|
||||
decodeEncode { echoRequestContent }
|
||||
} ~> check {
|
||||
response should haveNoContentEncoding
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), "Hello")
|
||||
}
|
||||
}
|
||||
"decode a GZIP encoded request and produce a Deflate encoded response if the request has an `Accept-Encoding: deflate` header" in {
|
||||
Post("/", helloGzipped) ~> `Content-Encoding`(gzip) ~> `Accept-Encoding`(deflate) ~> {
|
||||
decodeEncode { echoRequestContent }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(deflate)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), helloDeflated)
|
||||
}
|
||||
}
|
||||
"decode an unencoded request and produce a GZIP encoded response if the request has an `Accept-Encoding: gzip` header" in {
|
||||
Post("/", "Hello") ~> `Accept-Encoding`(gzip) ~> {
|
||||
decodeEncode { echoRequestContent }
|
||||
} ~> check {
|
||||
response should haveContentEncoding(gzip)
|
||||
strictify(responseEntity) shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), helloGzipped)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def compress(input: String, encoder: Encoder): ByteString = {
|
||||
val compressor = encoder.newCompressor
|
||||
compressor.compressAndFlush(ByteString(input)) ++ compressor.finish()
|
||||
}
|
||||
|
||||
def hexDump(bytes: Array[Byte]) = bytes.map("%02x" format _).mkString
|
||||
def fromHexDump(dump: String) = dump.grouped(2).toArray.map(chars ⇒ Integer.parseInt(new String(chars), 16).toByte)
|
||||
|
||||
def haveNoContentEncoding: Matcher[HttpResponse] = be(None) compose { (_: HttpResponse).header[`Content-Encoding`] }
|
||||
def haveContentEncoding(encoding: HttpEncoding): Matcher[HttpResponse] =
|
||||
be(Some(`Content-Encoding`(encoding))) compose { (_: HttpResponse).header[`Content-Encoding`] }
|
||||
|
||||
def readAs(string: String, charset: String = "UTF8") = be(string) compose { (_: ByteString).decodeString(charset) }
|
||||
|
||||
def strictify(entity: HttpEntity) = entity.toStrict(1.second).awaitResult(1.second)
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import StatusCodes.OK
|
||||
import headers._
|
||||
|
||||
class CookieDirectivesSpec extends RoutingSpec {
|
||||
|
||||
val deletedTimeStamp = DateTime.fromIsoDateTimeString("1800-01-01T00:00:00")
|
||||
|
||||
"The 'cookie' directive" should {
|
||||
"extract the respectively named cookie" in {
|
||||
Get() ~> addHeader(Cookie(HttpCookie("fancy", "pants"))) ~> {
|
||||
cookie("fancy") { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "fancy=pants" }
|
||||
}
|
||||
"reject the request if the cookie is not present" in {
|
||||
Get() ~> {
|
||||
cookie("fancy") { echoComplete }
|
||||
} ~> check { rejection shouldEqual MissingCookieRejection("fancy") }
|
||||
}
|
||||
"properly pass through inner rejections" in {
|
||||
Get() ~> addHeader(Cookie(HttpCookie("fancy", "pants"))) ~> {
|
||||
cookie("fancy") { c ⇒ reject(ValidationRejection("Dont like " + c.content)) }
|
||||
} ~> check { rejection shouldEqual ValidationRejection("Dont like pants") }
|
||||
}
|
||||
}
|
||||
|
||||
"The 'deleteCookie' directive" should {
|
||||
"add a respective Set-Cookie headers to successful responses" in {
|
||||
Get() ~> {
|
||||
deleteCookie("myCookie", "test.com") { completeOk }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
header[`Set-Cookie`] shouldEqual Some(`Set-Cookie`(HttpCookie("myCookie", "deleted", expires = deletedTimeStamp,
|
||||
domain = Some("test.com"))))
|
||||
}
|
||||
}
|
||||
|
||||
"support deleting multiple cookies at a time" in {
|
||||
Get() ~> {
|
||||
deleteCookie(HttpCookie("myCookie", "test.com"), HttpCookie("myCookie2", "foobar.com")) { completeOk }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
headers.collect { case `Set-Cookie`(x) ⇒ x } shouldEqual List(
|
||||
HttpCookie("myCookie", "deleted", expires = deletedTimeStamp),
|
||||
HttpCookie("myCookie2", "deleted", expires = deletedTimeStamp))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'optionalCookie' directive" should {
|
||||
"produce a `Some(cookie)` extraction if the cookie is present" in {
|
||||
Get() ~> Cookie(HttpCookie("abc", "123")) ~> {
|
||||
optionalCookie("abc") { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Some(abc=123)" }
|
||||
}
|
||||
"produce a `None` extraction if the cookie is not present" in {
|
||||
Get() ~> optionalCookie("abc") { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"let rejections from its inner route pass through" in {
|
||||
Get() ~> {
|
||||
optionalCookie("test-cookie") { _ ⇒
|
||||
validate(false, "ouch") { completeOk }
|
||||
}
|
||||
} ~> check { rejection shouldEqual ValidationRejection("ouch") }
|
||||
}
|
||||
}
|
||||
|
||||
"The 'setCookie' directive" should {
|
||||
"add a respective Set-Cookie headers to successful responses" in {
|
||||
Get() ~> {
|
||||
setCookie(HttpCookie("myCookie", "test.com")) { completeOk }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
header[`Set-Cookie`] shouldEqual Some(`Set-Cookie`(HttpCookie("myCookie", "test.com")))
|
||||
}
|
||||
}
|
||||
|
||||
"support setting multiple cookies at a time" in {
|
||||
Get() ~> {
|
||||
setCookie(HttpCookie("myCookie", "test.com"), HttpCookie("myCookie2", "foobar.com")) { completeOk }
|
||||
} ~> check {
|
||||
status shouldEqual OK
|
||||
headers.collect { case `Set-Cookie`(x) ⇒ x } shouldEqual List(
|
||||
HttpCookie("myCookie", "test.com"), HttpCookie("myCookie2", "foobar.com"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.event.LoggingAdapter
|
||||
import akka.http.impl.util._
|
||||
|
||||
class DebuggingDirectivesSpec extends RoutingSpec {
|
||||
var debugMsg = ""
|
||||
|
||||
def resetDebugMsg(): Unit = { debugMsg = "" }
|
||||
|
||||
val log = new LoggingAdapter {
|
||||
def isErrorEnabled = true
|
||||
def isWarningEnabled = true
|
||||
def isInfoEnabled = true
|
||||
def isDebugEnabled = true
|
||||
|
||||
def notifyError(message: String): Unit = {}
|
||||
def notifyError(cause: Throwable, message: String): Unit = {}
|
||||
def notifyWarning(message: String): Unit = {}
|
||||
def notifyInfo(message: String): Unit = {}
|
||||
def notifyDebug(message: String): Unit = { debugMsg += message + '\n' }
|
||||
}
|
||||
|
||||
"The 'logRequest' directive" should {
|
||||
"produce a proper log message for incoming requests" in {
|
||||
val route =
|
||||
withLog(log)(
|
||||
logRequest("1")(
|
||||
completeOk))
|
||||
|
||||
resetDebugMsg()
|
||||
Get("/hello") ~> route ~> check {
|
||||
response shouldEqual Ok
|
||||
debugMsg shouldEqual "1: HttpRequest(HttpMethod(GET),http://example.com/hello,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1))\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'logResponse' directive" should {
|
||||
"produce a proper log message for outgoing responses" in {
|
||||
val route =
|
||||
withLog(log)(
|
||||
logResult("2")(
|
||||
completeOk))
|
||||
|
||||
resetDebugMsg()
|
||||
Get("/hello") ~> route ~> check {
|
||||
response shouldEqual Ok
|
||||
debugMsg shouldEqual "2: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1)))\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'logRequestResponse' directive" should {
|
||||
"produce proper log messages for outgoing responses, thereby showing the corresponding request" in {
|
||||
val route =
|
||||
withLog(log)(
|
||||
logRequestResult("3")(
|
||||
completeOk))
|
||||
|
||||
resetDebugMsg()
|
||||
Get("/hello") ~> route ~> check {
|
||||
response shouldEqual Ok
|
||||
debugMsg shouldEqual """|3: Response for
|
||||
| Request : HttpRequest(HttpMethod(GET),http://example.com/hello,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1))
|
||||
| Response: Complete(HttpResponse(200 OK,List(),HttpEntity.Strict(none/none,ByteString()),HttpProtocol(HTTP/1.1)))
|
||||
|""".stripMarginWithNewline("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.{ MediaTypes, MediaRanges, StatusCodes }
|
||||
import akka.http.scaladsl.model.headers._
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class ExecutionDirectivesSpec extends RoutingSpec {
|
||||
object MyException extends RuntimeException
|
||||
val handler =
|
||||
ExceptionHandler {
|
||||
case MyException ⇒ complete(500, "Pling! Plong! Something went wrong!!!")
|
||||
}
|
||||
|
||||
"The `handleExceptions` directive" should {
|
||||
"handle an exception strictly thrown in the inner route with the supplied exception handler" in {
|
||||
exceptionShouldBeHandled {
|
||||
handleExceptions(handler) { ctx ⇒
|
||||
throw MyException
|
||||
}
|
||||
}
|
||||
}
|
||||
"handle an Future.failed RouteResult with the supplied exception handler" in {
|
||||
exceptionShouldBeHandled {
|
||||
handleExceptions(handler) { ctx ⇒
|
||||
Future.failed(MyException)
|
||||
}
|
||||
}
|
||||
}
|
||||
"handle an eventually failed Future[RouteResult] with the supplied exception handler" in {
|
||||
exceptionShouldBeHandled {
|
||||
handleExceptions(handler) { ctx ⇒
|
||||
Future {
|
||||
Thread.sleep(100)
|
||||
throw MyException
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"handle an exception happening during route building" in {
|
||||
exceptionShouldBeHandled {
|
||||
get {
|
||||
handleExceptions(handler) {
|
||||
throw MyException
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"not interfere with alternative routes" in {
|
||||
Get("/abc") ~>
|
||||
get {
|
||||
handleExceptions(handler)(reject) ~ { ctx ⇒
|
||||
throw MyException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
"not handle other exceptions" in {
|
||||
Get("/abc") ~>
|
||||
get {
|
||||
handleExceptions(handler) {
|
||||
throw new RuntimeException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
"always fall back to a default content type" in {
|
||||
Get("/abc") ~> Accept(MediaTypes.`application/json`) ~>
|
||||
get {
|
||||
handleExceptions(handler) {
|
||||
throw new RuntimeException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
|
||||
Get("/abc") ~> Accept(MediaTypes.`text/xml`, MediaRanges.`*/*`.withQValue(0f)) ~>
|
||||
get {
|
||||
handleExceptions(handler) {
|
||||
throw new RuntimeException
|
||||
}
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def exceptionShouldBeHandled(route: Route) =
|
||||
Get("/abc") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "Pling! Plong! Something went wrong!!!"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,370 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import java.io.{ File, FileOutputStream }
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ ExecutionContext, Future }
|
||||
import scala.util.Properties
|
||||
import org.scalatest.matchers.Matcher
|
||||
import org.scalatest.{ Inside, Inspectors }
|
||||
import akka.http.scaladsl.model.MediaTypes._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.impl.util._
|
||||
|
||||
class FileAndResourceDirectivesSpec extends RoutingSpec with Inspectors with Inside {
|
||||
|
||||
override def testConfigSource =
|
||||
"""akka.http.scaladsl.routing {
|
||||
| file-chunking-threshold-size = 16
|
||||
| file-chunking-chunk-size = 8
|
||||
| range-coalescing-threshold = 1
|
||||
|}""".stripMargin
|
||||
|
||||
"getFromFile" should {
|
||||
"reject non-GET requests" in {
|
||||
Put() ~> getFromFile("some") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to non-existing files" in {
|
||||
Get() ~> getFromFile("nonExistentFile") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directories" in {
|
||||
Get() ~> getFromFile(Properties.javaHome) ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"return the file content with the MediaType matching the file extension" in {
|
||||
val file = File.createTempFile("akkaHttpTest", ".PDF")
|
||||
try {
|
||||
writeAllText("This is PDF", file)
|
||||
Get() ~> getFromFile(file.getPath) ~> check {
|
||||
mediaType shouldEqual `application/pdf`
|
||||
definedCharset shouldEqual None
|
||||
responseAs[String] shouldEqual "This is PDF"
|
||||
headers should contain(`Last-Modified`(DateTime(file.lastModified)))
|
||||
}
|
||||
} finally file.delete
|
||||
}
|
||||
"return the file content with MediaType 'application/octet-stream' on unknown file extensions" in {
|
||||
val file = File.createTempFile("akkaHttpTest", null)
|
||||
try {
|
||||
writeAllText("Some content", file)
|
||||
Get() ~> getFromFile(file) ~> check {
|
||||
mediaType shouldEqual `application/octet-stream`
|
||||
responseAs[String] shouldEqual "Some content"
|
||||
}
|
||||
} finally file.delete
|
||||
}
|
||||
|
||||
"return a single range from a file" in {
|
||||
val file = File.createTempFile("partialTest", null)
|
||||
try {
|
||||
writeAllText("ABCDEFGHIJKLMNOPQRSTUVWXYZ", file)
|
||||
Get() ~> addHeader(Range(ByteRange(0, 10))) ~> getFromFile(file) ~> check {
|
||||
status shouldEqual StatusCodes.PartialContent
|
||||
headers should contain(`Content-Range`(ContentRange(0, 10, 26)))
|
||||
responseAs[String] shouldEqual "ABCDEFGHIJK"
|
||||
}
|
||||
} finally file.delete
|
||||
}
|
||||
|
||||
"return multiple ranges from a file at once" in {
|
||||
pending // FIXME: reactivate
|
||||
val file = File.createTempFile("partialTest", null)
|
||||
try {
|
||||
writeAllText("ABCDEFGHIJKLMNOPQRSTUVWXYZ", file)
|
||||
val rangeHeader = Range(ByteRange(1, 10), ByteRange.suffix(10))
|
||||
Get() ~> addHeader(rangeHeader) ~> getFromFile(file, ContentTypes.`text/plain`) ~> check {
|
||||
status shouldEqual StatusCodes.PartialContent
|
||||
header[`Content-Range`] shouldEqual None
|
||||
mediaType.withParams(Map.empty) shouldEqual `multipart/byteranges`
|
||||
|
||||
val parts = responseAs[Multipart.ByteRanges].toStrict(1.second).awaitResult(3.seconds).strictParts
|
||||
parts.size shouldEqual 2
|
||||
parts(0).entity.data.utf8String shouldEqual "BCDEFGHIJK"
|
||||
parts(1).entity.data.utf8String shouldEqual "QRSTUVWXYZ"
|
||||
}
|
||||
} finally file.delete
|
||||
}
|
||||
}
|
||||
|
||||
"getFromResource" should {
|
||||
"reject non-GET requests" in {
|
||||
Put() ~> getFromResource("some") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to non-existing resources" in {
|
||||
Get() ~> getFromResource("nonExistingResource") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources" in {
|
||||
Get() ~> getFromResource("someDir") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources with trailing slash" in {
|
||||
Get() ~> getFromResource("someDir/") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources from an Archive " in {
|
||||
Get() ~> getFromResource("com/typesafe/config") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources from an Archive with trailing slash" in {
|
||||
Get() ~> getFromResource("com/typesafe/config/") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"return the resource content with the MediaType matching the file extension" in {
|
||||
val route = getFromResource("sample.html")
|
||||
|
||||
def runCheck() =
|
||||
Get() ~> route ~> check {
|
||||
mediaType shouldEqual `text/html`
|
||||
forAtLeast(1, headers) { h ⇒
|
||||
inside(h) {
|
||||
case `Last-Modified`(dt) ⇒
|
||||
DateTime(2011, 7, 1) should be < dt
|
||||
dt.clicks should be < System.currentTimeMillis()
|
||||
}
|
||||
}
|
||||
responseAs[String] shouldEqual "<p>Lorem ipsum!</p>"
|
||||
}
|
||||
|
||||
runCheck()
|
||||
runCheck() // additional test to check that no internal state is kept
|
||||
}
|
||||
"return the resource content from an Archive" in {
|
||||
Get() ~> getFromResource("com/typesafe/config/Config.class") ~> check {
|
||||
mediaType shouldEqual `application/octet-stream`
|
||||
responseEntity.toStrict(1.second).awaitResult(1.second).data.asByteBuffer.getInt shouldEqual 0xCAFEBABE
|
||||
}
|
||||
}
|
||||
"return the file content with MediaType 'application/octet-stream' on unknown file extensions" in {
|
||||
Get() ~> getFromResource("sample.xyz") ~> check {
|
||||
mediaType shouldEqual `application/octet-stream`
|
||||
responseAs[String] shouldEqual "XyZ"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"getFromResourceDirectory" should {
|
||||
"reject requests to non-existing resources" in {
|
||||
Get("not/found") ~> getFromResourceDirectory("subDirectory") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
val verify = check {
|
||||
mediaType shouldEqual `application/pdf`
|
||||
responseAs[String] shouldEqual "123"
|
||||
}
|
||||
"return the resource content with the MediaType matching the file extension - example 1" in {
|
||||
Get("empty.pdf") ~> getFromResourceDirectory("subDirectory") ~> verify
|
||||
}
|
||||
"return the resource content with the MediaType matching the file extension - example 2" in {
|
||||
Get("empty.pdf") ~> getFromResourceDirectory("subDirectory/") ~> verify
|
||||
}
|
||||
"return the resource content with the MediaType matching the file extension - example 3" in {
|
||||
Get("subDirectory/empty.pdf") ~> getFromResourceDirectory("") ~> verify
|
||||
}
|
||||
"return the resource content from an Archive" in {
|
||||
Get("Config.class") ~> getFromResourceDirectory("com/typesafe/config") ~> check {
|
||||
mediaType shouldEqual `application/octet-stream`
|
||||
responseEntity.toStrict(1.second).awaitResult(1.second).data.asByteBuffer.getInt shouldEqual 0xCAFEBABE
|
||||
}
|
||||
}
|
||||
"reject requests to directory resources" in {
|
||||
Get() ~> getFromResourceDirectory("subDirectory") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources with trailing slash" in {
|
||||
Get() ~> getFromResourceDirectory("subDirectory/") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to sub directory resources" in {
|
||||
Get("sub") ~> getFromResourceDirectory("someDir") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to sub directory resources with trailing slash" in {
|
||||
Get("sub/") ~> getFromResourceDirectory("someDir") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources from an Archive" in {
|
||||
Get() ~> getFromResourceDirectory("com/typesafe/config") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
"reject requests to directory resources from an Archive with trailing slash" in {
|
||||
Get() ~> getFromResourceDirectory("com/typesafe/config/") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
}
|
||||
|
||||
"listDirectoryContents" should {
|
||||
val base = new File(getClass.getClassLoader.getResource("").toURI).getPath
|
||||
new File(base, "subDirectory/emptySub").mkdir()
|
||||
def eraseDateTime(s: String) = s.replaceAll("""\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d""", "xxxx-xx-xx xx:xx:xx")
|
||||
implicit val settings = RoutingSettings.default.copy(renderVanityFooter = false)
|
||||
|
||||
"properly render a simple directory" in {
|
||||
Get() ~> listDirectoryContents(base + "/someDir") ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/sub/">sub/</a> xxxx-xx-xx xx:xx:xx
|
||||
|<a href="/fileA.txt">fileA.txt</a> xxxx-xx-xx xx:xx:xx 3 B
|
||||
|<a href="/fileB.xml">fileB.xml</a> xxxx-xx-xx xx:xx:xx 0 B
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render a sub directory" in {
|
||||
Get("/sub/") ~> listDirectoryContents(base + "/someDir") ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /sub/</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /sub/</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/">../</a>
|
||||
|<a href="/sub/file.html">file.html</a> xxxx-xx-xx xx:xx:xx 0 B
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render the union of several directories" in {
|
||||
Get() ~> listDirectoryContents(base + "/someDir", base + "/subDirectory") ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/emptySub/">emptySub/</a> xxxx-xx-xx xx:xx:xx
|
||||
|<a href="/sub/">sub/</a> xxxx-xx-xx xx:xx:xx
|
||||
|<a href="/empty.pdf">empty.pdf</a> xxxx-xx-xx xx:xx:xx 3 B
|
||||
|<a href="/fileA.txt">fileA.txt</a> xxxx-xx-xx xx:xx:xx 3 B
|
||||
|<a href="/fileB.xml">fileB.xml</a> xxxx-xx-xx xx:xx:xx 0 B
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render an empty sub directory with vanity footer" in {
|
||||
val settings = 0 // shadow implicit
|
||||
Get("/emptySub/") ~> listDirectoryContents(base + "/subDirectory") ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /emptySub/</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /emptySub/</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/">../</a>
|
||||
|</pre>
|
||||
|<hr>
|
||||
|<div style="width:100%;text-align:right;color:gray">
|
||||
|<small>rendered by <a href="http://akka.io">Akka Http</a> on xxxx-xx-xx xx:xx:xx</small>
|
||||
|</div>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render an empty top-level directory" in {
|
||||
Get() ~> listDirectoryContents(base + "/subDirectory/emptySub") ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|(no files)
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render a simple directory with a path prefix" in {
|
||||
Get("/files/") ~> pathPrefix("files")(listDirectoryContents(base + "/someDir")) ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /files/</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /files/</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/files/sub/">sub/</a> xxxx-xx-xx xx:xx:xx
|
||||
|<a href="/files/fileA.txt">fileA.txt</a> xxxx-xx-xx xx:xx:xx 3 B
|
||||
|<a href="/files/fileB.xml">fileB.xml</a> xxxx-xx-xx xx:xx:xx 0 B
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render a sub directory with a path prefix" in {
|
||||
Get("/files/sub/") ~> pathPrefix("files")(listDirectoryContents(base + "/someDir")) ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /files/sub/</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /files/sub/</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|<a href="/files/">../</a>
|
||||
|<a href="/files/sub/file.html">file.html</a> xxxx-xx-xx xx:xx:xx 0 B
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"properly render an empty top-level directory with a path prefix" in {
|
||||
Get("/files/") ~> pathPrefix("files")(listDirectoryContents(base + "/subDirectory/emptySub")) ~> check {
|
||||
eraseDateTime(responseAs[String]) shouldEqual prep {
|
||||
"""<html>
|
||||
|<head><title>Index of /files/</title></head>
|
||||
|<body>
|
||||
|<h1>Index of /files/</h1>
|
||||
|<hr>
|
||||
|<pre>
|
||||
|(no files)
|
||||
|</pre>
|
||||
|<hr>
|
||||
|</body>
|
||||
|</html>
|
||||
|"""
|
||||
}
|
||||
}
|
||||
}
|
||||
"reject requests to file resources" in {
|
||||
Get() ~> listDirectoryContents(base + "subDirectory/empty.pdf") ~> check { handled shouldEqual (false) }
|
||||
}
|
||||
}
|
||||
|
||||
def prep(s: String) = s.stripMarginWithNewline("\n")
|
||||
|
||||
def writeAllText(text: String, file: File): Unit = {
|
||||
val fos = new FileOutputStream(file)
|
||||
try {
|
||||
fos.write(text.getBytes("UTF-8"))
|
||||
} finally fos.close()
|
||||
}
|
||||
|
||||
def evaluateTo[T](t: T, atMost: Duration = 100.millis)(implicit ec: ExecutionContext): Matcher[Future[T]] =
|
||||
be(t).compose[Future[T]] { fut ⇒
|
||||
fut.awaitResult(atMost)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,140 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.common.StrictForm
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshaller.HexInt
|
||||
import akka.http.scaladsl.model._
|
||||
import MediaTypes._
|
||||
|
||||
class FormFieldDirectivesSpec extends RoutingSpec {
|
||||
implicit val nodeSeqUnmarshaller =
|
||||
ScalaXmlSupport.nodeSeqUnmarshaller(`text/xml`, `text/html`, `text/plain`)
|
||||
|
||||
val nodeSeq: xml.NodeSeq = <b>yes</b>
|
||||
val urlEncodedForm = FormData(Map("firstName" -> "Mike", "age" -> "42"))
|
||||
val urlEncodedFormWithVip = FormData(Map("firstName" -> "Mike", "age" -> "42", "VIP" -> "true", "super" -> "<b>no</b>"))
|
||||
val multipartForm = Multipart.FormData {
|
||||
Map(
|
||||
"firstName" -> HttpEntity("Mike"),
|
||||
"age" -> HttpEntity(`text/xml`, "<int>42</int>"),
|
||||
"VIPBoolean" -> HttpEntity("true"))
|
||||
}
|
||||
val multipartFormWithTextHtml = Multipart.FormData {
|
||||
Map(
|
||||
"firstName" -> HttpEntity("Mike"),
|
||||
"age" -> HttpEntity(`text/xml`, "<int>42</int>"),
|
||||
"VIP" -> HttpEntity(`text/html`, "<b>yes</b>"),
|
||||
"super" -> HttpEntity("no"))
|
||||
}
|
||||
val multipartFormWithFile = Multipart.FormData(
|
||||
Multipart.FormData.BodyPart.Strict("file", HttpEntity(MediaTypes.`text/xml`, "<int>42</int>"),
|
||||
Map("filename" -> "age.xml")))
|
||||
|
||||
"The 'formFields' extraction directive" should {
|
||||
"properly extract the value of www-urlencoded form fields" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('firstName, "age".as[Int], 'sex?, "VIP" ? false) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "Mike42Nonefalse" }
|
||||
}
|
||||
"properly extract the value of www-urlencoded form fields when an explicit unmarshaller is given" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('firstName, "age".as(HexInt), 'sex?, "VIP" ? false) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "Mike66Nonefalse" }
|
||||
}
|
||||
"properly extract the value of multipart form fields" in {
|
||||
Post("/", multipartForm) ~> {
|
||||
formFields('firstName, "age", 'sex?, "VIP" ? nodeSeq) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "Mike<int>42</int>None<b>yes</b>" }
|
||||
}
|
||||
"extract StrictForm.FileData from a multipart part" in {
|
||||
Post("/", multipartFormWithFile) ~> {
|
||||
formFields('file.as[StrictForm.FileData]) {
|
||||
case StrictForm.FileData(name, HttpEntity.Strict(ct, data)) ⇒
|
||||
complete(s"type ${ct.mediaType} length ${data.length} filename ${name.get}")
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "type text/xml length 13 filename age.xml" }
|
||||
}
|
||||
"reject the request with a MissingFormFieldRejection if a required form field is missing" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('firstName, "age", 'sex, "VIP" ? false) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check { rejection shouldEqual MissingFormFieldRejection("sex") }
|
||||
}
|
||||
"properly extract the value if only a urlencoded deserializer is available for a multipart field that comes without a" +
|
||||
"Content-Type (or text/plain)" in {
|
||||
Post("/", multipartForm) ~> {
|
||||
formFields('firstName, "age", 'sex?, "VIPBoolean" ? false) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check {
|
||||
responseAs[String] shouldEqual "Mike<int>42</int>Nonetrue"
|
||||
}
|
||||
}
|
||||
"work even if only a FromStringUnmarshaller is available for a multipart field with custom Content-Type" in {
|
||||
Post("/", multipartFormWithTextHtml) ~> {
|
||||
formFields(('firstName, "age", 'super ? false)) { (firstName, age, vip) ⇒
|
||||
complete(firstName + age + vip)
|
||||
}
|
||||
} ~> check {
|
||||
responseAs[String] shouldEqual "Mike<int>42</int>false"
|
||||
}
|
||||
}
|
||||
"work even if only a FromEntityUnmarshaller is available for a www-urlencoded field" in {
|
||||
Post("/", urlEncodedFormWithVip) ~> {
|
||||
formFields('firstName, "age", 'sex?, "super" ? nodeSeq) { (firstName, age, sex, vip) ⇒
|
||||
complete(firstName + age + sex + vip)
|
||||
}
|
||||
} ~> check {
|
||||
responseAs[String] shouldEqual "Mike42None<b>no</b>"
|
||||
}
|
||||
}
|
||||
}
|
||||
"The 'formField' requirement directive" should {
|
||||
"block requests that do not contain the required formField" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('name ! "Mr. Mike") { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"block requests that contain the required parameter but with an unmatching value" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('firstName ! "Pete") { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"let requests pass that contain the required parameter with its required value" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('firstName ! "Mike") { completeOk }
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"The 'formField' requirement with explicit unmarshaller directive" should {
|
||||
"block requests that do not contain the required formField" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('oldAge.as(HexInt) ! 78) { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"block requests that contain the required parameter but with an unmatching value" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('age.as(HexInt) ! 78) { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"let requests pass that contain the required parameter with its required value" in {
|
||||
Post("/", urlEncodedForm) ~> {
|
||||
formFields('age.as(HexInt) ! 66 /* hex! */ ) { completeOk }
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,106 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes
|
||||
|
||||
import scala.concurrent.Future
|
||||
|
||||
class FutureDirectivesSpec extends RoutingSpec {
|
||||
|
||||
class TestException(msg: String) extends Exception(msg)
|
||||
object TestException extends Exception("XXX")
|
||||
def throwTestException[T](msgPrefix: String): T ⇒ Nothing = t ⇒ throw new TestException(msgPrefix + t)
|
||||
|
||||
implicit val exceptionHandler = ExceptionHandler {
|
||||
case e: TestException ⇒ complete(StatusCodes.InternalServerError, "Oops. " + e)
|
||||
}
|
||||
|
||||
"The `onComplete` directive" should {
|
||||
"unwrap a Future in the success case" in {
|
||||
var i = 0
|
||||
def nextNumber() = { i += 1; i }
|
||||
val route = onComplete(Future.successful(nextNumber())) { echoComplete }
|
||||
Get() ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Success(1)"
|
||||
}
|
||||
Get() ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Success(2)"
|
||||
}
|
||||
}
|
||||
"unwrap a Future in the failure case" in {
|
||||
Get() ~> onComplete(Future.failed[String](new RuntimeException("no"))) { echoComplete } ~> check {
|
||||
responseAs[String] shouldEqual "Failure(java.lang.RuntimeException: no)"
|
||||
}
|
||||
}
|
||||
"catch an exception in the success case" in {
|
||||
Get() ~> onComplete(Future.successful("ok")) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "Oops. akka.http.scaladsl.server.directives.FutureDirectivesSpec$TestException: EX when Success(ok)"
|
||||
}
|
||||
}
|
||||
"catch an exception in the failure case" in {
|
||||
Get() ~> onComplete(Future.failed[String](new RuntimeException("no"))) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "Oops. akka.http.scaladsl.server.directives.FutureDirectivesSpec$TestException: EX when Failure(java.lang.RuntimeException: no)"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The `onSuccess` directive" should {
|
||||
"unwrap a Future in the success case" in {
|
||||
Get() ~> onSuccess(Future.successful("yes")) { echoComplete } ~> check {
|
||||
responseAs[String] shouldEqual "yes"
|
||||
}
|
||||
}
|
||||
"propagate the exception in the failure case" in {
|
||||
Get() ~> onSuccess(Future.failed(TestException)) { echoComplete } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
}
|
||||
}
|
||||
"catch an exception in the success case" in {
|
||||
Get() ~> onSuccess(Future.successful("ok")) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "Oops. akka.http.scaladsl.server.directives.FutureDirectivesSpec$TestException: EX when ok"
|
||||
}
|
||||
}
|
||||
"catch an exception in the failure case" in {
|
||||
Get() ~> onSuccess(Future.failed(TestException)) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The `completeOrRecoverWith` directive" should {
|
||||
"complete the request with the Future's value if the future succeeds" in {
|
||||
Get() ~> completeOrRecoverWith(Future.successful("yes")) { echoComplete } ~> check {
|
||||
responseAs[String] shouldEqual "yes"
|
||||
}
|
||||
}
|
||||
"don't call the inner route if the Future succeeds" in {
|
||||
Get() ~> completeOrRecoverWith(Future.successful("ok")) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
responseAs[String] shouldEqual "ok"
|
||||
}
|
||||
}
|
||||
"recover using the inner route if the Future fails" in {
|
||||
val route = completeOrRecoverWith(Future.failed[String](TestException)) {
|
||||
case e ⇒ complete(s"Exception occurred: ${e.getMessage}")
|
||||
}
|
||||
|
||||
Get() ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Exception occurred: XXX"
|
||||
}
|
||||
}
|
||||
"catch an exception during recovery" in {
|
||||
Get() ~> completeOrRecoverWith(Future.failed[String](TestException)) { throwTestException("EX when ") } ~> check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "Oops. akka.http.scaladsl.server.directives.FutureDirectivesSpec$TestException: EX when akka.http.scaladsl.server.directives.FutureDirectivesSpec$TestException$: XXX"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
import akka.http.scaladsl.server._
|
||||
import org.scalatest.Inside
|
||||
|
||||
class HeaderDirectivesSpec extends RoutingSpec with Inside {
|
||||
|
||||
"The headerValuePF directive" should {
|
||||
lazy val myHeaderValue = headerValuePF { case Connection(tokens) ⇒ tokens.head }
|
||||
|
||||
"extract the respective header value if a matching request header is present" in {
|
||||
Get("/abc") ~> addHeader(Connection("close")) ~> myHeaderValue { echoComplete } ~> check {
|
||||
responseAs[String] shouldEqual "close"
|
||||
}
|
||||
}
|
||||
|
||||
"reject with an empty rejection set if no matching request header is present" in {
|
||||
Get("/abc") ~> myHeaderValue { echoComplete } ~> check { rejections shouldEqual Nil }
|
||||
}
|
||||
|
||||
"reject with a MalformedHeaderRejection if the extract function throws an exception" in {
|
||||
Get("/abc") ~> addHeader(Connection("close")) ~> {
|
||||
(headerValuePF { case _ ⇒ sys.error("Naah!") }) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) { case MalformedHeaderRejection("Connection", "Naah!", _) ⇒ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The headerValueByType directive" should {
|
||||
lazy val route =
|
||||
headerValueByType[Origin]() { origin ⇒
|
||||
complete(s"The first origin was ${origin.origins.head}")
|
||||
}
|
||||
"extract a header if the type is matching" in {
|
||||
val originHeader = Origin(HttpOrigin("http://localhost:8080"))
|
||||
Get("abc") ~> originHeader ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The first origin was http://localhost:8080"
|
||||
}
|
||||
}
|
||||
"reject a request if no header of the given type is present" in {
|
||||
Get("abc") ~> route ~> check {
|
||||
inside(rejection) {
|
||||
case MissingHeaderRejection("Origin") ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The optionalHeaderValue directive" should {
|
||||
lazy val myHeaderValue = optionalHeaderValue {
|
||||
case Connection(tokens) ⇒ Some(tokens.head)
|
||||
case _ ⇒ None
|
||||
}
|
||||
|
||||
"extract the respective header value if a matching request header is present" in {
|
||||
Get("/abc") ~> addHeader(Connection("close")) ~> myHeaderValue { echoComplete } ~> check {
|
||||
responseAs[String] shouldEqual "Some(close)"
|
||||
}
|
||||
}
|
||||
|
||||
"extract None if no matching request header is present" in {
|
||||
Get("/abc") ~> myHeaderValue { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
|
||||
"reject with a MalformedHeaderRejection if the extract function throws an exception" in {
|
||||
Get("/abc") ~> addHeader(Connection("close")) ~> {
|
||||
val myHeaderValue = optionalHeaderValue { case _ ⇒ sys.error("Naaah!") }
|
||||
myHeaderValue { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) { case MalformedHeaderRejection("Connection", "Naaah!", _) ⇒ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The optionalHeaderValueByType directive" should {
|
||||
val route =
|
||||
optionalHeaderValueByType[Origin]() {
|
||||
case Some(origin) ⇒ complete(s"The first origin was ${origin.origins.head}")
|
||||
case None ⇒ complete("No Origin header found.")
|
||||
}
|
||||
"extract Some(header) if the type is matching" in {
|
||||
val originHeader = Origin(HttpOrigin("http://localhost:8080"))
|
||||
Get("abc") ~> originHeader ~> route ~> check {
|
||||
responseAs[String] shouldEqual "The first origin was http://localhost:8080"
|
||||
}
|
||||
}
|
||||
"extract None if no header of the given type is present" in {
|
||||
Get("abc") ~> route ~> check {
|
||||
responseAs[String] shouldEqual "No Origin header found."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.headers.Host
|
||||
import org.scalatest.FreeSpec
|
||||
|
||||
class HostDirectivesSpec extends FreeSpec with GenericRoutingSpec {
|
||||
"The 'host' directive" - {
|
||||
"in its simple String form should" - {
|
||||
"block requests to unmatched hosts" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("spray.com") { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
|
||||
"let requests to matching hosts pass" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("spray.com", "spray.io") { completeOk }
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"in its simple RegEx form" - {
|
||||
"block requests to unmatched hosts" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("hairspray.*".r) { echoComplete }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
|
||||
"let requests to matching hosts pass and extract the full host" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("spra.*".r) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "spray.io" }
|
||||
}
|
||||
}
|
||||
|
||||
"in its group RegEx form" - {
|
||||
"block requests to unmatched hosts" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("hairspray(.*)".r) { echoComplete }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
|
||||
"let requests to matching hosts pass and extract the full host" in {
|
||||
Get() ~> Host("spray.io") ~> {
|
||||
host("spra(.*)".r) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "y.io" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,175 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.xml.NodeSeq
|
||||
import org.scalatest.Inside
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport
|
||||
import akka.http.scaladsl.unmarshalling._
|
||||
import akka.http.scaladsl.marshalling._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
|
||||
import MediaTypes._
|
||||
import HttpCharsets._
|
||||
import headers._
|
||||
import spray.json.DefaultJsonProtocol._
|
||||
|
||||
class MarshallingDirectivesSpec extends RoutingSpec with Inside {
|
||||
import ScalaXmlSupport._
|
||||
|
||||
private val iso88592 = HttpCharsets.getForKey("iso-8859-2").get
|
||||
implicit val IntUnmarshaller: FromEntityUnmarshaller[Int] =
|
||||
nodeSeqUnmarshaller(ContentTypeRange(`text/xml`, iso88592), `text/html`) map {
|
||||
case NodeSeq.Empty ⇒ throw Unmarshaller.NoContentException
|
||||
case x ⇒ { val i = x.text.toInt; require(i >= 0); i }
|
||||
}
|
||||
|
||||
implicit val IntMarshaller: ToEntityMarshaller[Int] =
|
||||
Marshaller.oneOf(ContentType(`application/xhtml+xml`), ContentType(`text/xml`, `UTF-8`)) { contentType ⇒
|
||||
nodeSeqMarshaller(contentType).wrap(contentType) { (i: Int) ⇒ <int>{ i }</int> }
|
||||
}
|
||||
|
||||
"The 'entityAs' directive" should {
|
||||
"extract an object from the requests entity using the in-scope Unmarshaller" in {
|
||||
Put("/", <p>cool</p>) ~> {
|
||||
entity(as[NodeSeq]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "<p>cool</p>" }
|
||||
}
|
||||
"return a RequestEntityExpectedRejection rejection if the request has no entity" in {
|
||||
Put() ~> {
|
||||
entity(as[Int]) { echoComplete }
|
||||
} ~> check { rejection shouldEqual RequestEntityExpectedRejection }
|
||||
}
|
||||
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope" in {
|
||||
Put("/", HttpEntity(`text/css`, "<p>cool</p>")) ~> {
|
||||
entity(as[NodeSeq]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`))
|
||||
}
|
||||
Put("/", HttpEntity(ContentType(`text/xml`, `UTF-16`), "<int>26</int>")) ~> {
|
||||
entity(as[Int]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`))
|
||||
}
|
||||
}
|
||||
"cancel UnsupportedRequestContentTypeRejections if a subsequent `entity` directive succeeds" in {
|
||||
Put("/", HttpEntity(`text/plain`, "yeah")) ~> {
|
||||
entity(as[NodeSeq]) { _ ⇒ completeOk } ~
|
||||
entity(as[String]) { _ ⇒ validate(false, "Problem") { completeOk } }
|
||||
} ~> check { rejection shouldEqual ValidationRejection("Problem") }
|
||||
}
|
||||
"return a ValidationRejection if the request entity is semantically invalid (IllegalArgumentException)" in {
|
||||
Put("/", HttpEntity(ContentType(`text/xml`, iso88592), "<int>-3</int>")) ~> {
|
||||
entity(as[Int]) { _ ⇒ completeOk }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case ValidationRejection("requirement failed", Some(_: IllegalArgumentException)) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
"return a MalformedRequestContentRejection if unmarshalling failed due to a not further classified error" in {
|
||||
Put("/", HttpEntity(`text/xml`, "<foo attr='illegal xml'")) ~> {
|
||||
entity(as[NodeSeq]) { _ ⇒ completeOk }
|
||||
} ~> check {
|
||||
rejection shouldEqual MalformedRequestContentRejection(
|
||||
"XML document structures must start and end within the same entity.", None)
|
||||
}
|
||||
}
|
||||
"extract an Option[T] from the requests entity using the in-scope Unmarshaller" in {
|
||||
Put("/", <p>cool</p>) ~> {
|
||||
entity(as[Option[NodeSeq]]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Some(<p>cool</p>)" }
|
||||
}
|
||||
"extract an Option[T] as None if the request has no entity" in {
|
||||
Put() ~> {
|
||||
entity(as[Option[Int]]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"return an UnsupportedRequestContentTypeRejection if no matching unmarshaller is in scope (for Option[T]s)" in {
|
||||
Put("/", HttpEntity(`text/css`, "<p>cool</p>")) ~> {
|
||||
entity(as[Option[NodeSeq]]) { echoComplete }
|
||||
} ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`text/xml`, `application/xml`, `text/html`, `application/xhtml+xml`))
|
||||
}
|
||||
}
|
||||
"properly extract with a super-unmarshaller" in {
|
||||
case class Person(name: String)
|
||||
val jsonUnmarshaller: FromEntityUnmarshaller[Person] = jsonFormat1(Person)
|
||||
val xmlUnmarshaller: FromEntityUnmarshaller[Person] =
|
||||
ScalaXmlSupport.nodeSeqUnmarshaller(`text/xml`).map(seq ⇒ Person(seq.text))
|
||||
|
||||
implicit val unmarshaller = Unmarshaller.firstOf(jsonUnmarshaller, xmlUnmarshaller)
|
||||
|
||||
val route = entity(as[Person]) { echoComplete }
|
||||
|
||||
Put("/", HttpEntity(`text/xml`, "<name>Peter Xml</name>")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Person(Peter Xml)"
|
||||
}
|
||||
Put("/", HttpEntity(`application/json`, """{ "name": "Paul Json" }""")) ~> route ~> check {
|
||||
responseAs[String] shouldEqual "Person(Paul Json)"
|
||||
}
|
||||
Put("/", HttpEntity(`text/plain`, """name = Sir Text }""")) ~> route ~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(`application/json`, `text/xml`))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'completeWith' directive" should {
|
||||
"provide a completion function converting custom objects to an HttpEntity using the in-scope marshaller" in {
|
||||
Get() ~> completeWith(instanceOf[Int]) { prod ⇒ prod(42) } ~> check {
|
||||
responseEntity shouldEqual HttpEntity(ContentType(`application/xhtml+xml`, `UTF-8`), "<int>42</int>")
|
||||
}
|
||||
}
|
||||
"return a UnacceptedResponseContentTypeRejection rejection if no acceptable marshaller is in scope" in {
|
||||
Get() ~> Accept(`text/css`) ~> completeWith(instanceOf[Int]) { prod ⇒ prod(42) } ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`)))
|
||||
}
|
||||
}
|
||||
"convert the response content to an accepted charset" in {
|
||||
Get() ~> `Accept-Charset`(`UTF-8`) ~> completeWith(instanceOf[String]) { prod ⇒ prod("Hällö") } ~> check {
|
||||
responseEntity shouldEqual HttpEntity(ContentType(`text/plain`, `UTF-8`), "Hällö")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'handleWith' directive" should {
|
||||
def times2(x: Int) = x * 2
|
||||
|
||||
"support proper round-trip content unmarshalling/marshalling to and from a function" in (
|
||||
Put("/", HttpEntity(`text/html`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
|
||||
~> check { responseEntity shouldEqual HttpEntity(ContentType(`text/xml`, `UTF-8`), "<int>84</int>") })
|
||||
|
||||
"result in UnsupportedRequestContentTypeRejection rejection if there is no unmarshaller supporting the requests charset" in (
|
||||
Put("/", HttpEntity(`text/xml`, "<int>42</int>")) ~> Accept(`text/xml`) ~> handleWith(times2)
|
||||
~> check {
|
||||
rejection shouldEqual UnsupportedRequestContentTypeRejection(Set(ContentTypeRange(`text/xml`, iso88592), `text/html`))
|
||||
})
|
||||
|
||||
"result in an UnacceptedResponseContentTypeRejection rejection if there is no marshaller supporting the requests Accept-Charset header" in (
|
||||
Put("/", HttpEntity(`text/html`, "<int>42</int>")) ~> addHeaders(Accept(`text/xml`), `Accept-Charset`(`UTF-16`)) ~>
|
||||
handleWith(times2) ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(`application/xhtml+xml`, ContentType(`text/xml`, `UTF-8`)))
|
||||
})
|
||||
}
|
||||
|
||||
"The marshalling infrastructure for JSON" should {
|
||||
import spray.json._
|
||||
case class Foo(name: String)
|
||||
implicit val fooFormat = jsonFormat1(Foo)
|
||||
val foo = Foo("Hällö")
|
||||
|
||||
"render JSON with UTF-8 encoding if no `Accept-Charset` request header is present" in {
|
||||
Get() ~> complete(foo) ~> check {
|
||||
responseEntity shouldEqual HttpEntity(ContentType(`application/json`, `UTF-8`), foo.toJson.prettyPrint)
|
||||
}
|
||||
}
|
||||
"reject JSON rendering if an `Accept-Charset` request header requests a non-UTF-8 encoding" in {
|
||||
Get() ~> `Accept-Charset`(`ISO-8859-1`) ~> complete(foo) ~> check {
|
||||
rejection shouldEqual UnacceptedResponseContentTypeRejection(Set(ContentType(`application/json`)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model.{ StatusCodes, HttpMethods }
|
||||
import akka.http.scaladsl.server._
|
||||
|
||||
class MethodDirectivesSpec extends RoutingSpec {
|
||||
|
||||
"get | put" should {
|
||||
lazy val getOrPut = (get | put) { completeOk }
|
||||
|
||||
"block POST requests" in {
|
||||
Post() ~> getOrPut ~> check { handled shouldEqual false }
|
||||
}
|
||||
"let GET requests pass" in {
|
||||
Get() ~> getOrPut ~> check { response shouldEqual Ok }
|
||||
}
|
||||
"let PUT requests pass" in {
|
||||
Put() ~> getOrPut ~> check { response shouldEqual Ok }
|
||||
}
|
||||
}
|
||||
|
||||
"two failed `get` directives" should {
|
||||
"only result in a single Rejection" in {
|
||||
Put() ~> {
|
||||
get { completeOk } ~
|
||||
get { completeOk }
|
||||
} ~> check {
|
||||
rejections shouldEqual List(MethodRejection(HttpMethods.GET))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"overrideMethodWithParameter" should {
|
||||
"change the request method" in {
|
||||
Get("/?_method=put") ~> overrideMethodWithParameter("_method") {
|
||||
get { complete("GET") } ~
|
||||
put { complete("PUT") }
|
||||
} ~> check { responseAs[String] shouldEqual "PUT" }
|
||||
}
|
||||
"not affect the request when not specified" in {
|
||||
Get() ~> overrideMethodWithParameter("_method") {
|
||||
get { complete("GET") } ~
|
||||
put { complete("PUT") }
|
||||
} ~> check { responseAs[String] shouldEqual "GET" }
|
||||
}
|
||||
"complete with 501 Not Implemented when not a valid method" in {
|
||||
Get("/?_method=hallo") ~> overrideMethodWithParameter("_method") {
|
||||
get { complete("GET") } ~
|
||||
put { complete("PUT") }
|
||||
} ~> check { status shouldEqual StatusCodes.NotImplemented }
|
||||
}
|
||||
}
|
||||
|
||||
"MethodRejections under a successful match" should {
|
||||
"be cancelled if the match happens after the rejection" in {
|
||||
Put() ~> {
|
||||
get { completeOk } ~
|
||||
put { reject(RequestEntityExpectedRejection) }
|
||||
} ~> check {
|
||||
rejections shouldEqual List(RequestEntityExpectedRejection)
|
||||
}
|
||||
}
|
||||
"be cancelled if the match happens after the rejection (example 2)" in {
|
||||
Put() ~> {
|
||||
(get & complete(Ok)) ~ (put & reject(RequestEntityExpectedRejection))
|
||||
} ~> check {
|
||||
rejections shouldEqual List(RequestEntityExpectedRejection)
|
||||
}
|
||||
}
|
||||
"be cancelled if the match happens before the rejection" in {
|
||||
Put() ~> {
|
||||
put { reject(RequestEntityExpectedRejection) } ~ get { completeOk }
|
||||
} ~> check {
|
||||
rejections shouldEqual List(RequestEntityExpectedRejection)
|
||||
}
|
||||
}
|
||||
"be cancelled if the match happens before the rejection (example 2)" in {
|
||||
Put() ~> {
|
||||
(put & reject(RequestEntityExpectedRejection)) ~ (get & complete(Ok))
|
||||
} ~> check {
|
||||
rejections shouldEqual List(RequestEntityExpectedRejection)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import headers._
|
||||
import HttpMethods._
|
||||
import MediaTypes._
|
||||
import Uri._
|
||||
|
||||
class MiscDirectivesSpec extends RoutingSpec {
|
||||
|
||||
"the extractClientIP directive" should {
|
||||
"extract from a X-Forwarded-For header" in {
|
||||
Get() ~> addHeaders(`X-Forwarded-For`("2.3.4.5"), RawHeader("x-real-ip", "1.2.3.4")) ~> {
|
||||
extractClientIP { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "2.3.4.5" }
|
||||
}
|
||||
"extract from a Remote-Address header" in {
|
||||
Get() ~> addHeaders(RawHeader("x-real-ip", "1.2.3.4"), `Remote-Address`(RemoteAddress("5.6.7.8"))) ~> {
|
||||
extractClientIP { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "5.6.7.8" }
|
||||
}
|
||||
"extract from a X-Real-IP header" in {
|
||||
Get() ~> addHeader(RawHeader("x-real-ip", "1.2.3.4")) ~> {
|
||||
extractClientIP { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "1.2.3.4" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import org.scalatest.{ FreeSpec, Inside }
|
||||
import akka.http.scaladsl.unmarshalling.Unmarshaller.HexInt
|
||||
|
||||
class ParameterDirectivesSpec extends FreeSpec with GenericRoutingSpec with Inside {
|
||||
|
||||
"when used with 'as[Int]' the parameter directive should" - {
|
||||
"extract a parameter value as Int" in {
|
||||
Get("/?amount=123") ~> {
|
||||
parameter('amount.as[Int]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "123" }
|
||||
}
|
||||
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
||||
Get("/?amount=1x3") ~> {
|
||||
parameter('amount.as[Int]) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case MalformedQueryParamRejection("amount", "'1x3' is not a valid 32-bit signed integer value", Some(_)) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
"supply typed default values" in {
|
||||
Get() ~> {
|
||||
parameter('amount ? 45) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "45" }
|
||||
}
|
||||
"create typed optional parameters that" - {
|
||||
"extract Some(value) when present" in {
|
||||
Get("/?amount=12") ~> {
|
||||
parameter("amount".as[Int]?) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Some(12)" }
|
||||
}
|
||||
"extract None when not present" in {
|
||||
Get() ~> {
|
||||
parameter("amount".as[Int]?) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
||||
Get("/?amount=x") ~> {
|
||||
parameter("amount".as[Int]?) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case MalformedQueryParamRejection("amount", "'x' is not a valid 32-bit signed integer value", Some(_)) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"when used with 'as(HexInt)' the parameter directive should" - {
|
||||
"extract parameter values as Int" in {
|
||||
Get("/?amount=1f") ~> {
|
||||
parameter('amount.as(HexInt)) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "31" }
|
||||
}
|
||||
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
||||
Get("/?amount=1x3") ~> {
|
||||
parameter('amount.as(HexInt)) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case MalformedQueryParamRejection("amount", "'1x3' is not a valid 32-bit hexadecimal integer value", Some(_)) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
"supply typed default values" in {
|
||||
Get() ~> {
|
||||
parameter('amount.as(HexInt) ? 45) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "45" }
|
||||
}
|
||||
"create typed optional parameters that" - {
|
||||
"extract Some(value) when present" in {
|
||||
Get("/?amount=A") ~> {
|
||||
parameter("amount".as(HexInt)?) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Some(10)" }
|
||||
}
|
||||
"extract None when not present" in {
|
||||
Get() ~> {
|
||||
parameter("amount".as(HexInt)?) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"cause a MalformedQueryParamRejection on illegal Int values" in {
|
||||
Get("/?amount=x") ~> {
|
||||
parameter("amount".as(HexInt)?) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case MalformedQueryParamRejection("amount", "'x' is not a valid 32-bit hexadecimal integer value", Some(_)) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"when used with 'as[Boolean]' the parameter directive should" - {
|
||||
"extract parameter values as Boolean" in {
|
||||
Get("/?really=true") ~> {
|
||||
parameter('really.as[Boolean]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "true" }
|
||||
Get("/?really=no") ~> {
|
||||
parameter('really.as[Boolean]) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "false" }
|
||||
}
|
||||
"extract optional parameter values as Boolean" in {
|
||||
Get() ~> {
|
||||
parameter('really.as[Boolean] ? false) { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "false" }
|
||||
}
|
||||
"cause a MalformedQueryParamRejection on illegal Boolean values" in {
|
||||
Get("/?really=absolutely") ~> {
|
||||
parameter('really.as[Boolean]) { echoComplete }
|
||||
} ~> check {
|
||||
inside(rejection) {
|
||||
case MalformedQueryParamRejection("really", "'absolutely' is not a valid Boolean value", None) ⇒
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"The 'parameters' extraction directive should" - {
|
||||
"extract the value of given parameters" in {
|
||||
Get("/?name=Parsons&FirstName=Ellen") ~> {
|
||||
parameters("name", 'FirstName) { (name, firstName) ⇒
|
||||
complete(firstName + name)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "EllenParsons" }
|
||||
}
|
||||
"correctly extract an optional parameter" in {
|
||||
Get("/?foo=bar") ~> parameters('foo ?) { echoComplete } ~> check { responseAs[String] shouldEqual "Some(bar)" }
|
||||
Get("/?foo=bar") ~> parameters('baz ?) { echoComplete } ~> check { responseAs[String] shouldEqual "None" }
|
||||
}
|
||||
"ignore additional parameters" in {
|
||||
Get("/?name=Parsons&FirstName=Ellen&age=29") ~> {
|
||||
parameters("name", 'FirstName) { (name, firstName) ⇒
|
||||
complete(firstName + name)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "EllenParsons" }
|
||||
}
|
||||
"reject the request with a MissingQueryParamRejection if a required parameter is missing" in {
|
||||
Get("/?name=Parsons&sex=female") ~> {
|
||||
parameters('name, 'FirstName, 'age) { (name, firstName, age) ⇒
|
||||
completeOk
|
||||
}
|
||||
} ~> check { rejection shouldEqual MissingQueryParamRejection("FirstName") }
|
||||
}
|
||||
"supply the default value if an optional parameter is missing" in {
|
||||
Get("/?name=Parsons&FirstName=Ellen") ~> {
|
||||
parameters("name"?, 'FirstName, 'age ? "29", 'eyes?) { (name, firstName, age, eyes) ⇒
|
||||
complete(firstName + name + age + eyes)
|
||||
}
|
||||
} ~> check { responseAs[String] shouldEqual "EllenSome(Parsons)29None" }
|
||||
}
|
||||
}
|
||||
|
||||
"The 'parameter' requirement directive should" - {
|
||||
"block requests that do not contain the required parameter" in {
|
||||
Get("/person?age=19") ~> {
|
||||
parameter('nose ! "large") { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"block requests that contain the required parameter but with an unmatching value" in {
|
||||
Get("/person?age=19&nose=small") ~> {
|
||||
parameter('nose ! "large") { completeOk }
|
||||
} ~> check { handled shouldEqual false }
|
||||
}
|
||||
"let requests pass that contain the required parameter with its required value" in {
|
||||
Get("/person?nose=large&eyes=blue") ~> {
|
||||
parameter('nose ! "large") { completeOk }
|
||||
} ~> check { response shouldEqual Ok }
|
||||
}
|
||||
"be useable for method tunneling" in {
|
||||
val route = {
|
||||
(post | parameter('method ! "post")) { complete("POST") } ~
|
||||
get { complete("GET") }
|
||||
}
|
||||
Get("/?method=post") ~> route ~> check { responseAs[String] shouldEqual "POST" }
|
||||
Post() ~> route ~> check { responseAs[String] shouldEqual "POST" }
|
||||
Get() ~> route ~> check { responseAs[String] shouldEqual "GET" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,339 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.server._
|
||||
import org.scalatest.Inside
|
||||
|
||||
class PathDirectivesSpec extends RoutingSpec with Inside {
|
||||
val echoUnmatchedPath = extractUnmatchedPath { echoComplete }
|
||||
def echoCaptureAndUnmatchedPath[T]: T ⇒ Route =
|
||||
capture ⇒ ctx ⇒ ctx.complete(capture.toString + ":" + ctx.unmatchedPath)
|
||||
|
||||
"""path("foo")""" should {
|
||||
val test = testFor(path("foo") { echoUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"reject [/foobar]" in test()
|
||||
"reject [/foo/bar]" in test()
|
||||
"accept [/foo] and clear the unmatchedPath" in test("")
|
||||
"reject [/foo/]" in test()
|
||||
}
|
||||
|
||||
"""path("foo" /)""" should {
|
||||
val test = testFor(path("foo" /) { echoUnmatchedPath })
|
||||
"reject [/foo]" in test()
|
||||
"accept [/foo/] and clear the unmatchedPath" in test("")
|
||||
}
|
||||
|
||||
"""path("")""" should {
|
||||
val test = testFor(path("") { echoUnmatchedPath })
|
||||
"reject [/foo]" in test()
|
||||
"accept [/] and clear the unmatchedPath" in test("")
|
||||
}
|
||||
|
||||
"""pathPrefix("foo")""" should {
|
||||
val test = testFor(pathPrefix("foo") { echoUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"accept [/foobar]" in test("bar")
|
||||
"accept [/foo/bar]" in test("/bar")
|
||||
"accept [/foo] and clear the unmatchedPath" in test("")
|
||||
"accept [/foo/] and clear the unmatchedPath" in test("/")
|
||||
}
|
||||
|
||||
"""pathPrefix("foo" / "bar")""" should {
|
||||
val test = testFor(pathPrefix("foo" / "bar") { echoUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"accept [/foo/bar]" in test("")
|
||||
"accept [/foo/bar/baz]" in test("/baz")
|
||||
}
|
||||
|
||||
"""pathPrefix("ab[cd]+".r)""" should {
|
||||
val test = testFor(pathPrefix("ab[cd]+".r) { echoCaptureAndUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"reject [/ab/cd]" in test()
|
||||
"reject [/abcdef]" in test("abcd:ef")
|
||||
"reject [/abcdd/ef]" in test("abcdd:/ef")
|
||||
}
|
||||
|
||||
"""pathPrefix("ab(cd)".r)""" should {
|
||||
val test = testFor(pathPrefix("ab(cd)+".r) { echoCaptureAndUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"reject [/ab/cd]" in test()
|
||||
"reject [/abcdef]" in test("cd:ef")
|
||||
"reject [/abcde/fg]" in test("cd:e/fg")
|
||||
}
|
||||
|
||||
"pathPrefix(regex)" should {
|
||||
"fail when the regex contains more than one group" in {
|
||||
an[IllegalArgumentException] must be thrownBy path("a(b+)(c+)".r) { echoCaptureAndUnmatchedPath }
|
||||
}
|
||||
}
|
||||
|
||||
"pathPrefix(IntNumber)" should {
|
||||
val test = testFor(pathPrefix(IntNumber) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/23]" in test("23:")
|
||||
"accept [/12345yes]" in test("12345:yes")
|
||||
"reject [/]" in test()
|
||||
"reject [/abc]" in test()
|
||||
"reject [/2147483648]" in test() // > Int.MaxValue
|
||||
}
|
||||
|
||||
"pathPrefix(CustomShortNumber)" should {
|
||||
object CustomShortNumber extends NumberMatcher[Short](Short.MaxValue, 10) {
|
||||
def fromChar(c: Char) = fromDecimalChar(c)
|
||||
}
|
||||
|
||||
val test = testFor(pathPrefix(CustomShortNumber) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/23]" in test("23:")
|
||||
"accept [/12345yes]" in test("12345:yes")
|
||||
"reject [/]" in test()
|
||||
"reject [/abc]" in test()
|
||||
"reject [/33000]" in test() // > Short.MaxValue
|
||||
}
|
||||
|
||||
"pathPrefix(JavaUUID)" should {
|
||||
val test = testFor(pathPrefix(JavaUUID) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/bdea8652-f26c-40ca-8157-0b96a2a8389d]" in test("bdea8652-f26c-40ca-8157-0b96a2a8389d:")
|
||||
"accept [/bdea8652-f26c-40ca-8157-0b96a2a8389dyes]" in test("bdea8652-f26c-40ca-8157-0b96a2a8389d:yes")
|
||||
"reject [/]" in test()
|
||||
"reject [/abc]" in test()
|
||||
}
|
||||
|
||||
"pathPrefix(Map(\"red\" -> 1, \"green\" -> 2, \"blue\" -> 3))" should {
|
||||
val test = testFor(pathPrefix(Map("red" -> 1, "green" -> 2, "blue" -> 3)) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/green]" in test("2:")
|
||||
"accept [/redsea]" in test("1:sea")
|
||||
"reject [/black]" in test()
|
||||
}
|
||||
|
||||
"pathPrefix(Map.empty)" should {
|
||||
val test = testFor(pathPrefix(Map[String, Int]()) { echoCaptureAndUnmatchedPath })
|
||||
"reject [/black]" in test()
|
||||
}
|
||||
|
||||
"pathPrefix(Segment)" should {
|
||||
val test = testFor(pathPrefix(Segment) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/abc]" in test("abc:")
|
||||
"accept [/abc/]" in test("abc:/")
|
||||
"accept [/abc/def]" in test("abc:/def")
|
||||
"reject [/]" in test()
|
||||
}
|
||||
|
||||
"pathPrefix(Segments)" should {
|
||||
val test = testFor(pathPrefix(Segments) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/]" in test("List():")
|
||||
"accept [/a/b/c]" in test("List(a, b, c):")
|
||||
"accept [/a/b/c/]" in test("List(a, b, c):/")
|
||||
}
|
||||
|
||||
"""pathPrefix(separateOnSlashes("a/b"))""" should {
|
||||
val test = testFor(pathPrefix(separateOnSlashes("a/b")) { echoUnmatchedPath })
|
||||
"accept [/a/b]" in test("")
|
||||
"accept [/a/b/]" in test("/")
|
||||
"accept [/a/c]" in test()
|
||||
}
|
||||
"""pathPrefix(separateOnSlashes("abc"))""" should {
|
||||
val test = testFor(pathPrefix(separateOnSlashes("abc")) { echoUnmatchedPath })
|
||||
"accept [/abc]" in test("")
|
||||
"accept [/abcdef]" in test("def")
|
||||
"accept [/ab]" in test()
|
||||
}
|
||||
|
||||
"""pathPrefixTest("a" / Segment ~ Slash)""" should {
|
||||
val test = testFor(pathPrefixTest("a" / Segment ~ Slash) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/a/bc/]" in test("bc:/a/bc/")
|
||||
"accept [/a/bc]" in test()
|
||||
"accept [/a/]" in test()
|
||||
}
|
||||
|
||||
"""pathSuffix("edit" / Segment)""" should {
|
||||
val test = testFor(pathSuffix("edit" / Segment) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/orders/123/edit]" in test("123:/orders/")
|
||||
"accept [/orders/123/ed]" in test()
|
||||
"accept [/edit]" in test()
|
||||
}
|
||||
|
||||
"""pathSuffix("foo" / "bar" ~ "baz")""" should {
|
||||
val test = testFor(pathSuffix("foo" / "bar" ~ "baz") { echoUnmatchedPath })
|
||||
"accept [/orders/barbaz/foo]" in test("/orders/")
|
||||
"accept [/orders/bazbar/foo]" in test()
|
||||
}
|
||||
|
||||
"pathSuffixTest(Slash)" should {
|
||||
val test = testFor(pathSuffixTest(Slash) { echoUnmatchedPath })
|
||||
"accept [/]" in test("/")
|
||||
"accept [/foo/]" in test("/foo/")
|
||||
"accept [/foo]" in test()
|
||||
}
|
||||
|
||||
"""pathPrefix("foo" | "bar")""" should {
|
||||
val test = testFor(pathPrefix("foo" | "bar") { echoUnmatchedPath })
|
||||
"accept [/foo]" in test("")
|
||||
"accept [/foops]" in test("ps")
|
||||
"accept [/bar]" in test("")
|
||||
"reject [/baz]" in test()
|
||||
}
|
||||
|
||||
"""pathSuffix(!"foo")""" should {
|
||||
val test = testFor(pathSuffix(!"foo") { echoUnmatchedPath })
|
||||
"accept [/bar]" in test("/bar")
|
||||
"reject [/foo]" in test()
|
||||
}
|
||||
|
||||
"pathPrefix(IntNumber?)" should {
|
||||
val test = testFor(pathPrefix(IntNumber?) { echoCaptureAndUnmatchedPath })
|
||||
"accept [/12]" in test("Some(12):")
|
||||
"accept [/12a]" in test("Some(12):a")
|
||||
"accept [/foo]" in test("None:foo")
|
||||
}
|
||||
|
||||
"""pathPrefix("foo"?)""" should {
|
||||
val test = testFor(pathPrefix("foo"?) { echoUnmatchedPath })
|
||||
"accept [/foo]" in test("")
|
||||
"accept [/fool]" in test("l")
|
||||
"accept [/bar]" in test("bar")
|
||||
}
|
||||
|
||||
"""pathPrefix("foo") & pathEnd""" should {
|
||||
val test = testFor((pathPrefix("foo") & pathEnd) { echoUnmatchedPath })
|
||||
"reject [/foobar]" in test()
|
||||
"reject [/foo/bar]" in test()
|
||||
"accept [/foo] and clear the unmatchedPath" in test("")
|
||||
"reject [/foo/]" in test()
|
||||
}
|
||||
|
||||
"""pathPrefix("foo") & pathEndOrSingleSlash""" should {
|
||||
val test = testFor((pathPrefix("foo") & pathEndOrSingleSlash) { echoUnmatchedPath })
|
||||
"reject [/foobar]" in test()
|
||||
"reject [/foo/bar]" in test()
|
||||
"accept [/foo] and clear the unmatchedPath" in test("")
|
||||
"accept [/foo/] and clear the unmatchedPath" in test("")
|
||||
}
|
||||
|
||||
"""pathPrefix(IntNumber.repeat(separator = "."))""" should {
|
||||
{
|
||||
val test = testFor(pathPrefix(IntNumber.repeat(min = 2, max = 5, separator = ".")) { echoCaptureAndUnmatchedPath })
|
||||
"reject [/foo]" in test()
|
||||
"reject [/1foo]" in test()
|
||||
"reject [/1.foo]" in test()
|
||||
"accept [/1.2foo]" in test("List(1, 2):foo")
|
||||
"accept [/1.2.foo]" in test("List(1, 2):.foo")
|
||||
"accept [/1.2.3foo]" in test("List(1, 2, 3):foo")
|
||||
"accept [/1.2.3.foo]" in test("List(1, 2, 3):.foo")
|
||||
"accept [/1.2.3.4foo]" in test("List(1, 2, 3, 4):foo")
|
||||
"accept [/1.2.3.4.foo]" in test("List(1, 2, 3, 4):.foo")
|
||||
"accept [/1.2.3.4.5foo]" in test("List(1, 2, 3, 4, 5):foo")
|
||||
"accept [/1.2.3.4.5.foo]" in test("List(1, 2, 3, 4, 5):.foo")
|
||||
"accept [/1.2.3.4.5.6foo]" in test("List(1, 2, 3, 4, 5):.6foo")
|
||||
"accept [/1.2.3.]" in test("List(1, 2, 3):.")
|
||||
"accept [/1.2.3/]" in test("List(1, 2, 3):/")
|
||||
"accept [/1.2.3./]" in test("List(1, 2, 3):./")
|
||||
}
|
||||
{
|
||||
val test = testFor(pathPrefix(IntNumber.repeat(2, ".")) { echoCaptureAndUnmatchedPath })
|
||||
"reject [/bar]" in test()
|
||||
"reject [/1bar]" in test()
|
||||
"reject [/1.bar]" in test()
|
||||
"accept [/1.2bar]" in test("List(1, 2):bar")
|
||||
"accept [/1.2.bar]" in test("List(1, 2):.bar")
|
||||
"accept [/1.2.3bar]" in test("List(1, 2):.3bar")
|
||||
}
|
||||
}
|
||||
|
||||
"PathMatchers" should {
|
||||
{
|
||||
val test = testFor(path(Rest.tmap { case Tuple1(s) ⇒ Tuple1(s.split('-').toList) }) { echoComplete })
|
||||
"support the hmap modifier in accept [/yes-no]" in test("List(yes, no)")
|
||||
}
|
||||
{
|
||||
val test = testFor(path(Rest.map(_.split('-').toList)) { echoComplete })
|
||||
"support the map modifier in accept [/yes-no]" in test("List(yes, no)")
|
||||
}
|
||||
{
|
||||
val test = testFor(path(Rest.tflatMap { case Tuple1(s) ⇒ Some(s).filter("yes" ==).map(x ⇒ Tuple1(x)) }) { echoComplete })
|
||||
"support the hflatMap modifier in accept [/yes]" in test("yes")
|
||||
"support the hflatMap modifier in reject [/blub]" in test()
|
||||
}
|
||||
{
|
||||
val test = testFor(path(Rest.flatMap(s ⇒ Some(s).filter("yes" ==))) { echoComplete })
|
||||
"support the flatMap modifier in accept [/yes]" in test("yes")
|
||||
"support the flatMap modifier reject [/blub]" in test()
|
||||
}
|
||||
}
|
||||
|
||||
implicit class WithIn(str: String) {
|
||||
def in(f: String ⇒ Unit) = convertToWordSpecStringWrapper(str) in f(str)
|
||||
def in(body: ⇒ Unit) = convertToWordSpecStringWrapper(str) in body
|
||||
}
|
||||
|
||||
case class testFor(route: Route) {
|
||||
def apply(expectedResponse: String = null): String ⇒ Unit = exampleString ⇒
|
||||
"\\[([^\\]]+)\\]".r.findFirstMatchIn(exampleString) match {
|
||||
case Some(uri) ⇒ Get(uri.group(1)) ~> route ~> check {
|
||||
if (expectedResponse eq null) handled shouldEqual false
|
||||
else responseAs[String] shouldEqual expectedResponse
|
||||
}
|
||||
case None ⇒ failTest("Example '" + exampleString + "' doesn't contain a test uri")
|
||||
}
|
||||
}
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes._
|
||||
|
||||
"the `redirectToTrailingSlashIfMissing` directive" should {
|
||||
val route = redirectToTrailingSlashIfMissing(Found) { completeOk }
|
||||
|
||||
"pass if the request path already has a trailing slash" in {
|
||||
Get("/foo/bar/") ~> route ~> check { response shouldEqual Ok }
|
||||
}
|
||||
|
||||
"redirect if the request path doesn't have a trailing slash" in {
|
||||
Get("/foo/bar") ~> route ~> checkRedirectTo("/foo/bar/")
|
||||
}
|
||||
|
||||
"preserves the query and the frag when redirect" in {
|
||||
Get("/foo/bar?query#frag") ~> route ~> checkRedirectTo("/foo/bar/?query#frag")
|
||||
}
|
||||
|
||||
"redirect with the given redirection status code" in {
|
||||
Get("/foo/bar") ~>
|
||||
redirectToTrailingSlashIfMissing(MovedPermanently) { completeOk } ~>
|
||||
check { status shouldEqual MovedPermanently }
|
||||
}
|
||||
}
|
||||
|
||||
"the `redirectToNoTrailingSlashIfPresent` directive" should {
|
||||
val route = redirectToNoTrailingSlashIfPresent(Found) { completeOk }
|
||||
|
||||
"pass if the request path already doesn't have a trailing slash" in {
|
||||
Get("/foo/bar") ~> route ~> check { response shouldEqual Ok }
|
||||
}
|
||||
|
||||
"redirect if the request path has a trailing slash" in {
|
||||
Get("/foo/bar/") ~> route ~> checkRedirectTo("/foo/bar")
|
||||
}
|
||||
|
||||
"preserves the query and the frag when redirect" in {
|
||||
Get("/foo/bar/?query#frag") ~> route ~> checkRedirectTo("/foo/bar?query#frag")
|
||||
}
|
||||
|
||||
"redirect with the given redirection status code" in {
|
||||
Get("/foo/bar/") ~>
|
||||
redirectToNoTrailingSlashIfPresent(MovedPermanently) { completeOk } ~>
|
||||
check { status shouldEqual MovedPermanently }
|
||||
}
|
||||
}
|
||||
|
||||
import akka.http.scaladsl.model.headers.Location
|
||||
import akka.http.scaladsl.model.Uri
|
||||
|
||||
private def checkRedirectTo(expectedUri: Uri) =
|
||||
check {
|
||||
status shouldBe a[Redirection]
|
||||
inside(header[Location]) {
|
||||
case Some(Location(uri)) ⇒
|
||||
(if (expectedUri.isAbsolute) uri else uri.toRelative) shouldEqual expectedUri
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.concurrent.Await
|
||||
import scala.concurrent.duration._
|
||||
import akka.http.scaladsl.model.StatusCodes._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.impl.util._
|
||||
import akka.stream.scaladsl.{ Sink, Source }
|
||||
import akka.util.ByteString
|
||||
import org.scalatest.{ Inside, Inspectors }
|
||||
|
||||
class RangeDirectivesSpec extends RoutingSpec with Inspectors with Inside {
|
||||
lazy val wrs =
|
||||
mapSettings(_.copy(rangeCountLimit = 10, rangeCoalescingThreshold = 1L)) &
|
||||
withRangeSupport
|
||||
|
||||
def bytes(length: Byte) = Array.tabulate[Byte](length)(_.toByte)
|
||||
|
||||
"The `withRangeSupport` directive" should {
|
||||
def completeWithRangedBytes(length: Byte) = wrs(complete(bytes(length)))
|
||||
|
||||
"return an Accept-Ranges(bytes) header for GET requests" in {
|
||||
Get() ~> { wrs { complete("any") } } ~> check {
|
||||
headers should contain(`Accept-Ranges`(RangeUnits.Bytes))
|
||||
}
|
||||
}
|
||||
|
||||
"not return an Accept-Ranges(bytes) header for non-GET requests" in {
|
||||
Put() ~> { wrs { complete("any") } } ~> check {
|
||||
headers should not contain `Accept-Ranges`(RangeUnits.Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
"return a Content-Range header for a ranged request with a single range" in {
|
||||
Get() ~> addHeader(Range(ByteRange(0, 1))) ~> completeWithRangedBytes(10) ~> check {
|
||||
headers should contain(`Content-Range`(ContentRange(0, 1, 10)))
|
||||
status shouldEqual PartialContent
|
||||
responseAs[Array[Byte]] shouldEqual bytes(2)
|
||||
}
|
||||
}
|
||||
|
||||
"return a partial response for a ranged request with a single range with undefined lastBytePosition" in {
|
||||
Get() ~> addHeader(Range(ByteRange.fromOffset(5))) ~> completeWithRangedBytes(10) ~> check {
|
||||
responseAs[Array[Byte]] shouldEqual Array[Byte](5, 6, 7, 8, 9)
|
||||
}
|
||||
}
|
||||
|
||||
"return a partial response for a ranged request with a single suffix range" in {
|
||||
Get() ~> addHeader(Range(ByteRange.suffix(1))) ~> completeWithRangedBytes(10) ~> check {
|
||||
responseAs[Array[Byte]] shouldEqual Array[Byte](9)
|
||||
}
|
||||
}
|
||||
|
||||
"return a partial response for a ranged request with a overlapping suffix range" in {
|
||||
Get() ~> addHeader(Range(ByteRange.suffix(100))) ~> completeWithRangedBytes(10) ~> check {
|
||||
responseAs[Array[Byte]] shouldEqual bytes(10)
|
||||
}
|
||||
}
|
||||
|
||||
"be transparent to non-GET requests" in {
|
||||
Post() ~> addHeader(Range(ByteRange(1, 2))) ~> completeWithRangedBytes(5) ~> check {
|
||||
responseAs[Array[Byte]] shouldEqual bytes(5)
|
||||
}
|
||||
}
|
||||
|
||||
"be transparent to non-200 responses" in {
|
||||
Get() ~> addHeader(Range(ByteRange(1, 2))) ~> Route.seal(wrs(reject())) ~> check {
|
||||
status == NotFound
|
||||
headers.exists { case `Content-Range`(_, _) ⇒ true; case _ ⇒ false } shouldEqual false
|
||||
}
|
||||
}
|
||||
|
||||
"reject an unsatisfiable single range" in {
|
||||
Get() ~> addHeader(Range(ByteRange(100, 200))) ~> completeWithRangedBytes(10) ~> check {
|
||||
rejection shouldEqual UnsatisfiableRangeRejection(Seq(ByteRange(100, 200)), 10)
|
||||
}
|
||||
}
|
||||
|
||||
"reject an unsatisfiable single suffix range with length 0" in {
|
||||
Get() ~> addHeader(Range(ByteRange.suffix(0))) ~> completeWithRangedBytes(42) ~> check {
|
||||
rejection shouldEqual UnsatisfiableRangeRejection(Seq(ByteRange.suffix(0)), 42)
|
||||
}
|
||||
}
|
||||
|
||||
"return a mediaType of 'multipart/byteranges' for a ranged request with multiple ranges" in {
|
||||
Get() ~> addHeader(Range(ByteRange(0, 10), ByteRange(0, 10))) ~> completeWithRangedBytes(10) ~> check {
|
||||
mediaType.withParams(Map.empty) shouldEqual MediaTypes.`multipart/byteranges`
|
||||
}
|
||||
}
|
||||
|
||||
"return a 'multipart/byteranges' for a ranged request with multiple coalesced ranges and expect ranges in ascending order" in {
|
||||
Get() ~> addHeader(Range(ByteRange(5, 10), ByteRange(0, 1), ByteRange(1, 2))) ~> {
|
||||
wrs { complete("Some random and not super short entity.") }
|
||||
} ~> check {
|
||||
header[`Content-Range`] should be(None)
|
||||
val parts = Await.result(responseAs[Multipart.ByteRanges].parts.grouped(1000).runWith(Sink.head), 1.second)
|
||||
parts.size shouldEqual 2
|
||||
inside(parts(0)) {
|
||||
case Multipart.ByteRanges.BodyPart(range, entity, unit, headers) ⇒
|
||||
range shouldEqual ContentRange.Default(0, 2, Some(39))
|
||||
unit shouldEqual RangeUnits.Bytes
|
||||
Await.result(entity.dataBytes.utf8String, 100.millis) shouldEqual "Som"
|
||||
}
|
||||
inside(parts(1)) {
|
||||
case Multipart.ByteRanges.BodyPart(range, entity, unit, headers) ⇒
|
||||
range shouldEqual ContentRange.Default(5, 10, Some(39))
|
||||
unit shouldEqual RangeUnits.Bytes
|
||||
Await.result(entity.dataBytes.utf8String, 100.millis) shouldEqual "random"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"return a 'multipart/byteranges' for a ranged request with multiple ranges if entity data source isn't reusable" in {
|
||||
val content = "Some random and not super short entity."
|
||||
def entityData() = StreamUtils.oneTimeSource(Source.single(ByteString(content)))
|
||||
|
||||
Get() ~> addHeader(Range(ByteRange(5, 10), ByteRange(0, 1), ByteRange(1, 2))) ~> {
|
||||
wrs { complete(HttpEntity.Default(MediaTypes.`text/plain`, content.length, entityData())) }
|
||||
} ~> check {
|
||||
header[`Content-Range`] should be(None)
|
||||
val parts = Await.result(responseAs[Multipart.ByteRanges].parts.grouped(1000).runWith(Sink.head), 1.second)
|
||||
parts.size shouldEqual 2
|
||||
}
|
||||
}
|
||||
|
||||
"reject a request with too many requested ranges" in {
|
||||
val ranges = (1 to 20).map(a ⇒ ByteRange.fromOffset(a))
|
||||
Get() ~> addHeader(Range(ranges)) ~> completeWithRangedBytes(100) ~> check {
|
||||
rejection shouldEqual TooManyRangesRejection(10)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import akka.http.scaladsl.model._
|
||||
import MediaTypes._
|
||||
import headers._
|
||||
import StatusCodes._
|
||||
|
||||
import akka.http.scaladsl.server._
|
||||
|
||||
class RespondWithDirectivesSpec extends RoutingSpec {
|
||||
|
||||
"overrideStatusCode" should {
|
||||
"set the given status on successful responses" in {
|
||||
Get() ~> {
|
||||
overrideStatusCode(Created) { completeOk }
|
||||
} ~> check { response shouldEqual HttpResponse(Created) }
|
||||
}
|
||||
"leave rejections unaffected" in {
|
||||
Get() ~> {
|
||||
overrideStatusCode(Created) { reject }
|
||||
} ~> check { rejections shouldEqual Nil }
|
||||
}
|
||||
}
|
||||
|
||||
val customHeader = RawHeader("custom", "custom")
|
||||
val customHeader2 = RawHeader("custom2", "custom2")
|
||||
val existingHeader = RawHeader("custom", "existing")
|
||||
|
||||
"respondWithHeader" should {
|
||||
val customHeader = RawHeader("custom", "custom")
|
||||
"add the given header to successful responses" in {
|
||||
Get() ~> {
|
||||
respondWithHeader(customHeader) { completeOk }
|
||||
} ~> check { response shouldEqual HttpResponse(headers = customHeader :: Nil) }
|
||||
}
|
||||
}
|
||||
"respondWithHeaders" should {
|
||||
"add the given headers to successful responses" in {
|
||||
Get() ~> {
|
||||
respondWithHeaders(customHeader, customHeader2) { completeOk }
|
||||
} ~> check { response shouldEqual HttpResponse(headers = customHeader :: customHeader2 :: Nil) }
|
||||
}
|
||||
}
|
||||
"respondWithDefaultHeader" should {
|
||||
def route(extraHeaders: HttpHeader*) = respondWithDefaultHeader(customHeader) {
|
||||
respondWithHeaders(extraHeaders: _*) {
|
||||
completeOk
|
||||
}
|
||||
}
|
||||
|
||||
"add the given header to a response if the header was missing before" in {
|
||||
Get() ~> route() ~> check { response shouldEqual HttpResponse(headers = customHeader :: Nil) }
|
||||
}
|
||||
"not change a response if the header already existed" in {
|
||||
Get() ~> route(existingHeader) ~> check { response shouldEqual HttpResponse(headers = existingHeader :: Nil) }
|
||||
}
|
||||
}
|
||||
"respondWithDefaultHeaders" should {
|
||||
def route(extraHeaders: HttpHeader*) = respondWithDefaultHeaders(customHeader, customHeader2) {
|
||||
respondWithHeaders(extraHeaders: _*) {
|
||||
completeOk
|
||||
}
|
||||
}
|
||||
|
||||
"add the given headers to a response if the header was missing before" in {
|
||||
Get() ~> route() ~> check { response shouldEqual HttpResponse(headers = customHeader :: customHeader2 :: Nil) }
|
||||
}
|
||||
"not update an existing header" in {
|
||||
Get() ~> route(existingHeader) ~> check {
|
||||
response shouldEqual HttpResponse(headers = List(customHeader2, existingHeader))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,148 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import org.scalatest.FreeSpec
|
||||
|
||||
import scala.concurrent.Promise
|
||||
import akka.http.scaladsl.marshallers.xml.ScalaXmlSupport._
|
||||
import akka.http.scaladsl.marshalling._
|
||||
import akka.http.scaladsl.server._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.impl.util._
|
||||
import headers._
|
||||
import StatusCodes._
|
||||
import MediaTypes._
|
||||
|
||||
class RouteDirectivesSpec extends FreeSpec with GenericRoutingSpec {
|
||||
|
||||
"The `complete` directive should" - {
|
||||
"by chainable with the `&` operator" in {
|
||||
Get() ~> (get & complete("yeah")) ~> check { responseAs[String] shouldEqual "yeah" }
|
||||
}
|
||||
"be lazy in its argument evaluation, independently of application style" in {
|
||||
var i = 0
|
||||
Put() ~> {
|
||||
get { complete { i += 1; "get" } } ~
|
||||
put { complete { i += 1; "put" } } ~
|
||||
(post & complete { i += 1; "post" })
|
||||
} ~> check {
|
||||
responseAs[String] shouldEqual "put"
|
||||
i shouldEqual 1
|
||||
}
|
||||
}
|
||||
"support completion from response futures" - {
|
||||
"simple case without marshaller" in {
|
||||
Get() ~> {
|
||||
get & complete(Promise.successful(HttpResponse(entity = "yup")).future)
|
||||
} ~> check { responseAs[String] shouldEqual "yup" }
|
||||
}
|
||||
"for successful futures and marshalling" in {
|
||||
Get() ~> complete(Promise.successful("yes").future) ~> check { responseAs[String] shouldEqual "yes" }
|
||||
}
|
||||
"for failed futures and marshalling" in {
|
||||
object TestException extends RuntimeException
|
||||
Get() ~> complete(Promise.failed[String](TestException).future) ~>
|
||||
check {
|
||||
status shouldEqual StatusCodes.InternalServerError
|
||||
responseAs[String] shouldEqual "There was an internal server error."
|
||||
}
|
||||
}
|
||||
"for futures failed with a RejectionError" in {
|
||||
Get() ~> complete(Promise.failed[String](RejectionError(AuthorizationFailedRejection)).future) ~>
|
||||
check {
|
||||
rejection shouldEqual AuthorizationFailedRejection
|
||||
}
|
||||
}
|
||||
}
|
||||
"allow easy handling of futured ToResponseMarshallers" in pending /*{
|
||||
trait RegistrationStatus
|
||||
case class Registered(name: String) extends RegistrationStatus
|
||||
case object AlreadyRegistered extends RegistrationStatus
|
||||
|
||||
val route =
|
||||
get {
|
||||
path("register" / Segment) { name ⇒
|
||||
def registerUser(name: String): Future[RegistrationStatus] = Future.successful {
|
||||
name match {
|
||||
case "otto" ⇒ AlreadyRegistered
|
||||
case _ ⇒ Registered(name)
|
||||
}
|
||||
}
|
||||
complete {
|
||||
registerUser(name).map[ToResponseMarshallable] {
|
||||
case Registered(_) ⇒ HttpEntity.Empty
|
||||
case AlreadyRegistered ⇒
|
||||
import spray.json.DefaultJsonProtocol._
|
||||
import spray.httpx.SprayJsonSupport._
|
||||
(StatusCodes.BadRequest, Map("error" -> "User already Registered"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Get("/register/otto") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
}
|
||||
Get("/register/karl") ~> route ~> check {
|
||||
status shouldEqual StatusCodes.OK
|
||||
entity shouldEqual HttpEntity.Empty
|
||||
}
|
||||
}*/
|
||||
"do Content-Type negotiation for multi-marshallers" in pendingUntilFixed {
|
||||
val route = get & complete(Data("Ida", 83))
|
||||
|
||||
import akka.http.scaladsl.model.headers.Accept
|
||||
Get().withHeaders(Accept(MediaTypes.`application/json`)) ~> route ~> check {
|
||||
responseAs[String] shouldEqual
|
||||
"""{
|
||||
| "name": "Ida",
|
||||
| "age": 83
|
||||
|}""".stripMarginWithNewline("\n")
|
||||
}
|
||||
Get().withHeaders(Accept(MediaTypes.`text/xml`)) ~> route ~> check {
|
||||
responseAs[xml.NodeSeq] shouldEqual <data><name>Ida</name><age>83</age></data>
|
||||
}
|
||||
pending
|
||||
/*Get().withHeaders(Accept(MediaTypes.`text/plain`)) ~> HttpService.sealRoute(route) ~> check {
|
||||
status shouldEqual StatusCodes.NotAcceptable
|
||||
}*/
|
||||
}
|
||||
}
|
||||
|
||||
"the redirect directive should" - {
|
||||
"produce proper 'Found' redirections" in {
|
||||
Get() ~> {
|
||||
redirect("/foo", Found)
|
||||
} ~> check {
|
||||
response shouldEqual HttpResponse(
|
||||
status = 302,
|
||||
entity = HttpEntity(`text/html`, "The requested resource temporarily resides under <a href=\"/foo\">this URI</a>."),
|
||||
headers = Location("/foo") :: Nil)
|
||||
}
|
||||
}
|
||||
|
||||
"produce proper 'NotModified' redirections" in {
|
||||
Get() ~> {
|
||||
redirect("/foo", NotModified)
|
||||
} ~> check { response shouldEqual HttpResponse(304, headers = Location("/foo") :: Nil) }
|
||||
}
|
||||
}
|
||||
|
||||
case class Data(name: String, age: Int)
|
||||
object Data {
|
||||
//import spray.json.DefaultJsonProtocol._
|
||||
//import spray.httpx.SprayJsonSupport._
|
||||
|
||||
val jsonMarshaller: ToEntityMarshaller[Data] = FIXME // jsonFormat2(Data.apply)
|
||||
val xmlMarshaller: ToEntityMarshaller[Data] = FIXME
|
||||
/*Marshaller.delegate[Data, xml.NodeSeq](MediaTypes.`text/xml`) { (data: Data) ⇒
|
||||
<data><name>{ data.name }</name><age>{ data.age }</age></data>
|
||||
}*/
|
||||
|
||||
implicit val dataMarshaller: ToResponseMarshaller[Data] = FIXME
|
||||
//ToResponseMarshaller.oneOf(MediaTypes.`application/json`, MediaTypes.`text/xml`)(jsonMarshaller, xmlMarshaller)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import akka.http.scaladsl.model.StatusCodes._
|
||||
|
||||
class SchemeDirectivesSpec extends RoutingSpec {
|
||||
"the extractScheme directive" should {
|
||||
"extract the Uri scheme" in {
|
||||
Put("http://localhost/", "Hello") ~> extractScheme { echoComplete } ~> check { responseAs[String] shouldEqual "http" }
|
||||
}
|
||||
}
|
||||
|
||||
"""the scheme("http") directive""" should {
|
||||
"let requests with an http Uri scheme pass" in {
|
||||
Put("http://localhost/", "Hello") ~> scheme("http") { completeOk } ~> check { response shouldEqual Ok }
|
||||
}
|
||||
"reject requests with an https Uri scheme" in {
|
||||
Get("https://localhost/") ~> scheme("http") { completeOk } ~> check { rejections shouldEqual List(SchemeRejection("http")) }
|
||||
}
|
||||
"cancel SchemeRejection if other scheme passed" in {
|
||||
val route =
|
||||
scheme("https") { completeOk } ~
|
||||
scheme("http") { reject }
|
||||
|
||||
Put("http://localhost/", "Hello") ~> route ~> check {
|
||||
rejections should be(Nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
"""the scheme("https") directive""" should {
|
||||
"let requests with an https Uri scheme pass" in {
|
||||
Put("https://localhost/", "Hello") ~> scheme("https") { completeOk } ~> check { response shouldEqual Ok }
|
||||
}
|
||||
"reject requests with an http Uri scheme" in {
|
||||
Get("http://localhost/") ~> scheme("https") { completeOk } ~> check { rejections shouldEqual List(SchemeRejection("https")) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server
|
||||
package directives
|
||||
|
||||
import scala.concurrent.Future
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.model.headers._
|
||||
import akka.http.scaladsl.server.AuthenticationFailedRejection.{ CredentialsRejected, CredentialsMissing }
|
||||
|
||||
class SecurityDirectivesSpec extends RoutingSpec {
|
||||
val dontAuth = authenticateBasicAsync[String]("MyRealm", _ ⇒ Future.successful(None))
|
||||
val doAuth = authenticateBasicPF("MyRealm", { case UserCredentials.Provided(name) ⇒ name })
|
||||
val authWithAnonymous = doAuth.withAnonymousUser("We are Legion")
|
||||
|
||||
val challenge = HttpChallenge("Basic", "MyRealm")
|
||||
|
||||
"basic authentication" should {
|
||||
"reject requests without Authorization header with an AuthenticationFailedRejection" in {
|
||||
Get() ~> {
|
||||
dontAuth { echoComplete }
|
||||
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsMissing, challenge) }
|
||||
}
|
||||
"reject unauthenticated requests with Authorization header with an AuthenticationFailedRejection" in {
|
||||
Get() ~> Authorization(BasicHttpCredentials("Bob", "")) ~> {
|
||||
dontAuth { echoComplete }
|
||||
} ~> check { rejection shouldEqual AuthenticationFailedRejection(CredentialsRejected, challenge) }
|
||||
}
|
||||
"reject requests with illegal Authorization header with 401" in {
|
||||
Get() ~> RawHeader("Authorization", "bob alice") ~> Route.seal {
|
||||
dontAuth { echoComplete }
|
||||
} ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
responseAs[String] shouldEqual "The resource requires authentication, which was not supplied with the request"
|
||||
header[`WWW-Authenticate`] shouldEqual Some(`WWW-Authenticate`(challenge))
|
||||
}
|
||||
}
|
||||
"extract the object representing the user identity created by successful authentication" in {
|
||||
Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> {
|
||||
doAuth { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "Alice" }
|
||||
}
|
||||
"extract the object representing the user identity created for the anonymous user" in {
|
||||
Get() ~> {
|
||||
authWithAnonymous { echoComplete }
|
||||
} ~> check { responseAs[String] shouldEqual "We are Legion" }
|
||||
}
|
||||
"properly handle exceptions thrown in its inner route" in {
|
||||
object TestException extends RuntimeException
|
||||
Get() ~> Authorization(BasicHttpCredentials("Alice", "")) ~> {
|
||||
Route.seal {
|
||||
doAuth { _ ⇒ throw TestException }
|
||||
}
|
||||
} ~> check { status shouldEqual StatusCodes.InternalServerError }
|
||||
}
|
||||
}
|
||||
"authentication directives" should {
|
||||
"properly stack" in {
|
||||
val otherChallenge = HttpChallenge("MyAuth", "MyRealm2")
|
||||
val otherAuth: Directive1[String] = authenticateOrRejectWithChallenge { (cred: Option[HttpCredentials]) ⇒
|
||||
Future.successful(Left(otherChallenge))
|
||||
}
|
||||
val bothAuth = dontAuth | otherAuth
|
||||
|
||||
Get() ~> Route.seal(bothAuth { echoComplete }) ~> check {
|
||||
status shouldEqual StatusCodes.Unauthorized
|
||||
headers.collect {
|
||||
case `WWW-Authenticate`(challenge +: Nil) ⇒ challenge
|
||||
} shouldEqual Seq(challenge, otherChallenge)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2015 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.directives
|
||||
|
||||
import scala.collection.immutable.Seq
|
||||
import akka.http.impl.engine.ws.InternalCustomHeader
|
||||
import akka.http.scaladsl.model.headers.{ UpgradeProtocol, Upgrade }
|
||||
import akka.http.scaladsl.model.{ HttpRequest, StatusCodes, HttpResponse }
|
||||
import akka.http.scaladsl.model.ws.{ Message, UpgradeToWebsocket }
|
||||
import akka.http.scaladsl.server.{ Route, RoutingSpec }
|
||||
import akka.stream.FlowMaterializer
|
||||
import akka.stream.scaladsl.Flow
|
||||
|
||||
class WebsocketDirectivesSpec extends RoutingSpec {
|
||||
"the handleWebsocketMessages directive" should {
|
||||
"handle websocket requests" in {
|
||||
Get("http://localhost/") ~> Upgrade(List(UpgradeProtocol("websocket"))) ~>
|
||||
emulateHttpCore ~> Route.seal(handleWebsocketMessages(Flow[Message])) ~>
|
||||
check {
|
||||
status shouldEqual StatusCodes.SwitchingProtocols
|
||||
}
|
||||
}
|
||||
"reject non-websocket requests" in {
|
||||
Get("http://localhost/") ~> emulateHttpCore ~> Route.seal(handleWebsocketMessages(Flow[Message])) ~> check {
|
||||
status shouldEqual StatusCodes.BadRequest
|
||||
responseAs[String] shouldEqual "Expected Websocket Upgrade request"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Only checks for upgrade header and then adds UpgradeToWebsocket mock header */
|
||||
def emulateHttpCore(req: HttpRequest): HttpRequest =
|
||||
req.header[Upgrade] match {
|
||||
case Some(upgrade) if upgrade.hasWebsocket ⇒ req.copy(headers = req.headers :+ upgradeToWebsocketHeaderMock)
|
||||
case _ ⇒ req
|
||||
}
|
||||
def upgradeToWebsocketHeaderMock: UpgradeToWebsocket =
|
||||
new InternalCustomHeader("UpgradeToWebsocketMock") with UpgradeToWebsocket {
|
||||
def requestedProtocols: Seq[String] = Nil
|
||||
|
||||
def handleMessages(handlerFlow: Flow[Message, Message, Any], subprotocol: Option[String])(implicit mat: FlowMaterializer): HttpResponse =
|
||||
HttpResponse(StatusCodes.SwitchingProtocols)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.server.util
|
||||
|
||||
import org.scalatest.{ Matchers, WordSpec }
|
||||
|
||||
class TupleOpsSpec extends WordSpec with Matchers {
|
||||
import TupleOps._
|
||||
|
||||
"The TupleOps" should {
|
||||
|
||||
"support folding over tuples using a binary poly-function" in {
|
||||
object Funky extends BinaryPolyFunc {
|
||||
implicit def step1 = at[Double, Int](_ + _)
|
||||
implicit def step2 = at[Double, Symbol]((d, s) ⇒ (d + s.name.tail.toInt).toByte)
|
||||
implicit def step3 = at[Byte, String]((byte, s) ⇒ byte + s.toLong)
|
||||
}
|
||||
(1, 'X2, "3").foldLeft(0.0)(Funky) shouldEqual 6L
|
||||
}
|
||||
|
||||
"support joining tuples" in {
|
||||
(1, 'X2, "3") join () shouldEqual (1, 'X2, "3")
|
||||
() join (1, 'X2, "3") shouldEqual (1, 'X2, "3")
|
||||
(1, 'X2, "3") join (4.0, 5L) shouldEqual (1, 'X2, "3", 4.0, 5L)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,289 @@
|
|||
/**
|
||||
* Copyright (C) 2009-2014 Typesafe Inc. <http://www.typesafe.com>
|
||||
*/
|
||||
|
||||
package akka.http.scaladsl.unmarshalling
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{ Future, Await }
|
||||
import org.scalatest.matchers.Matcher
|
||||
import org.scalatest.{ BeforeAndAfterAll, FreeSpec, Matchers }
|
||||
import akka.http.scaladsl.testkit.ScalatestUtils
|
||||
import akka.util.ByteString
|
||||
import akka.actor.ActorSystem
|
||||
import akka.stream.ActorFlowMaterializer
|
||||
import akka.stream.scaladsl._
|
||||
import akka.http.scaladsl.model._
|
||||
import akka.http.scaladsl.util.FastFuture._
|
||||
import akka.http.impl.util._
|
||||
import headers._
|
||||
import MediaTypes._
|
||||
|
||||
class UnmarshallingSpec extends FreeSpec with Matchers with BeforeAndAfterAll with ScalatestUtils {
|
||||
implicit val system = ActorSystem(getClass.getSimpleName)
|
||||
implicit val materializer = ActorFlowMaterializer()
|
||||
import system.dispatcher
|
||||
|
||||
"The PredefinedFromEntityUnmarshallers." - {
|
||||
"stringUnmarshaller should unmarshal `text/plain` content in UTF-8 to Strings" in {
|
||||
Unmarshal(HttpEntity("Hällö")).to[String] should evaluateTo("Hällö")
|
||||
}
|
||||
"charArrayUnmarshaller should unmarshal `text/plain` content in UTF-8 to char arrays" in {
|
||||
Unmarshal(HttpEntity("árvíztűrő ütvefúrógép")).to[Array[Char]] should evaluateTo("árvíztűrő ütvefúrógép".toCharArray)
|
||||
}
|
||||
}
|
||||
|
||||
"The MultipartUnmarshallers." - {
|
||||
|
||||
"multipartGeneralUnmarshaller should correctly unmarshal 'multipart/*' content with" - {
|
||||
"an empty part" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
|
||||
}
|
||||
"two empty parts" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|--XYZABC
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)),
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(ContentTypes.`text/plain(UTF-8)`)))
|
||||
}
|
||||
"a part without entity and missing header separation CRLF" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|Content-type: text/xml
|
||||
|Age: 12
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity.empty(MediaTypes.`text/xml`), List(Age(12))))
|
||||
}
|
||||
"an implicitly typed part (without headers)" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|
|
||||
|Perfectly fine part content.
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "Perfectly fine part content.")))
|
||||
}
|
||||
"one non-empty form-data part" in {
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
|
||||
"""---
|
||||
|Content-type: text/plain; charset=UTF8
|
||||
|content-disposition: form-data; name="email"
|
||||
|
|
||||
|test@there.com
|
||||
|-----""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "test@there.com"),
|
||||
List(`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")))))
|
||||
}
|
||||
"two different parts" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345",
|
||||
"""--12345
|
||||
|
|
||||
|first part, with a trailing newline
|
||||
|
|
||||
|--12345
|
||||
|Content-Type: application/octet-stream
|
||||
|Content-Transfer-Encoding: binary
|
||||
|
|
||||
|filecontent
|
||||
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, with a trailing newline\r\n")),
|
||||
Multipart.General.BodyPart.Strict(
|
||||
HttpEntity(`application/octet-stream`, "filecontent"),
|
||||
List(RawHeader("Content-Transfer-Encoding", "binary"))))
|
||||
}
|
||||
"illegal headers" in (
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|Date: unknown
|
||||
|content-disposition: form-data; name=email
|
||||
|
|
||||
|test@there.com
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(
|
||||
HttpEntity(ContentTypes.`text/plain(UTF-8)`, "test@there.com"),
|
||||
List(`Content-Disposition`(ContentDispositionTypes.`form-data`, Map("name" -> "email")),
|
||||
RawHeader("date", "unknown")))))
|
||||
"a full example (Strict)" in {
|
||||
Unmarshal(HttpEntity(`multipart/mixed` withBoundary "12345",
|
||||
"""preamble and
|
||||
|more preamble
|
||||
|--12345
|
||||
|
|
||||
|first part, implicitly typed
|
||||
|--12345
|
||||
|Content-Type: application/octet-stream
|
||||
|
|
||||
|second part, explicitly typed
|
||||
|--12345--
|
||||
|epilogue and
|
||||
|more epilogue""".stripMarginWithNewline("\r\n"))).to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")),
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "second part, explicitly typed")))
|
||||
}
|
||||
"a full example (Default)" in {
|
||||
val content = """preamble and
|
||||
|more preamble
|
||||
|--12345
|
||||
|
|
||||
|first part, implicitly typed
|
||||
|--12345
|
||||
|Content-Type: application/octet-stream
|
||||
|
|
||||
|second part, explicitly typed
|
||||
|--12345--
|
||||
|epilogue and
|
||||
|more epilogue""".stripMarginWithNewline("\r\n")
|
||||
val byteStrings = content.map(c ⇒ ByteString(c.toString)) // one-char ByteStrings
|
||||
Unmarshal(HttpEntity.Default(`multipart/mixed` withBoundary "12345", content.length, Source(byteStrings)))
|
||||
.to[Multipart.General] should haveParts(
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(ContentTypes.`text/plain(UTF-8)`, "first part, implicitly typed")),
|
||||
Multipart.General.BodyPart.Strict(HttpEntity(`application/octet-stream`, "second part, explicitly typed")))
|
||||
}
|
||||
}
|
||||
|
||||
"multipartGeneralUnmarshaller should reject illegal multipart content with" - {
|
||||
"an empty entity" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC", ByteString.empty))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity"
|
||||
}
|
||||
"an entity without initial boundary" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/mixed` withBoundary "XYZABC",
|
||||
"""this is
|
||||
|just preamble text""".stripMarginWithNewline("\r\n")))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Unexpected end of multipart entity"
|
||||
}
|
||||
"a stray boundary" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "ABC",
|
||||
"""--ABC
|
||||
|Content-type: text/plain; charset=UTF8
|
||||
|--ABCContent-type: application/json
|
||||
|content-disposition: form-data; name="email"
|
||||
|-----""".stripMarginWithNewline("\r\n")))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Illegal multipart boundary in message content"
|
||||
}
|
||||
"duplicate Content-Type header" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
|
||||
"""---
|
||||
|Content-type: text/plain; charset=UTF8
|
||||
|Content-type: application/json
|
||||
|content-disposition: form-data; name="email"
|
||||
|
|
||||
|test@there.com
|
||||
|-----""".stripMarginWithNewline("\r\n")))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual
|
||||
"multipart part must not contain more than one Content-Type header"
|
||||
}
|
||||
"a missing header-separating CRLF (in Strict entity)" in {
|
||||
Await.result(Unmarshal(HttpEntity(`multipart/form-data` withBoundary "-",
|
||||
"""---
|
||||
|not good here
|
||||
|-----""".stripMarginWithNewline("\r\n")))
|
||||
.to[Multipart.General].failed, 1.second).getMessage shouldEqual "Illegal character ' ' in header name"
|
||||
}
|
||||
"a missing header-separating CRLF (in Default entity)" in {
|
||||
val content = """---
|
||||
|
|
||||
|ok
|
||||
|---
|
||||
|not ok
|
||||
|-----""".stripMarginWithNewline("\r\n")
|
||||
val byteStrings = content.map(c ⇒ ByteString(c.toString)) // one-char ByteStrings
|
||||
val contentType = `multipart/form-data` withBoundary "-"
|
||||
Await.result(Unmarshal(HttpEntity.Default(contentType, content.length, Source(byteStrings)))
|
||||
.to[Multipart.General]
|
||||
.flatMap(_ toStrict 1.second).failed, 1.second).getMessage shouldEqual "Illegal character ' ' in header name"
|
||||
}
|
||||
}
|
||||
|
||||
"multipartByteRangesUnmarshaller should correctly unmarshal multipart/byteranges content with two different parts" in {
|
||||
Unmarshal(HttpEntity(`multipart/byteranges` withBoundary "12345",
|
||||
"""--12345
|
||||
|Content-Range: bytes 0-2/26
|
||||
|Content-Type: text/plain
|
||||
|
|
||||
|ABC
|
||||
|--12345
|
||||
|Content-Range: bytes 23-25/26
|
||||
|Content-Type: text/plain
|
||||
|
|
||||
|XYZ
|
||||
|--12345--""".stripMarginWithNewline("\r\n"))).to[Multipart.ByteRanges] should haveParts(
|
||||
Multipart.ByteRanges.BodyPart.Strict(ContentRange(0, 2, 26), HttpEntity(ContentTypes.`text/plain`, "ABC")),
|
||||
Multipart.ByteRanges.BodyPart.Strict(ContentRange(23, 25, 26), HttpEntity(ContentTypes.`text/plain`, "XYZ")))
|
||||
}
|
||||
|
||||
"multipartFormDataUnmarshaller should correctly unmarshal 'multipart/form-data' content" - {
|
||||
"with one element" in {
|
||||
Unmarshal(HttpEntity(`multipart/form-data` withBoundary "XYZABC",
|
||||
"""--XYZABC
|
||||
|content-disposition: form-data; name=email
|
||||
|
|
||||
|test@there.com
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n"))).to[Multipart.FormData] should haveParts(
|
||||
Multipart.FormData.BodyPart.Strict("email", HttpEntity(ContentTypes.`application/octet-stream`, "test@there.com")))
|
||||
}
|
||||
"with a file" in {
|
||||
Unmarshal {
|
||||
HttpEntity.Default(
|
||||
contentType = `multipart/form-data` withBoundary "XYZABC",
|
||||
contentLength = 1, // not verified during unmarshalling
|
||||
data = Source {
|
||||
List(
|
||||
ByteString {
|
||||
"""--XYZABC
|
||||
|Content-Disposition: form-data; name="email"
|
||||
|
|
||||
|test@there.com
|
||||
|--XYZABC
|
||||
|Content-Dispo""".stripMarginWithNewline("\r\n")
|
||||
},
|
||||
ByteString {
|
||||
"""sition: form-data; name="userfile"; filename="test.dat"
|
||||
|Content-Type: application/pdf
|
||||
|Content-Transfer-Encoding: binary
|
||||
|
|
||||
|filecontent
|
||||
|--XYZABC--""".stripMarginWithNewline("\r\n")
|
||||
})
|
||||
})
|
||||
}.to[Multipart.FormData].flatMap(_.toStrict(1.second)) should haveParts(
|
||||
Multipart.FormData.BodyPart.Strict("email", HttpEntity(ContentTypes.`application/octet-stream`, "test@there.com")),
|
||||
Multipart.FormData.BodyPart.Strict("userfile", HttpEntity(MediaTypes.`application/pdf`, "filecontent"),
|
||||
Map("filename" -> "test.dat"), List(RawHeader("Content-Transfer-Encoding", "binary"))))
|
||||
}
|
||||
// TODO: reactivate after multipart/form-data unmarshalling integrity verification is implemented
|
||||
//
|
||||
// "reject illegal multipart content" in {
|
||||
// val Left(MalformedContent(msg, _)) = HttpEntity(`multipart/form-data` withBoundary "XYZABC", "--noboundary--").as[MultipartFormData]
|
||||
// msg shouldEqual "Missing start boundary"
|
||||
// }
|
||||
// "reject illegal form-data content" in {
|
||||
// val Left(MalformedContent(msg, _)) = HttpEntity(`multipart/form-data` withBoundary "XYZABC",
|
||||
// """|--XYZABC
|
||||
// |content-disposition: form-data; named="email"
|
||||
// |
|
||||
// |test@there.com
|
||||
// |--XYZABC--""".stripMargin).as[MultipartFormData]
|
||||
// msg shouldEqual "Illegal multipart/form-data content: unnamed body part (no Content-Disposition header or no 'name' parameter)"
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
override def afterAll() = system.shutdown()
|
||||
|
||||
def haveParts[T <: Multipart](parts: Multipart.BodyPart.Strict*): Matcher[Future[T]] =
|
||||
equal(parts).matcher[Seq[Multipart.BodyPart.Strict]] compose { x ⇒
|
||||
Await.result(x
|
||||
.fast.flatMap {
|
||||
_.parts
|
||||
.mapAsync(1)(_ toStrict 1.second)
|
||||
.grouped(100)
|
||||
.runWith(Sink.head)
|
||||
}
|
||||
.fast.recover { case _: NoSuchElementException ⇒ Nil }, 1.second)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue