/*
* 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;
}
}
}