/* * Copyright 2013-2016 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.cloud.netflix.feign.support; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.text.CollationElementIterator; import java.util.Collection; import java.util.List; import java.util.Map; import org.junit.Before; import org.junit.Test; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestParam; import com.fasterxml.jackson.annotation.JsonAutoDetect; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assume.assumeTrue; import feign.MethodMetadata; import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import lombok.ToString; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; /** * @author chadjaros */ public class SpringMvcContractTests { private static final Class<?> EXECUTABLE_TYPE; static { Class<?> executableType; try { executableType = Class.forName("java.lang.reflect.Executable"); } catch (ClassNotFoundException ex) { executableType = null; } EXECUTABLE_TYPE = executableType; } private SpringMvcContract contract; @Before public void setup() { this.contract = new SpringMvcContract(); } @Test public void testProcessAnnotationOnMethod_Simple() throws Exception { Method method = TestTemplate_Simple.class.getDeclaredMethod("getTest", String.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/test/{id}", data.template().url()); assertEquals("GET", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); } @Test public void testProcessAnnotations_Simple() throws Exception { Method method = TestTemplate_Simple.class.getDeclaredMethod("getTest", String.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/test/{id}", data.template().url()); assertEquals("GET", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); assertEquals("id", data.indexToName().get(0).iterator().next()); } @Test public void testProcessAnnotations_SimpleGetMapping() throws Exception { Method method = TestTemplate_Simple.class.getDeclaredMethod("getMappingTest", String.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/test/{id}", data.template().url()); assertEquals("GET", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); assertEquals("id", data.indexToName().get(0).iterator().next()); } @Test public void testProcessAnnotations_Class_AnnotationsGetSpecificTest() throws Exception { Method method = TestTemplate_Class_Annotations.class .getDeclaredMethod("getSpecificTest", String.class, String.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/prepend/{classId}/test/{testId}", data.template().url()); assertEquals("GET", data.template().method()); assertEquals("classId", data.indexToName().get(0).iterator().next()); assertEquals("testId", data.indexToName().get(1).iterator().next()); } @Test public void testProcessAnnotations_Class_AnnotationsGetAllTests() throws Exception { Method method = TestTemplate_Class_Annotations.class .getDeclaredMethod("getAllTests", String.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/prepend/{classId}", data.template().url()); assertEquals("GET", data.template().method()); assertEquals("classId", data.indexToName().get(0).iterator().next()); } @Test public void testProcessAnnotations_ExtendedInterface() throws Exception { Method extendedMethod = TestTemplate_Extended.class.getMethod("getAllTests", String.class); MethodMetadata extendedData = this.contract.parseAndValidateMetadata( extendedMethod.getDeclaringClass(), extendedMethod); Method method = TestTemplate_Class_Annotations.class .getDeclaredMethod("getAllTests", String.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals(extendedData.template().url(), data.template().url()); assertEquals(extendedData.template().method(), data.template().method()); assertEquals(data.indexToName().get(0).iterator().next(), data.indexToName().get(0).iterator().next()); } @Test public void testProcessAnnotations_SimplePost() throws Exception { Method method = TestTemplate_Simple.class.getDeclaredMethod("postTest", TestObject.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("", data.template().url()); assertEquals("POST", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); } @Test public void testProcessAnnotations_SimplePostMapping() throws Exception { Method method = TestTemplate_Simple.class.getDeclaredMethod("postMappingTest", TestObject.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("", data.template().url()); assertEquals("POST", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); } @Test public void testProcessAnnotationsOnMethod_Advanced() throws Exception { Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTest", String.class, String.class, Integer.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/advanced/test/{id}", data.template().url()); assertEquals("PUT", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); } @Test public void testProcessAnnotationsOnMethod_Advanced_UnknownAnnotation() throws Exception { Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTest", String.class, String.class, Integer.class); this.contract.parseAndValidateMetadata(method.getDeclaringClass(), method); // Don't throw an exception and this passes } @Test public void testProcessAnnotations_Advanced() throws Exception { Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTest", String.class, String.class, Integer.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/advanced/test/{id}", data.template().url()); assertEquals("PUT", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); assertEquals("Authorization", data.indexToName().get(0).iterator().next()); assertEquals("id", data.indexToName().get(1).iterator().next()); assertEquals("amount", data.indexToName().get(2).iterator().next()); assertNotNull(data.indexToExpander().get(2)); assertEquals("{Authorization}", data.template().headers().get("Authorization").iterator().next()); assertEquals("{amount}", data.template().queries().get("amount").iterator().next()); } @Test public void testProcessAnnotations_Aliased() throws Exception { Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTest2", String.class, Integer.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/advanced/test2", data.template().url()); assertEquals("PUT", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); assertEquals("Authorization", data.indexToName().get(0).iterator().next()); assertEquals("amount", data.indexToName().get(1).iterator().next()); assertEquals("{Authorization}", data.template().headers().get("Authorization").iterator().next()); assertEquals("{amount}", data.template().queries().get("amount").iterator().next()); } @Test public void testProcessAnnotations_Advanced2() throws Exception { Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTest"); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/advanced", data.template().url()); assertEquals("GET", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); } @Test public void testProcessAnnotations_Advanced3() throws Exception { Method method = TestTemplate_Simple.class.getDeclaredMethod("getTest"); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("", data.template().url()); assertEquals("GET", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); } @Test public void testProcessAnnotations_ListParams() throws Exception { Method method = TestTemplate_ListParams.class.getDeclaredMethod("getTest", List.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/test", data.template().url()); assertEquals("GET", data.template().method()); assertEquals("[{id}]", data.template().queries().get("id").toString()); assertNotNull(data.indexToExpander().get(0)); } @Test public void testProcessAnnotations_ListParamsWithoutName() throws Exception { Method method = TestTemplate_ListParamsWithoutName.class.getDeclaredMethod("getTest", List.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/test", data.template().url()); assertEquals("GET", data.template().method()); assertEquals("[{id}]", data.template().queries().get("id").toString()); assertNotNull(data.indexToExpander().get(0)); } @Test public void testProcessAnnotations_MapParams() throws Exception { Method method = TestTemplate_MapParams.class.getDeclaredMethod("getTest", Map.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/test", data.template().url()); assertEquals("GET", data.template().method()); assertNotNull(data.queryMapIndex()); assertEquals(0, data.queryMapIndex().intValue()); } @Test public void testProcessHeaders() throws Exception { Method method = TestTemplate_Headers.class.getDeclaredMethod("getTest", String.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/test/{id}", data.template().url()); assertEquals("GET", data.template().method()); assertEquals("bar", data.template().headers().get("X-Foo").iterator().next()); } @Test public void testProcessAnnotations_Fallback() throws Exception { Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTestFallback", String.class, String.class, Integer.class); assumeTrue("does not have java 8 parameter names", hasJava8ParameterNames(method)); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/advanced/testfallback/{id}", data.template().url()); assertEquals("PUT", data.template().method()); assertEquals(MediaType.APPLICATION_JSON_VALUE, data.template().headers().get("Accept").iterator().next()); assertEquals("Authorization", data.indexToName().get(0).iterator().next()); assertEquals("id", data.indexToName().get(1).iterator().next()); assertEquals("amount", data.indexToName().get(2).iterator().next()); assertEquals("{Authorization}", data.template().headers().get("Authorization").iterator().next()); assertEquals("{amount}", data.template().queries().get("amount").iterator().next()); } /** * For abstract (e.g. interface) methods, only Java 8 Parameter names (compiler arg * -parameters) can supply parameter names; bytecode-based strategies use local * variable declarations, of which there are none for abstract methods. * @param m * @return whether a parameter name was found * @throws IllegalArgumentException if method has no parameters */ private static boolean hasJava8ParameterNames(Method m) { org.springframework.util.Assert.isTrue(m.getParameterTypes().length > 0, "method has no parameters"); if (EXECUTABLE_TYPE != null) { Method getParameters = ReflectionUtils.findMethod(EXECUTABLE_TYPE, "getParameters"); try { Object[] parameters = (Object[]) getParameters.invoke(m); Method isNamePresent = ReflectionUtils .findMethod(parameters[0].getClass(), "isNamePresent"); return Boolean.TRUE.equals(isNamePresent.invoke(parameters[0])); } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) { } } return false; } @Test public void testProcessHeaderMap() throws Exception { Method method = TestTemplate_HeaderMap.class.getDeclaredMethod("headerMap", MultiValueMap.class, String.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/headerMap", data.template().url()); assertEquals("GET", data.template().method()); assertEquals(0, data.headerMapIndex().intValue()); Map<String, Collection<String>> headers = data.template().headers(); assertEquals("{aHeader}", headers.get("aHeader").iterator().next()); } @Test(expected = IllegalStateException.class) public void testProcessHeaderMapMoreThanOnce() throws Exception { Method method = TestTemplate_HeaderMap.class.getDeclaredMethod( "headerMapMoreThanOnce", MultiValueMap.class, MultiValueMap.class); this.contract.parseAndValidateMetadata(method.getDeclaringClass(), method); } @Test public void testProcessQueryMap() throws Exception { Method method = TestTemplate_QueryMap.class.getDeclaredMethod("queryMap", MultiValueMap.class, String.class); MethodMetadata data = this.contract .parseAndValidateMetadata(method.getDeclaringClass(), method); assertEquals("/queryMap", data.template().url()); assertEquals("GET", data.template().method()); assertEquals(0, data.queryMapIndex().intValue()); Map<String, Collection<String>> params = data.template().queries(); assertEquals("{aParam}", params.get("aParam").iterator().next()); } @Test(expected = IllegalStateException.class) public void testProcessQueryMapMoreThanOnce() throws Exception { Method method = TestTemplate_QueryMap.class.getDeclaredMethod( "queryMapMoreThanOnce", MultiValueMap.class, MultiValueMap.class); this.contract.parseAndValidateMetadata(method.getDeclaringClass(), method); } public interface TestTemplate_Simple { @RequestMapping(value = "/test/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) ResponseEntity<TestObject> getTest(@PathVariable("id") String id); @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) TestObject getTest(); @GetMapping(value = "/test/{id}", produces = MediaType.APPLICATION_JSON_VALUE) ResponseEntity<TestObject> getMappingTest(@PathVariable("id") String id); @RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE) TestObject postTest(@RequestBody TestObject object); @PostMapping(produces = MediaType.APPLICATION_JSON_VALUE) TestObject postMappingTest(@RequestBody TestObject object); } @RequestMapping("/prepend/{classId}") public interface TestTemplate_Class_Annotations { @RequestMapping(value = "/test/{testId}", method = RequestMethod.GET) TestObject getSpecificTest(@PathVariable("classId") String classId, @PathVariable("testId") String testId); @RequestMapping(method = RequestMethod.GET) TestObject getAllTests(@PathVariable("classId") String classId); } public interface TestTemplate_Extended extends TestTemplate_Class_Annotations { } public interface TestTemplate_Headers { @RequestMapping(value = "/test/{id}", method = RequestMethod.GET, headers = "X-Foo=bar") ResponseEntity<TestObject> getTest(@PathVariable("id") String id); } public interface TestTemplate_ListParams { @RequestMapping(value = "/test", method = RequestMethod.GET) ResponseEntity<TestObject> getTest(@RequestParam("id") List<String> id); } public interface TestTemplate_ListParamsWithoutName { @RequestMapping(value = "/test", method = RequestMethod.GET) ResponseEntity<TestObject> getTest(@RequestParam List<String> id); } public interface TestTemplate_MapParams { @RequestMapping(value = "/test", method = RequestMethod.GET) ResponseEntity<TestObject> getTest(@RequestParam Map<String, String> params); } public interface TestTemplate_HeaderMap { @RequestMapping(path = "/headerMap") String headerMap( @RequestHeader MultiValueMap<String, String> headerMap, @RequestHeader(name = "aHeader") String aHeader); @RequestMapping(path = "/headerMapMoreThanOnce") String headerMapMoreThanOnce( @RequestHeader MultiValueMap<String, String> headerMap1, @RequestHeader MultiValueMap<String, String> headerMap2); } public interface TestTemplate_QueryMap { @RequestMapping(path = "/queryMap") String queryMap( @RequestParam MultiValueMap<String, String> queryMap, @RequestParam(name = "aParam") String aParam); @RequestMapping(path = "/queryMapMoreThanOnce") String queryMapMoreThanOnce( @RequestParam MultiValueMap<String, String> queryMap1, @RequestParam MultiValueMap<String, String> queryMap2); } @JsonAutoDetect @RequestMapping("/advanced") public interface TestTemplate_Advanced { @ExceptionHandler @RequestMapping(path = "/test/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) ResponseEntity<TestObject> getTest(@RequestHeader("Authorization") String auth, @PathVariable("id") String id, @RequestParam("amount") Integer amount); @RequestMapping(path = "/test2", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) ResponseEntity<TestObject> getTest2( @RequestHeader(name = "Authorization") String auth, @RequestParam(name = "amount") Integer amount); @ExceptionHandler @RequestMapping(path = "/testfallback/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE) ResponseEntity<TestObject> getTestFallback(@RequestHeader String Authorization, @PathVariable String id, @RequestParam Integer amount); @RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) TestObject getTest(); } @AllArgsConstructor @NoArgsConstructor @ToString @JsonAutoDetect(fieldVisibility = JsonAutoDetect.Visibility.ANY, getterVisibility = JsonAutoDetect.Visibility.NONE, setterVisibility = JsonAutoDetect.Visibility.NONE) public class TestObject { public String something; public Double number; @Override public boolean equals(Object o) { if (this == o) { return true; } if (o == null || getClass() != o.getClass()) { return false; } TestObject that = (TestObject) o; if (this.number != null ? !this.number.equals(that.number) : that.number != null) { return false; } if (this.something != null ? !this.something.equals(that.something) : that.something != null) { return false; } return true; } @Override public int hashCode() { int result = (this.something != null ? this.something.hashCode() : 0); result = 31 * result + (this.number != null ? this.number.hashCode() : 0); return result; } } }