/* * Copyright (c) 2013-2014 the original author or authors * * 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. */ package io.werval.runtime.routes; import com.acme.app.FakeController; import io.werval.api.Application; import io.werval.api.Mode; import io.werval.api.exceptions.IllegalRouteException; import io.werval.api.http.Method; import io.werval.api.http.QueryString; import io.werval.api.http.RequestHeader; import io.werval.api.routes.ControllerParams; import io.werval.api.routes.Route; import io.werval.api.routes.RouteBuilder; import io.werval.api.routes.Routes; import io.werval.runtime.ApplicationInstance; import io.werval.runtime.http.CookiesInstance; import io.werval.runtime.http.HeadersInstance; import io.werval.runtime.http.QueryStringInstance; import io.werval.runtime.http.RequestHeaderInstance; import io.werval.util.URLs; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import org.junit.Test; import static io.werval.api.http.Method.GET; import static io.werval.api.http.Method.POST; import static io.werval.api.http.ProtocolVersion.HTTP_1_1; import static io.werval.api.routes.RouteBuilder.p; import static io.werval.util.Charsets.UTF_8; import static io.werval.util.Iterables.count; import static io.werval.util.Iterables.first; import static io.werval.util.Iterables.skip; import static java.util.Collections.emptyList; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; /** * Assert that Routes and Route types behave correctly and that RouteBuilder is able to parse all routes definitions. */ public class RoutesTest { @Test public void givenRoutesBuildFromCodeWhenToStringExpectCorrectOutput() { Route route = new RouteBuilderInstance().route( GET ) .on( "/foo/:id/bar/:slug" ) .to( FakeController.class, c -> c.another( p( "id", String.class ), p( "slug", Integer.class ) ) ) .modifiedBy( "service", "foo" ) .build(); assertThat( route.toString(), equalTo( "GET /foo/:id/bar/:slug com.acme.app.FakeController.another( String id, Integer slug ) service foo" ) ); } /** * Declarative setup of route parsing tests. */ @SuppressWarnings( "unchecked" ) public static enum RoutesToTest { // Simple routes SIMPLE_1( "GET / com.acme.app.FakeController.test()", GET, "/", FakeController.class, "test" ), SIMPLE_2( " POST /foo/bar com.acme.app.FakeController.test()", POST, "/foo/bar", FakeController.class, "test" ), // Modifiers MODIFIER_TRANSIENT( "GET / com.acme.app.FakeController.test() transient", GET, "/", FakeController.class, "test", Arrays.asList( "transient" ) ), MODIFIER_SERVICE( "GET / com.acme.app.FakeController.test() service", GET, "/", FakeController.class, "test", Arrays.asList( "service" ) ), // Controller params CONTROLLER_PARAMS_1( "GET /foo/:id/bar/:slug com.acme.app.FakeController.another(String id ,Integer slug )", GET, "/foo/:id/bar/:slug", FakeController.class, "another", new RoutesToTest.Params() { @Override public Map<String, Class<?>> params() { Map<String, Class<?>> params = new LinkedHashMap<>(); params.put( "id", String.class ); params.put( "slug", Integer.class ); return params; } } ), CONTROLLER_PARAMS_2( "GET /foo/bar/:slug/cathedral/:id com.acme.app.FakeController.another( String id, Integer slug )", GET, "/foo/bar/:slug/cathedral/:id", FakeController.class, "another", new RoutesToTest.Params() { @Override public Map<String, Class<?>> params() { Map<String, Class<?>> params = new LinkedHashMap<>(); params.put( "id", String.class ); params.put( "slug", Integer.class ); return params; } } ), // Wildcards WILDCARDS_1( "GET /static/*path com.acme.app.FakeController.wild( String path )", GET, "/static/*path", FakeController.class, "wild", new RoutesToTest.Params() { @Override public Map<String, Class<?>> params() { Map<String, Class<?>> params = new LinkedHashMap<>(); params.put( "path", String.class ); return params; } } ), WILDCARDS_2( "GET /d/*path/:slug com.acme.app.FakeController.another( String path, Integer slug )", GET, "/d/*path/:slug", FakeController.class, "another", new RoutesToTest.Params() { @Override public Map<String, Class<?>> params() { Map<String, Class<?>> params = new LinkedHashMap<>(); params.put( "path", String.class ); params.put( "slug", Integer.class ); return params; } } ), // Query string QUERY_STRING_1( "GET /nothing/at/all com.acme.app.FakeController.another( String id, Integer slug )", GET, "/nothing/at/all", FakeController.class, "another", new RoutesToTest.Params() { @Override public Map<String, Class<?>> params() { Map<String, Class<?>> params = new LinkedHashMap<>(); params.put( "id", String.class ); params.put( "slug", Integer.class ); return params; } } ), QUERY_STRING_2( "GET /foo/:id/bar com.acme.app.FakeController.another( String id, Integer slug )", GET, "/foo/:id/bar", FakeController.class, "another", new RoutesToTest.Params() { @Override public Map<String, Class<?>> params() { Map<String, Class<?>> params = new LinkedHashMap<>(); params.put( "id", String.class ); params.put( "slug", Integer.class ); return params; } } ), // No parenthesis NO_PARENTHESIS_1( " POST /foo/bar com.acme.app.FakeController.test", POST, "/foo/bar", FakeController.class, "test" ), NO_PARENTHESIS_2( " POST /foo/bar com.acme.app.FakeController.test transient", POST, "/foo/bar", FakeController.class, "test", Arrays.asList( "transient" ) ), // Wrong route strings WRONG_STRING_1( "WRONG /route", IllegalRouteException.class ), WRONG_STRING_2( "", IllegalRouteException.class ), WRONG_STRING_3( null, IllegalRouteException.class ), WRONG_STRING_4( "# GET / com.acme.app.FakeController.test()", IllegalRouteException.class ), WRONG_STRING_5( "GET foo/bar com.acme.app.FakeController.test()", IllegalRouteException.class ), // Wrong controllers WRONG_CONTROLLER_1( "GET /foo /bar com.acme.Controller.method()", IllegalRouteException.class ), WRONG_CONTROLLER_2( "GET / unknown.Type.method()", IllegalRouteException.class ), // Wrong methods WRONG_METHOD_1( "GET / com.acme.app.FakeController.unknownMethod()", IllegalRouteException.class ), WRONG_METHOD_2( "GET / com.acme.app.FakeController.test( WhatTheHeck param )", IllegalRouteException.class ), WRONG_METHOD_3( "GET / com.acme.app.FakeController.noOutcome", IllegalRouteException.class ), // Wrong parameters WRONG_PARAMS_1( "GET /foo/:id/bar/:slugf com.acme.app.FakeController.another( String id, Integer slug )", IllegalRouteException.class ), WRONG_PARAMS_2( "GET /foo/:idf/bar/:slug com.acme.app.FakeController.another( java.lang.String id, Integer slug )", IllegalRouteException.class ), WRONG_PARAMS_3( "GET /:wrong com.acme.app.FakeController.test()", IllegalRouteException.class ), WRONG_PARAMS_4( "GET /a/*path/:id/:slug com.acme.app.FakeController.another( String id, Integer slug )", IllegalRouteException.class ), WRONG_PARAMS_5( "GET /a/*path com.acme.app.FakeController.wild( path )", IllegalRouteException.class ), // Parameter is missing type info WRONG_PARAMS_6( "GET /foo/:slug/bar/:slug/cathedral/:id com.acme.app.FakeController.another( String id, Integer slug )", IllegalRouteException.class ), WRONG_PARAMS_99( "", IllegalRouteException.class ); // Members private String routeString; private Method httpMethod; private String path; private Class<?> controllerType; private String controllerMethod; private Map<String, Class<?>> parameters; private List<String> modifiers; private Class<? extends Exception> expectedException; private RoutesToTest( String routeString, Class<? extends Exception> expectedException ) { this.routeString = routeString; this.expectedException = expectedException; } private RoutesToTest( String routeString, Method httpMethod, String path, Class<?> controllerType, String controllerMethod ) { this.routeString = routeString; this.httpMethod = httpMethod; this.path = path; this.controllerType = controllerType; this.controllerMethod = controllerMethod; this.parameters = Collections.emptyMap(); this.modifiers = Collections.emptyList(); } private RoutesToTest( String routeString, Method httpMethod, String path, Class<?> controllerType, String controllerMethod, List<String> modifiers ) { this( routeString, httpMethod, path, controllerType, controllerMethod ); this.modifiers = modifiers; } private RoutesToTest( String routeString, Method httpMethod, String path, Class<?> controllerType, String controllerMethod, RoutesToTest.Params params ) { this( routeString, httpMethod, path, controllerType, controllerMethod ); this.parameters = params.params(); } public static interface Params { Map<String, Class<?>> params(); } } @Test public void givenUnderTestRoutesWhenParsingExpectCorrectResult() throws Exception { Application app = new ApplicationInstance( Mode.TEST, new RoutesParserProvider() ); RouteBuilder builder = new RouteBuilderInstance( app ); for( RoutesToTest refRoute : RoutesToTest.values() ) { System.out.println( "Parsing route: " + refRoute.routeString ); try { Route route = builder.parse().route( refRoute.routeString ); System.out.println( "Parsed route: " + route ); assertRoute( route, refRoute ); } catch( Exception ex ) { if( refRoute.expectedException == null || !refRoute.expectedException.isAssignableFrom( ex.getClass() ) ) { throw ex; } } } } @Test public void givenMultipleRoutesStringWhenParsingExpectCorrectRoutes() { Application app = new ApplicationInstance( Mode.TEST, new RoutesParserProvider( "\n" + RoutesToTest.SIMPLE_1.routeString + "\n\n \n# ignore me\n # me too \n" + RoutesToTest.SIMPLE_2.routeString + "\n" ) ); app.activate(); try { assertThat( count( app.routes() ), is( 2L ) ); Route one = first( app.routes() ); Route two = first( skip( 1, app.routes() ) ); assertRoute( one, RoutesToTest.SIMPLE_1 ); assertRoute( two, RoutesToTest.SIMPLE_2 ); } finally { app.passivate(); } } @Test public void givenRoutesWhenMatchingExpectCorrectRoutes() { Application app = new ApplicationInstance( Mode.TEST, new RoutesParserProvider() ); RouteBuilder builder = new RouteBuilderInstance( app ); Route index = builder.parse().route( "GET / " + FakeController.class.getName() + ".index()" ); Route foo = builder.parse().route( "GET /foo " + FakeController.class.getName() + ".foo()" ); Route bar = builder.parse().route( "GET /bar " + FakeController.class.getName() + ".bar()" ); Route another = builder.parse().route( "GET /foo/:id/bar/:slug " + FakeController.class.getName() + ".another(String id,Integer slug)" ); Route anotherOne = builder.parse().route( "GET /zeng/:id " + FakeController.class.getName() + ".another(String id,Integer slug)" ); Routes routes = new RoutesInstance( index, foo, bar, another, anotherOne ); assertThat( routes.route( reqHeadForGet( "/" ) ), equalTo( index ) ); assertThat( routes.route( reqHeadForGet( "/?a=b" ) ), equalTo( index ) ); assertThat( routes.route( reqHeadForGet( "/foo" ) ), equalTo( foo ) ); assertThat( routes.route( reqHeadForGet( "/bar" ) ), equalTo( bar ) ); assertThat( routes.route( reqHeadForGet( "/foo/1234567890/bar/42" ) ), equalTo( another ) ); assertThat( routes.route( reqHeadForGet( "/foo/1234567890/bar/42?a=b" ) ), equalTo( another ) ); assertThat( routes.route( reqHeadForGet( "/zeng/123?slug=qs" ) ), equalTo( anotherOne ) ); assertThat( routes.route( reqHeadForGet( "/zeng/123?slug=qs&a=b" ) ), equalTo( anotherOne ) ); } /* package */ static RequestHeader reqHeadForGet( String requestUri ) { QueryString.Decoder queryStringDecoder = new QueryString.Decoder( requestUri, UTF_8 ); String requestPath = URLs.decode( queryStringDecoder.path(), UTF_8 ); QueryString queryString = new QueryStringInstance( queryStringDecoder.parameters() ); return new RequestHeaderInstance( null, "identity", "127.0.0.1", false, false, emptyList(), HTTP_1_1, GET, requestUri, requestPath, queryString, new HeadersInstance(), new CookiesInstance() ); } private void assertRoute( Route route, RoutesToTest refRoute ) { String messageSuffix = " of " + refRoute.name() + "[" + route + "]"; assertThat( "HTTP Method" + messageSuffix, route.httpMethod(), equalTo( refRoute.httpMethod ) ); assertThat( "URI/Path" + messageSuffix, route.path(), equalTo( refRoute.path ) ); assertThat( "Controller Type" + messageSuffix, route.controllerType().getName(), equalTo( refRoute.controllerType.getName() ) ); assertThat( "Controller Method" + messageSuffix, route.controllerMethodName(), equalTo( refRoute.controllerMethod ) ); assertThat( "Parameters Count" + messageSuffix, count( ( (RouteInstance) route ).controllerParams().names() ), equalTo( count( refRoute.parameters.keySet() ) ) ); assertThat( "Modifiers Count" + messageSuffix, count( route.modifiers() ), equalTo( count( refRoute.modifiers ) ) ); ControllerParams routeParameters = ( (RouteInstance) route ).controllerParams(); Map<String, Class<?>> refRouteParameters = new LinkedHashMap<>( refRoute.parameters ); for( Entry<String, ControllerParams.Param> routeEntry : routeParameters.asMap().entrySet() ) { String routeParamName = routeEntry.getKey(); assertThat( "Parameter " + routeParamName + messageSuffix, routeEntry.getValue().type().getName(), equalTo( refRouteParameters.get( routeParamName ).getName() ) ); } List<String> routeModifiers = new ArrayList<>( route.modifiers() ); List<String> refRouteModifiers = new ArrayList<>( refRoute.modifiers ); for( int idx = 0; idx < routeModifiers.size(); idx++ ) { String routeModifier = routeModifiers.get( idx ); assertThat( "Modifier " + routeModifier + messageSuffix, routeModifier, equalTo( refRouteModifiers.get( idx ) ) ); } } }