/* * #%L * Wisdom-Framework * %% * Copyright (C) 2013 - 2014 Wisdom Framework * %% * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * #L% */ package org.wisdom.router; import com.google.common.base.Throwables; import com.google.common.collect.ImmutableList; import org.junit.After; import org.junit.Before; import org.junit.Test; import org.wisdom.api.Controller; import org.wisdom.api.DefaultController; import org.wisdom.api.concurrent.ExecutionContextService; import org.wisdom.api.concurrent.ManagedExecutorService; import org.wisdom.api.http.*; import org.wisdom.api.interception.Filter; import org.wisdom.api.interception.Interceptor; import org.wisdom.api.interception.RequestContext; import org.wisdom.api.router.Route; import org.wisdom.api.router.RouteBuilder; import org.wisdom.test.parents.FakeConfiguration; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.Collections; import java.util.Random; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.regex.Pattern; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; /** * Test the router implementation */ public class RouterTest { RequestRouter router = new RequestRouter(); Request request; @Before public void setUp() { request = mock(Request.class); when(request.contentMimeType()).thenReturn("text/plain"); Context context = mock(Context.class); when(context.request()).thenReturn(request); Context.CONTEXT.set(context); } @After public void tearDown() { Context.CONTEXT.remove(); } @Test public void simpleRoute() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "foo") )); router.bindController(controller); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request)).isNotNull(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).getControllerObject()).isEqualTo(controller); } @Test public void missingRoute() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "foo") )); router.bindController(controller); assertThat(router.getRouteFor(HttpMethod.GET, "/bar", request).isUnbound()).isTrue(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).getControllerObject()).isEqualTo(controller); } @Test public void routeMissingBecauseOfBadMethod() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "foo") )); router.bindController(controller); assertThat(router.getRouteFor(HttpMethod.PUT, "/foo", request).isUnbound()).isTrue(); assertThat(router.getRouteFor(HttpMethod.DELETE, "/foo", request).isUnbound()).isTrue(); assertThat(router.getRouteFor(HttpMethod.POST, "/foo", request).isUnbound()).isTrue(); } @Test public void routeMissingBecauseOfBrokenMethod() throws Exception { Controller controller = new DefaultController() { @org.wisdom.api.annotations.Route(method = HttpMethod.GET, uri = "/foo") public String hello() { return "hello"; } @org.wisdom.api.annotations.Route(method = HttpMethod.GET, uri = "/bar") public Result hello2() { return ok("hello"); } }; // Must not trow an exception. router.bindController(controller); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).isUnbound()).isTrue(); assertThat(router.getRouteFor(HttpMethod.GET, "/bar", request).isUnbound()).isTrue(); controller = new DefaultController() { @org.wisdom.api.annotations.Route(method = HttpMethod.GET, uri = "/foo") public Result hello() { return ok("hello"); } @org.wisdom.api.annotations.Route(method = HttpMethod.GET, uri = "/bar") public Result hello2() { return ok("hello"); } }; // Must not trow an exception. router.bindController(controller); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).isUnbound()).isFalse(); assertThat(router.getRouteFor(HttpMethod.GET, "/bar", request).isUnbound()).isFalse(); } @Test public void routeWithPathParameter() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo/{id}").to(controller, "foo") )); router.bindController(controller); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).isUnbound()).isTrue(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo/", request).isUnbound()).isTrue(); Route route = router.getRouteFor(HttpMethod.GET, "/foo/test", request); assertThat(route.isUnbound()).isFalse(); assertThat(route.getPathParametersEncoded("/foo/test").get("id")).isEqualToIgnoringCase("test"); } @Test public void routeWithTwoPathParameters() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo/{id}/{email}").to(controller, "foo") )); router.bindController(controller); Route route = router.getRouteFor(HttpMethod.GET, "/foo/1234/foo@aol.com", request); assertThat(route).isNotNull(); assertThat(route.getPathParametersEncoded("/foo/1234/foo@aol.com").get("id")).isEqualToIgnoringCase("1234"); assertThat(route.getPathParametersEncoded("/foo/1234/foo@aol.com").get("email")).isEqualToIgnoringCase ("foo@aol.com"); } /** * Test made to reproduce #248. */ @Test public void routeWithRegex() { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/{type<[0-9]+>}").to(controller, "foo") )); router.bindController(controller); Route route = router.getRouteFor(HttpMethod.GET, "/99", request); assertThat(route).isNotNull(); assertThat(route.getPathParametersEncoded("/99").get("type")).isEqualToIgnoringCase("99"); route = router.getRouteFor(HttpMethod.GET, "/xx", request); assertThat(route.isUnbound()).isTrue(); } @Test public void subRoute() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo/*").to(controller, "foo") )); router.bindController(controller); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).isUnbound()).isTrue(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo/bar", request)).isNotNull(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo/bar", request).getControllerObject()).isEqualTo(controller); assertThat(router.getRouteFor(HttpMethod.GET, "/foo/bar/baz", request)).isNotNull(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo/bar/baz", request).getControllerObject()).isEqualTo(controller); } @Test public void subRouteAsParameter() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo/{path+}").to(controller, "foo") )); router.bindController(controller); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).isUnbound()).isTrue(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo/", request).isUnbound()).isTrue(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo/bar", request).isUnbound()).isFalse(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo/bar", request).getControllerObject()).isEqualTo(controller); Route route = router.getRouteFor(HttpMethod.GET, "/foo/bar/baz", request); assertThat(route).isNotNull(); assertThat(route.getControllerObject()).isEqualTo(controller); assertThat(route.getPathParametersEncoded("/foo/bar/baz").get("path")).isEqualToIgnoringCase("bar/baz"); } @Test public void unbindTest() { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo/{path+}").to(controller, "foo") )); router.bindController(controller); assertThat(router.getRouteFor(HttpMethod.GET, "/foo/bar", request).isUnbound()).isFalse(); router.unbindController(controller); assertThat(router.getRouteFor(HttpMethod.GET, "/foo/bar", request).isUnbound()).isTrue(); } @Test public void testBindAndUnbindFilters() { Filter filter = new Filter() { @Override public Result call(Route route, RequestContext context) throws Exception { return null; } @Override public Pattern uri() { return null; } @Override public int priority() { return 0; } }; router.bindFilter(filter); assertThat(router.getFilters()).hasSize(1).contains(filter); router.unbindFilter(filter); assertThat(router.getFilters()).hasSize(0); } @Test public void testThatFiltersCannotBeAddedTwice() { Filter filter = new Filter() { @Override public Result call(Route route, RequestContext context) throws Exception { return null; } @Override public Pattern uri() { return null; } @Override public int priority() { return 0; } }; router.bindFilter(filter); assertThat(router.getFilters()).hasSize(1).contains(filter); router.bindFilter(filter); assertThat(router.getFilters()).hasSize(1).contains(filter); router.unbindFilter(filter); assertThat(router.getFilters()).hasSize(0); } @Test public void testBindAndUnbindFiltersWithProxy() { final Filter filter = new Filter() { @Override public Result call(Route route, RequestContext context) throws Exception { return null; } @Override public Pattern uri() { return null; } @Override public int priority() { return 0; } }; Filter proxy = (Filter) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Filter.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(filter, args); } }); router.bindFilter(proxy); assertThat(router.getFilters()).hasSize(1); assertThat(router.getDirectReferenceOnFilters().contains(proxy)).isTrue(); assertThat(router.getDirectReferenceOnFilters().contains(filter)).isTrue(); assertThat(router.getFilters().contains(filter)).isTrue(); router.unbindFilter(proxy); assertThat(router.getFilters()).hasSize(0); } @Test public void testThatProxiesCannotBeAddedTwice() { final Filter filter = new Filter() { @Override public Result call(Route route, RequestContext context) throws Exception { return null; } @Override public Pattern uri() { return null; } @Override public int priority() { return 0; } }; Filter proxy = (Filter) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Filter.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(filter, args); } }); Filter proxy2 = (Filter) Proxy.newProxyInstance(this.getClass().getClassLoader(), new Class[]{Filter.class}, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return method.invoke(filter, args); } }); router.bindFilter(proxy); assertThat(router.getFilters()).hasSize(1); assertThat(router.getDirectReferenceOnFilters().contains(proxy)).isTrue(); assertThat(router.getDirectReferenceOnFilters().contains(filter)).isTrue(); assertThat(router.getFilters().contains(filter)).isTrue(); router.bindFilter(proxy2); assertThat(router.getFilters()).hasSize(1); assertThat(router.getDirectReferenceOnFilters().contains(proxy)).isTrue(); assertThat(router.getDirectReferenceOnFilters().contains(filter)).isTrue(); assertThat(router.getFilters().contains(filter)).isTrue(); router.unbindFilter(proxy); assertThat(router.getFilters()).hasSize(0); } @Test public void testRouteSelectionAccordingToMimeType() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "bar").accepting("application/json"), new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "foo").accepting("text/plain") )); router.bindController(controller); // The request has a text/plain content: assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request)).isNotNull(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).getControllerMethod().getName()).isEqualTo("foo"); when(request.contentMimeType()).thenReturn("application/json"); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request)).isNotNull(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).getControllerMethod().getName()).isEqualTo ("bar"); when(request.contentMimeType()).thenReturn("application/bin"); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request)).isNotNull(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).isUnbound()).isEqualTo(true); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).getUnboundStatus()) .isEqualTo(Status.UNSUPPORTED_MEDIA_TYPE); } @Test public void testRouteSelectionAccordingToMimeTypeWithWildcards() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "bar").accepting("text/*"), new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "foo").accepting("text/plain") )); router.bindController(controller); // The request has a text/plain content: assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request)).isNotNull(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).getControllerMethod().getName()).isEqualTo("foo"); when(request.contentMimeType()).thenReturn("text/json"); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request)).isNotNull(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).getControllerMethod().getName()).isEqualTo ("bar"); when(request.contentMimeType()).thenReturn("application/bin"); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request)).isNotNull(); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).isUnbound()).isEqualTo(true); assertThat(router.getRouteFor(HttpMethod.GET, "/foo", request).getUnboundStatus()) .isEqualTo(Status.UNSUPPORTED_MEDIA_TYPE); } @Test public void testThatTheVaryHeaderIsSet() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "bar").accepting("application/json"), new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "foo").accepting("text/plain") )); router.bindController(controller); // The request has a text/plain content: Route actual = router.getRouteFor(HttpMethod.GET, "/foo", request); Result result = actual.invoke(); assertThat(result.getHeaders().get(HeaderNames.VARY)).isEqualTo(HeaderNames.CONTENT_TYPE); when(request.contentMimeType()).thenReturn("application/bin"); actual = router.getRouteFor(HttpMethod.GET, "/foo", request); result = actual.invoke(); assertThat(result.getStatusCode()).isEqualTo(Status.UNSUPPORTED_MEDIA_TYPE); } @Test public void testThatVaryIsNotSetWhenNotNeeded() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "bar") )); router.bindController(controller); // The request has a text/plain content: Route actual = router.getRouteFor(HttpMethod.GET, "/foo", request); Result result = actual.invoke(); assertThat(result.getHeaders().get(HeaderNames.VARY)).isNull(); } @Test public void testWeGetNotAcceptableWhenTheAcceptedTypeAreNotAccepted() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "bar").accepting("application/json") )); router.bindController(controller); Route actual = router.getRouteFor(HttpMethod.GET, "/foo", request); Result result = actual.invoke(); assertThat(result.getStatusCode()).isEqualTo(Status.UNSUPPORTED_MEDIA_TYPE); when(request.contentMimeType()).thenReturn("application/json"); actual = router.getRouteFor(HttpMethod.GET, "/foo", request); result = actual.invoke(); assertThat(result.getStatusCode()).isEqualTo(Status.OK); assertThat(result.getHeaders().get(HeaderNames.VARY)).isEqualTo(HeaderNames.CONTENT_TYPE); } @Test public void testThatProducedMimeTypeIsHandled() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo").to(controller, "foo").produces("text/foo"), new RouteBuilder().route(HttpMethod.GET).on("/bar").to(controller, "bar").produces("text/plain") )); router.bindController(controller); // The request has a text/plain content: Route actual = router.getRouteFor(HttpMethod.GET, "/foo", request); Result result = actual.invoke(); assertThat(result.getHeaders().get(HeaderNames.CONTENT_TYPE)).isEqualToIgnoringCase("text/foo"); } @Test public void testThatExactMatchAreUsed() throws Exception { FakeController controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo/stuff").to(controller, "foo"), new RouteBuilder().route(HttpMethod.GET).on("/foo/{id}").to(controller, "bar") )); router.bindController(controller); Route actual = router.getRouteFor(HttpMethod.GET, "/foo/stuff", request); Result result = actual.invoke(); assertThat(result.getStatusCode()).isEqualTo(Status.CREATED); actual = router.getRouteFor(HttpMethod.GET, "/foo/test", request); result = actual.invoke(); assertThat(result.getStatusCode()).isEqualTo(Status.OK); // Invert the declaration of the routes router.unbindController(controller); controller = new FakeController(); controller.setRoutes(ImmutableList.of( new RouteBuilder().route(HttpMethod.GET).on("/foo/{id}").to(controller, "bar"), new RouteBuilder().route(HttpMethod.GET).on("/foo/stuff").to(controller, "foo") )); router.bindController(controller); actual = router.getRouteFor(HttpMethod.GET, "/foo/stuff", request); result = actual.invoke(); assertThat(result.getStatusCode()).isEqualTo(Status.CREATED); actual = router.getRouteFor(HttpMethod.GET, "/foo/test", request); result = actual.invoke(); assertThat(result.getStatusCode()).isEqualTo(Status.OK); } @Test public void testConcurrencyForFilters() throws InterruptedException { RequestRouter router = new RequestRouter(); // Now start bunch of thread adding, every 10 addition, we iterate over the set. int num = 250; CountDownLatch startSignal = new CountDownLatch(1); CountDownLatch doneSignal = new CountDownLatch(num); ExecutorService executor = Executors.newFixedThreadPool(num); AtomicInteger success = new AtomicInteger(); Random random = new Random(); for (int i = 0; i < num; ++i) { final int id = i; executor.submit(() -> { try { Filter mock = createFakeFilter(random.nextInt()); startSignal.await(); router.bindFilter(mock); if (id % 10 == 0) { for (Filter filter : router.getFilters()) { assertThat(filter).isNotNull(); } router.getFilters().stream().forEach(f -> { assertThat(f).isNotNull(); }); } success.incrementAndGet(); } catch (Throwable e) { e.printStackTrace(); } finally { doneSignal.countDown(); } }); } startSignal.countDown(); doneSignal.await(10, TimeUnit.SECONDS); assertThat(success.get()).isEqualTo(num); assertThat(router.getFilters().size()).isEqualTo(num); } private Filter createFakeFilter(int priority) { return new Filter() { @Override public Result call(Route route, RequestContext context) throws Exception { return null; } @Override public Pattern uri() { return null; } @Override public int priority() { return priority; } }; } }