package net.redpipe.engine; import java.io.IOException; import java.nio.charset.Charset; import java.util.Base64; import java.util.concurrent.CountDownLatch; import javax.ws.rs.client.Client; import javax.ws.rs.client.ClientBuilder; import javax.ws.rs.client.WebTarget; import javax.ws.rs.core.HttpHeaders; import javax.ws.rs.core.Response.Status; import javax.ws.rs.sse.SseEventSource; import org.junit.After; import org.junit.Before; import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; import io.netty.handler.codec.http.cookie.ClientCookieDecoder; import io.netty.handler.codec.http.cookie.ClientCookieEncoder; import io.netty.handler.codec.http.cookie.Cookie; import io.vertx.core.json.JsonArray; import io.vertx.core.json.JsonObject; import io.vertx.ext.auth.shiro.ShiroAuthOptions; import io.vertx.ext.auth.shiro.ShiroAuthRealmType; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.VertxUnitRunner; import io.vertx.ext.web.client.WebClientOptions; import io.vertx.reactivex.core.buffer.Buffer; import io.vertx.reactivex.ext.auth.AuthProvider; import io.vertx.reactivex.ext.auth.shiro.ShiroAuth; import io.vertx.reactivex.ext.web.client.HttpRequest; import io.vertx.reactivex.ext.web.client.WebClient; import io.vertx.reactivex.ext.web.codec.BodyCodec; import io.vertx.reactivex.ext.web.handler.AuthHandler; import io.vertx.reactivex.ext.web.handler.BasicAuthHandler; import io.vertx.reactivex.ext.web.handler.UserSessionHandler; import net.redpipe.engine.core.AppGlobals; import net.redpipe.engine.core.Server; @RunWith(VertxUnitRunner.class) public class ApiTest { private final static String IGNORE = "**IGNORE**"; private Server server; private WebClient webClient; @Before public void prepare(TestContext context) throws IOException { Async async = context.async(); server = new Server() { @Override protected AuthProvider setupAuthenticationRoutes() { AppGlobals globals = AppGlobals.get(); AuthProvider auth = ShiroAuth.create(globals.getVertx(), new ShiroAuthOptions() .setType(ShiroAuthRealmType.PROPERTIES) .setConfig(new JsonObject())); globals.getRouter().route().handler(UserSessionHandler.create(auth)); AuthHandler authHandler = BasicAuthHandler.create(auth); globals.getRouter().route().handler(context -> { // only filter if we have a header, otherwise it will try to force auth, regardless if whether // we want auth if(context.request().getHeader(HttpHeaders.AUTHORIZATION) != null) { // make sure we pause until we're ready to read context.request().pause(); authHandler.handle(context); }else context.next(); }); globals.getRouter().route().handler(context -> { // unpause now that we have auth if(context.request().getHeader(HttpHeaders.AUTHORIZATION) != null) { context.request().resume(); } context.next(); }); return auth; } }; server.start(TestResource.class, TestResourceRxJava1.class) .subscribe(() -> { webClient = WebClient.create(server.getVertx(), new WebClientOptions().setDefaultHost("localhost").setDefaultPort(9000)); async.complete(); }, x -> { x.printStackTrace(); context.fail(x); async.complete(); }); } @After public void finish(TestContext context) { webClient.close(); Async async = context.async(); server.close().subscribe(() -> async.complete(), x -> { context.fail(x); async.complete(); }); } @Test public void checkSession(TestContext context) { Async async = context.async(); webClient .get("/hello") .as(BodyCodec.string()) .rxSend() .flatMap(r -> { String sessionCookie = r.getHeader("set-cookie"); context.assertNotNull(sessionCookie, "must have a session"); Cookie sessionCookieValue = ClientCookieDecoder.LAX.decode(sessionCookie); String sentSessionCookie = ClientCookieEncoder.LAX.encode(sessionCookieValue); return webClient.get("/hello") .putHeader("cookie", sentSessionCookie) .as(BodyCodec.string()) .rxSend() .flatMap(r2 -> { String sessionCookie2 = r2.getHeader("set-cookie"); context.assertNull(sessionCookie2, "reload does not recreate session"); return webClient.get("/does-not-exist") .putHeader("cookie", sentSessionCookie) .as(BodyCodec.string()) .rxSend(); }).map(r2 -> { String sessionCookie2 = r2.getHeader("set-cookie"); context.assertNull(sessionCookie2, "404 does not clear session"); return r.body(); }); }) .doOnError(x -> context.fail(x)) .subscribe(response -> { async.complete(); }); } @Test public void checkErrorCodeRespected(TestContext context) { checkRequest(Status.NOT_FOUND.getStatusCode(), IGNORE, "/does-not-exist", context); } @Test public void checkPlainHello(TestContext context) { checkRequest(Status.OK.getStatusCode(), "hello", "/hello", context); } @Test public void checkInject(TestContext context) { checkRequest(Status.OK.getStatusCode(), "ok", "/inject", context); } @Test public void checkInjectRx1(TestContext context) { checkRequest(Status.OK.getStatusCode(), "ok", "/rx1/inject", context); } @Test public void checkInjectUser(TestContext context) { checkRequest(Status.OK.getStatusCode(), "ok", "/inject-user", context, toAuth("root:w00t")); } @Test public void checkInjectUserRx1(TestContext context) { checkRequest(Status.OK.getStatusCode(), "ok", "/rx1/inject-user", context, toAuth("root:w00t")); } @Test public void checkInjectUserRequired(TestContext context) { checkRequest(Status.UNAUTHORIZED.getStatusCode(), "User required", "/inject-user", context); } @Test public void checkInjectUserRequiredRx1(TestContext context) { checkRequest(Status.UNAUTHORIZED.getStatusCode(), "User required", "/rx1/inject-user", context); } @Test public void checkInjectUserInvalid(TestContext context) { checkRequest(Status.UNAUTHORIZED.getStatusCode(), "Unauthorized", "/inject-user", context, toAuth("root:invalid")); } @Test public void checkInjectUserInvalidRx1(TestContext context) { checkRequest(Status.UNAUTHORIZED.getStatusCode(), "Unauthorized", "/rx1/inject-user", context, toAuth("root:invalid")); } @Ignore("Requires https://github.com/resteasy/Resteasy/pull/1596 merged and released") @Test public void checkAuthRoleForbidden(TestContext context) { checkRequest(Status.FORBIDDEN.getStatusCode(), null, "/auth-create", context, toAuth("bar:gee")); } @Ignore("Requires https://github.com/resteasy/Resteasy/pull/1596 merged and released") @Test public void checkAuthRoleForbiddenRx1(TestContext context) { checkRequest(Status.FORBIDDEN.getStatusCode(), null, "/rx1/auth-create", context, toAuth("bar:gee")); } @Test public void checkAuthRoleOK(TestContext context) { checkRequest(200, "ok", "/auth-create", context, toAuth("root:w00t")); } @Test public void checkAuthRoleOKRx1(TestContext context) { checkRequest(200, "ok", "/rx1/auth-create", context, toAuth("root:w00t")); } @Test public void checkAuthRoleCheckOK(TestContext context) { checkRequest(200, "true", "/auth-check", context, toAuth("root:w00t")); } @Test public void checkAuthRoleCheckOKRx1(TestContext context) { checkRequest(200, "true", "/rx1/auth-check", context, toAuth("root:w00t")); } @Test public void checkAuthRoleCheckDenied(TestContext context) { checkRequest(200, "false", "/auth-check", context, toAuth("bar:gee")); } @Test public void checkAuthRoleCheckDeniedRx1(TestContext context) { checkRequest(200, "false", "/rx1/auth-check", context, toAuth("bar:gee")); } private String toAuth(String userPass) { return "Basic "+Base64.getEncoder().encodeToString(userPass.getBytes(Charset.forName("us-ascii"))); } @Test public void checkHelloSingle(TestContext context) { checkRequest(200, "hello", "/hello-single", context); } @Test public void checkHelloSingleRx1(TestContext context) { checkRequest(200, "hello", "/rx1/hello-single", context); } @Test public void checkHelloObservable(TestContext context) { checkRequest(200, "onetwo", "/hello-observable", context); } @Test public void checkHelloObservableRx1(TestContext context) { checkRequest(200, "onetwo", "/rx1/hello-observable", context); } @Test public void checkHelloObservableCollect(TestContext context) { checkHelloObservableCollect("", context); } @Test public void checkHelloObservableCollectRx1(TestContext context) { checkHelloObservableCollect("/rx1", context); } private void checkHelloObservableCollect(String prefix, TestContext context) { Async async = context.async(); webClient .get(prefix+"/hello-observable-collect") .as(BodyCodec.jsonArray()) .rxSend() .map(r -> { context.assertEquals(new JsonArray().add("one").add("two"), r.body()); return r; }) .doOnError(x -> context.fail(x)) .subscribe(response -> { async.complete(); }); } @Test public void checkHelloObservableSSE(TestContext context) { checkHelloObservableSSE("", context); } @Test public void checkHelloObservableSSERx1(TestContext context) { checkHelloObservableSSE("/rx1", context); } private void checkHelloObservableSSE(String prefix, TestContext context) { CountDownLatch latch = new CountDownLatch(2); Client client = ClientBuilder.newClient(); WebTarget target = client.target("http://localhost:9000"+prefix+"/hello-observable-sse"); SseEventSource msgEventSource = SseEventSource.target(target).build(); try (SseEventSource eventSource = msgEventSource){ eventSource.register(event -> { if(latch.getCount() == 2) context.assertEquals("one", event.readData(String.class)); else context.assertEquals("two", event.readData(String.class)); latch.countDown(); }, ex -> { context.fail(ex); }); eventSource.open(); try { latch.await(); } catch (InterruptedException e) { e.printStackTrace(); } } } @Test public void checkCompletableRx1(TestContext context) { checkRequest(204, null, "/rx1/completable", context); } @Test public void checkCompletableRx2(TestContext context) { checkRequest(204, null, "/completable", context); } @Test public void checkEmptyMaybe(TestContext context) { checkRequest(404, null, "/maybe-empty", context); } @Test public void checkFulfilledMaybe(TestContext context) { checkRequest(200, "something", "/maybe-fulfilled", context); } @Test public void checkContextSingle(TestContext context) { checkRequest(200, "ok", "/context-single", context); } @Test public void checkContextSingleRx1(TestContext context) { checkRequest(200, "ok", "/rx1/context-single", context); } @Test public void checkContextCompletable(TestContext context) { checkRequest(204, null, "/context-completable", context); } @Test public void checkContextFlowable(TestContext context) { checkRequest(200, "ok", "/context-flowable", context); } @Test public void checkContextObservable(TestContext context) { checkRequest(200, "ok", "/context-observable", context); } @Test public void checkContextObservableRx1(TestContext context) { checkRequest(200, "ok", "/rx1/context-observable", context); } @Test public void checkContextMaybe(TestContext context) { checkRequest(200, "ok", "/context-maybe", context); } private void checkRequest(int expectedStatus, String expectedBody, String url, TestContext context) { checkRequest(expectedStatus, expectedBody, url, context, null); } private void checkRequest(int expectedStatus, String expectedBody, String url, TestContext context, String authHeader) { Async async = context.async(); HttpRequest<Buffer> request = webClient .get(url); if(authHeader != null) request.putHeader(HttpHeaders.AUTHORIZATION, authHeader); request.as(BodyCodec.string()) .rxSend() .map(r -> { context.assertEquals(expectedStatus, r.statusCode()); if(expectedBody != IGNORE) context.assertEquals(expectedBody, r.body()); return r; }) .doOnError(context::fail) .subscribe(response -> async.complete()); } }