/* * Copyright 2002-2017 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 org.springframework.web.reactive.result.method; import java.lang.reflect.Method; import java.net.URI; import java.util.Arrays; import java.util.Collections; import java.util.EnumSet; import java.util.HashSet; import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import org.junit.Before; import org.junit.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; import org.springframework.mock.http.server.reactive.test.MockServerHttpRequest; import org.springframework.stereotype.Controller; import org.springframework.util.MultiValueMap; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.method.HandlerMethod; import org.springframework.web.reactive.BindingContext; import org.springframework.web.reactive.HandlerMapping; import org.springframework.web.reactive.HandlerResult; import org.springframework.web.reactive.result.method.RequestMappingInfo.*; import org.springframework.web.server.MethodNotAllowedException; import org.springframework.web.server.NotAcceptableStatusException; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.ServerWebInputException; import org.springframework.web.server.UnsupportedMediaTypeStatusException; import org.springframework.web.server.support.HttpRequestPathHelper; import static org.hamcrest.CoreMatchers.*; import static org.junit.Assert.*; import static org.springframework.mock.http.server.reactive.test.MockServerHttpRequest.*; import static org.springframework.web.bind.annotation.RequestMethod.*; import static org.springframework.web.method.MvcAnnotationPredicates.*; import static org.springframework.web.method.ResolvableMethod.*; import static org.springframework.web.reactive.result.method.RequestMappingInfo.*; /** * Unit tests for {@link RequestMappingInfoHandlerMapping}. * * @author Rossen Stoyanchev */ public class RequestMappingInfoHandlerMappingTests { private TestRequestMappingInfoHandlerMapping handlerMapping; @Before public void setup() throws Exception { this.handlerMapping = new TestRequestMappingInfoHandlerMapping(); this.handlerMapping.registerHandler(new TestController()); } @Test public void getMappingPathPatterns() throws Exception { String[] patterns = {"/foo/*", "/foo", "/bar/*", "/bar"}; RequestMappingInfo info = paths(patterns).build(); Set<String> actual = this.handlerMapping.getMappingPathPatterns(info); assertEquals(new HashSet<>(Arrays.asList(patterns)), actual); } @Test public void getHandlerDirectMatch() throws Exception { Method expected = on(TestController.class).annot(getMapping("/foo").params()).resolveMethod(); ServerWebExchange exchange = get("/foo").toExchange(); HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block(); assertEquals(expected, hm.getMethod()); } @Test public void getHandlerGlobMatch() throws Exception { Method expected = on(TestController.class).annot(requestMapping("/ba*").method(GET, HEAD)).resolveMethod(); ServerWebExchange exchange = get("/bar").toExchange(); HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block(); assertEquals(expected, hm.getMethod()); } @Test public void getHandlerEmptyPathMatch() throws Exception { Method expected = on(TestController.class).annot(requestMapping("")).resolveMethod(); ServerWebExchange exchange = get("").toExchange(); HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block(); assertEquals(expected, hm.getMethod()); exchange = get("/").toExchange(); hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block(); assertEquals(expected, hm.getMethod()); } @Test public void getHandlerBestMatch() throws Exception { Method expected = on(TestController.class).annot(getMapping("/foo").params("p")).resolveMethod(); ServerWebExchange exchange = get("/foo?p=anything").toExchange(); HandlerMethod hm = (HandlerMethod) this.handlerMapping.getHandler(exchange).block(); assertEquals(expected, hm.getMethod()); } @Test public void getHandlerRequestMethodNotAllowed() throws Exception { ServerWebExchange exchange = MockServerHttpRequest.post("/bar").toExchange(); Mono<Object> mono = this.handlerMapping.getHandler(exchange); assertError(mono, MethodNotAllowedException.class, ex -> assertEquals(EnumSet.of(HttpMethod.GET, HttpMethod.HEAD), ex.getSupportedMethods())); } @Test // SPR-9603 public void getHandlerRequestMethodMatchFalsePositive() throws Exception { ServerWebExchange exchange = get("/users").accept(MediaType.APPLICATION_XML).toExchange(); this.handlerMapping.registerHandler(new UserController()); Mono<Object> mono = this.handlerMapping.getHandler(exchange); StepVerifier.create(mono) .expectError(NotAcceptableStatusException.class) .verify(); } @Test // SPR-8462 public void getHandlerMediaTypeNotSupported() throws Exception { testHttpMediaTypeNotSupportedException("/person/1"); testHttpMediaTypeNotSupportedException("/person/1/"); testHttpMediaTypeNotSupportedException("/person/1.json"); } @Test public void getHandlerTestInvalidContentType() throws Exception { ServerWebExchange exchange = MockServerHttpRequest.put("/person/1").header("content-type", "bogus").toExchange(); Mono<Object> mono = this.handlerMapping.getHandler(exchange); assertError(mono, UnsupportedMediaTypeStatusException.class, ex -> assertEquals("Response status 415 with reason \"Invalid mime type \"bogus\": does not contain '/'\"", ex.getMessage())); } @Test // SPR-8462 public void getHandlerTestMediaTypeNotAcceptable() throws Exception { testMediaTypeNotAcceptable("/persons"); testMediaTypeNotAcceptable("/persons/"); testMediaTypeNotAcceptable("/persons.json"); } @Test // SPR-12854 public void getHandlerTestRequestParamMismatch() throws Exception { ServerWebExchange exchange = get("/params").toExchange(); Mono<Object> mono = this.handlerMapping.getHandler(exchange); assertError(mono, ServerWebInputException.class, ex -> { assertThat(ex.getReason(), containsString("[foo=bar]")); assertThat(ex.getReason(), containsString("[bar=baz]")); }); } @Test public void getHandlerHttpOptions() throws Exception { testHttpOptions("/foo", EnumSet.of(HttpMethod.GET, HttpMethod.HEAD)); testHttpOptions("/person/1", EnumSet.of(HttpMethod.PUT)); testHttpOptions("/persons", EnumSet.of(HttpMethod.GET, HttpMethod.HEAD, HttpMethod.POST, HttpMethod.PUT, HttpMethod.PATCH, HttpMethod.DELETE, HttpMethod.OPTIONS)); testHttpOptions("/something", EnumSet.of(HttpMethod.PUT, HttpMethod.POST)); } @Test public void getHandlerProducibleMediaTypesAttribute() throws Exception { ServerWebExchange exchange = get("/content").accept(MediaType.APPLICATION_XML).toExchange(); this.handlerMapping.getHandler(exchange).block(); String name = HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE; assertEquals(Collections.singleton(MediaType.APPLICATION_XML), exchange.getAttributes().get(name)); exchange = get("/content").accept(MediaType.APPLICATION_JSON).toExchange(); this.handlerMapping.getHandler(exchange).block(); assertNull("Negated expression shouldn't be listed as producible type", exchange.getAttributes().get(name)); } @Test @SuppressWarnings("unchecked") public void handleMatchUriTemplateVariables() throws Exception { String lookupPath = "/1/2"; ServerWebExchange exchange = get(lookupPath).toExchange(); RequestMappingInfo key = paths("/{path1}/{path2}").build(); this.handlerMapping.handleMatch(key, lookupPath, exchange); String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; Map<String, String> uriVariables = (Map<String, String>) exchange.getAttributes().get(name); assertNotNull(uriVariables); assertEquals("1", uriVariables.get("path1")); assertEquals("2", uriVariables.get("path2")); } @Test // SPR-9098 public void handleMatchUriTemplateVariablesDecode() throws Exception { RequestMappingInfo key = paths("/{group}/{identifier}").build(); URI url = URI.create("/group/a%2Fb"); ServerWebExchange exchange = MockServerHttpRequest.method(HttpMethod.GET, url).toExchange(); HttpRequestPathHelper pathHelper = new HttpRequestPathHelper(); pathHelper.setUrlDecode(false); String lookupPath = pathHelper.getLookupPathForRequest(exchange); this.handlerMapping.setPathHelper(pathHelper); this.handlerMapping.handleMatch(key, lookupPath, exchange); String name = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; @SuppressWarnings("unchecked") Map<String, String> uriVariables = (Map<String, String>) exchange.getAttributes().get(name); assertNotNull(uriVariables); assertEquals("group", uriVariables.get("group")); assertEquals("a/b", uriVariables.get("identifier")); } @Test public void handleMatchBestMatchingPatternAttribute() throws Exception { RequestMappingInfo key = paths("/{path1}/2", "/**").build(); ServerWebExchange exchange = get("/1/2").toExchange(); this.handlerMapping.handleMatch(key, "/1/2", exchange); assertEquals("/{path1}/2", exchange.getAttributes().get(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); } @Test public void handleMatchBestMatchingPatternAttributeNoPatternsDefined() throws Exception { RequestMappingInfo key = paths().build(); ServerWebExchange exchange = get("/1/2").toExchange(); this.handlerMapping.handleMatch(key, "/1/2", exchange); assertEquals("/1/2", exchange.getAttributes().get(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE)); } @Test public void handleMatchMatrixVariables() throws Exception { MultiValueMap<String, String> matrixVariables; Map<String, String> uriVariables; ServerWebExchange exchange = get("/").toExchange(); handleMatch(exchange, "/{cars}", "/cars;colors=red,blue,green;year=2012"); matrixVariables = getMatrixVariables(exchange, "cars"); uriVariables = getUriTemplateVariables(exchange); assertNotNull(matrixVariables); assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors")); assertEquals("2012", matrixVariables.getFirst("year")); assertEquals("cars", uriVariables.get("cars")); exchange = get("/").toExchange(); handleMatch(exchange, "/{cars:[^;]+}{params}", "/cars;colors=red,blue,green;year=2012"); matrixVariables = getMatrixVariables(exchange, "params"); uriVariables = getUriTemplateVariables(exchange); assertNotNull(matrixVariables); assertEquals(Arrays.asList("red", "blue", "green"), matrixVariables.get("colors")); assertEquals("2012", matrixVariables.getFirst("year")); assertEquals("cars", uriVariables.get("cars")); assertEquals(";colors=red,blue,green;year=2012", uriVariables.get("params")); exchange = get("/").toExchange(); handleMatch(exchange, "/{cars:[^;]+}{params}", "/cars"); matrixVariables = getMatrixVariables(exchange, "params"); uriVariables = getUriTemplateVariables(exchange); assertNull(matrixVariables); assertEquals("cars", uriVariables.get("cars")); assertEquals("", uriVariables.get("params")); } @Test public void handleMatchMatrixVariablesDecoding() throws Exception { HttpRequestPathHelper urlPathHelper = new HttpRequestPathHelper(); urlPathHelper.setUrlDecode(false); this.handlerMapping.setPathHelper(urlPathHelper); ServerWebExchange exchange = get("/").toExchange(); handleMatch(exchange, "/path{filter}", "/path;mvar=a%2fb"); MultiValueMap<String, String> matrixVariables = getMatrixVariables(exchange, "filter"); Map<String, String> uriVariables = getUriTemplateVariables(exchange); assertNotNull(matrixVariables); assertEquals(Collections.singletonList("a/b"), matrixVariables.get("mvar")); assertEquals(";mvar=a/b", uriVariables.get("filter")); } @SuppressWarnings("unchecked") private <T> void assertError(Mono<Object> mono, final Class<T> exceptionClass, final Consumer<T> consumer) { StepVerifier.create(mono) .consumeErrorWith(error -> { assertEquals(exceptionClass, error.getClass()); consumer.accept((T) error); }) .verify(); } private void testHttpMediaTypeNotSupportedException(String url) throws Exception { ServerWebExchange exchange = MockServerHttpRequest.put(url).contentType(MediaType.APPLICATION_JSON).toExchange(); Mono<Object> mono = this.handlerMapping.getHandler(exchange); assertError(mono, UnsupportedMediaTypeStatusException.class, ex -> assertEquals("Invalid supported consumable media types", Collections.singletonList(new MediaType("application", "xml")), ex.getSupportedMediaTypes())); } private void testHttpOptions(String requestURI, Set<HttpMethod> allowedMethods) throws Exception { ServerWebExchange exchange = MockServerHttpRequest.options(requestURI).toExchange(); HandlerMethod handlerMethod = (HandlerMethod) this.handlerMapping.getHandler(exchange).block(); BindingContext bindingContext = new BindingContext(); InvocableHandlerMethod invocable = new InvocableHandlerMethod(handlerMethod); Mono<HandlerResult> mono = invocable.invoke(exchange, bindingContext); HandlerResult result = mono.block(); assertNotNull(result); Optional<Object> value = result.getReturnValue(); assertTrue(value.isPresent()); assertEquals(HttpHeaders.class, value.get().getClass()); assertEquals(allowedMethods, ((HttpHeaders) value.get()).getAllow()); } private void testMediaTypeNotAcceptable(String url) throws Exception { ServerWebExchange exchange = get(url).accept(MediaType.APPLICATION_JSON).toExchange(); Mono<Object> mono = this.handlerMapping.getHandler(exchange); assertError(mono, NotAcceptableStatusException.class, ex -> assertEquals("Invalid supported producible media types", Collections.singletonList(new MediaType("application", "xml")), ex.getSupportedMediaTypes())); } private void handleMatch(ServerWebExchange exchange, String pattern, String lookupPath) { RequestMappingInfo info = paths(pattern).build(); this.handlerMapping.handleMatch(info, lookupPath, exchange); } @SuppressWarnings("unchecked") private MultiValueMap<String, String> getMatrixVariables(ServerWebExchange exchange, String uriVarName) { String attrName = HandlerMapping.MATRIX_VARIABLES_ATTRIBUTE; return ((Map<String, MultiValueMap<String, String>>) exchange.getAttributes().get(attrName)).get(uriVarName); } @SuppressWarnings("unchecked") private Map<String, String> getUriTemplateVariables(ServerWebExchange exchange) { String attrName = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE; return (Map<String, String>) exchange.getAttributes().get(attrName); } @SuppressWarnings("unused") @Controller private static class TestController { @GetMapping("/foo") public void foo() { } @GetMapping(path = "/foo", params="p") public void fooParam() { } @RequestMapping(path = "/ba*", method = { GET, HEAD }) public void bar() { } @RequestMapping(path = "") public void empty() { } @PutMapping(path = "/person/{id}", consumes="application/xml") public void consumes(@RequestBody String text) { } @RequestMapping(path = "/persons", produces="application/xml") public String produces() { return ""; } @RequestMapping(path = "/params", params="foo=bar") public String param() { return ""; } @RequestMapping(path = "/params", params="bar=baz") public String param2() { return ""; } @RequestMapping(path = "/content", produces="application/xml") public String xmlContent() { return ""; } @RequestMapping(path = "/content", produces="!application/xml") public String nonXmlContent() { return ""; } @RequestMapping(path = "/something", method = OPTIONS) public HttpHeaders fooOptions() { HttpHeaders headers = new HttpHeaders(); headers.add("Allow", "PUT,POST"); return headers; } } @SuppressWarnings("unused") @Controller private static class UserController { @GetMapping(path = "/users", produces = "application/json") public void getUser() { } @PutMapping(path = "/users") public void saveUser() { } } private static class TestRequestMappingInfoHandlerMapping extends RequestMappingInfoHandlerMapping { void registerHandler(Object handler) { super.detectHandlerMethods(handler); } @Override protected boolean isHandler(Class<?> beanType) { return AnnotationUtils.findAnnotation(beanType, RequestMapping.class) != null; } @Override protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) { RequestMapping annot = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class); if (annot != null) { BuilderConfiguration options = new BuilderConfiguration(); options.setPathHelper(getPathHelper()); options.setPathMatcher(getPathMatcher()); options.setSuffixPatternMatch(true); options.setTrailingSlashMatch(true); return paths(annot.value()).methods(annot.method()) .params(annot.params()).headers(annot.headers()) .consumes(annot.consumes()).produces(annot.produces()) .options(options).build(); } else { return null; } } } }