/* * 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.valid; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.text.ParseException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.cloud.netflix.feign.EnableFeignClients; import org.springframework.cloud.netflix.feign.FeignClient; import org.springframework.cloud.netflix.feign.FeignFormatterRegistrar; import org.springframework.cloud.netflix.feign.ribbon.LoadBalancerFeignClient; import org.springframework.cloud.netflix.feign.support.FallbackCommand; import org.springframework.cloud.netflix.ribbon.RibbonClient; import org.springframework.cloud.netflix.ribbon.RibbonClients; import org.springframework.cloud.netflix.ribbon.StaticServerList; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.format.Formatter; import org.springframework.format.FormatterRegistry; import org.springframework.http.HttpEntity; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; 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 org.springframework.web.bind.annotation.RestController; import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandKey; import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.ServerList; import feign.Client; import feign.Feign; import feign.Logger; import feign.RequestInterceptor; import feign.RequestTemplate; import feign.Target; import feign.hystrix.FallbackFactory; import feign.hystrix.SetterFactory; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import rx.Observable; import rx.Single; /** * @author Spencer Gibb * @author Jakub Narloch * @author Erik Kringen */ @RunWith(SpringJUnit4ClassRunner.class) @SpringBootTest(classes = FeignClientTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT, value = { "spring.application.name=feignclienttest", "logging.level.org.springframework.cloud.netflix.feign.valid=DEBUG", "feign.httpclient.enabled=false", "feign.okhttp.enabled=false", "feign.hystrix.enabled=true"}) @DirtiesContext public class FeignClientTests { public static final String HELLO_WORLD_1 = "hello world 1"; public static final String OI_TERRA_2 = "oi terra 2"; public static final String MYHEADER1 = "myheader1"; public static final String MYHEADER2 = "myheader2"; @Value("${local.server.port}") private int port = 0; @Autowired private TestClient testClient; @Autowired private TestClientServiceId testClientServiceId; @Autowired private DecodingTestClient decodingTestClient; @Autowired private Client feignClient; @Autowired HystrixClient hystrixClient; @Autowired private HystrixClientWithFallBackFactory hystrixClientWithFallBackFactory; @Autowired @Qualifier("localapp3FeignClient") HystrixClient namedHystrixClient; @Autowired HystrixSetterFactoryClient hystrixSetterFactoryClient; protected enum Arg { A, B; @Override public String toString() { return name().toLowerCase(Locale.ENGLISH); } } protected static class OtherArg { public final String value; public OtherArg(String value) { this.value = value; } @Override public String toString() { return value; } } @FeignClient(name = "localapp", configuration = TestClientConfig.class) protected interface TestClient { @RequestMapping(method = RequestMethod.GET, path = "/hello") Hello getHello(); @RequestMapping(method = RequestMethod.GET, path = "${feignClient.methodLevelRequestMappingPath}") Hello getHelloUsingPropertyPlaceHolder(); @RequestMapping(method = RequestMethod.GET, path = "/hello") Single<Hello> getHelloSingle(); @RequestMapping(method = RequestMethod.GET, path = "/hellos") List<Hello> getHellos(); @RequestMapping(method = RequestMethod.GET, path = "/hellostrings") List<String> getHelloStrings(); @RequestMapping(method = RequestMethod.GET, path = "/helloheaders") List<String> getHelloHeaders(); @RequestMapping(method = RequestMethod.GET, path = "/helloheadersplaceholders", headers = "myPlaceholderHeader=${feignClient.myPlaceholderHeader}") String getHelloHeadersPlaceholders(); @RequestMapping(method = RequestMethod.GET, path = "/helloparams") List<String> getParams(@RequestParam("params") List<String> params); @RequestMapping(method = RequestMethod.GET, path = "/hellos") HystrixCommand<List<Hello>> getHellosHystrix(); @RequestMapping(method = RequestMethod.GET, path = "/noContent") ResponseEntity<Void> noContent(); @RequestMapping(method = RequestMethod.HEAD, path = "/head") ResponseEntity<Void> head(); @RequestMapping(method = RequestMethod.GET, path = "/hello") HttpEntity<Hello> getHelloEntity(); @RequestMapping(method = RequestMethod.POST, consumes = "application/vnd.io.spring.cloud.test.v1+json", produces = "application/vnd.io.spring.cloud.test.v1+json", path = "/complex") String moreComplexContentType(String body); @RequestMapping(method = RequestMethod.GET, path = "/tostring") String getToString(@RequestParam("arg") Arg arg); @RequestMapping(method = RequestMethod.GET, path = "/tostring2") String getToString(@RequestParam("arg") OtherArg arg); @RequestMapping(method = RequestMethod.GET, path = "/tostringcollection") Collection<String> getToString(@RequestParam("arg") Collection<OtherArg> args); } public static class TestClientConfig { @Bean public RequestInterceptor interceptor1() { return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { template.header(MYHEADER1, "myheader1value"); } }; } @Bean public RequestInterceptor interceptor2() { return new RequestInterceptor() { @Override public void apply(RequestTemplate template) { template.header(MYHEADER2, "myheader2value"); } }; } } @FeignClient(name = "localapp1") protected interface TestClientServiceId { @RequestMapping(method = RequestMethod.GET, path = "/hello") Hello getHello(); } @FeignClient(name = "localapp2", decode404 = true) protected interface DecodingTestClient { @RequestMapping(method = RequestMethod.GET, path = "/notFound") ResponseEntity<String> notFound(); } @FeignClient(name = "localapp3", fallback = HystrixClientFallback.class) protected interface HystrixClient { @RequestMapping(method = RequestMethod.GET, path = "/fail") Single<Hello> failSingle(); @RequestMapping(method = RequestMethod.GET, path = "/fail") Hello fail(); @RequestMapping(method = RequestMethod.GET, path = "/fail") HystrixCommand<Hello> failCommand(); @RequestMapping(method = RequestMethod.GET, path = "/fail") Observable<Hello> failObservable(); @RequestMapping(method = RequestMethod.GET, path = "/fail") Future<Hello> failFuture(); } @FeignClient(name = "localapp4", fallbackFactory = HystrixClientFallbackFactory.class) protected interface HystrixClientWithFallBackFactory { @RequestMapping(method = RequestMethod.GET, path = "/fail") Hello fail(); } static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClientWithFallBackFactory> { @Override public HystrixClientWithFallBackFactory create(final Throwable cause) { return new HystrixClientWithFallBackFactory() { @Override public Hello fail() { assertNotNull("Cause was null", cause); return new Hello("Hello from the fallback side: " + cause.getMessage()); } }; } } static class HystrixClientFallback implements HystrixClient { @Override public Hello fail() { return new Hello("fallback"); } @Override public Single<Hello> failSingle() { return Single.just(new Hello("fallbacksingle")); } @Override public HystrixCommand<Hello> failCommand() { return new FallbackCommand<>(new Hello("fallbackcommand")); } @Override public Observable<Hello> failObservable() { return Observable.just(new Hello("fallbackobservable")); } @Override public Future<Hello> failFuture() { return new FallbackCommand<>(new Hello("fallbackfuture")).queue(); } } @FeignClient(name = "localapp5", configuration = TestHystrixSetterFactoryClientConfig.class) protected interface HystrixSetterFactoryClient { @RequestMapping(method = RequestMethod.GET, path = "/hellos") HystrixCommand<List<Hello>> getHellosHystrix(); } public static class TestHystrixSetterFactoryClientConfig { public static final String SETTER_PREFIX = "SETTER-"; @Bean public SetterFactory commandKeyIsRequestLineSetterFactory() { return new SetterFactory() { @Override public HystrixCommand.Setter create(Target<?> target, Method method) { String groupKey = SETTER_PREFIX + target.name(); RequestMapping requestMapping = method .getAnnotation(RequestMapping.class); String commandKey = SETTER_PREFIX + requestMapping.method()[0] + " " + requestMapping .path()[0]; return HystrixCommand.Setter .withGroupKey(HystrixCommandGroupKey.Factory.asKey(groupKey)) .andCommandKey(HystrixCommandKey.Factory.asKey(commandKey)); } }; } } @Configuration @EnableAutoConfiguration @RestController @EnableFeignClients(clients = { TestClientServiceId.class, TestClient.class, DecodingTestClient.class, HystrixClient.class, HystrixClientWithFallBackFactory.class, HystrixSetterFactoryClient.class}, defaultConfiguration = TestDefaultFeignConfig.class) @RibbonClients({ @RibbonClient(name = "localapp", configuration = LocalRibbonClientConfiguration.class), @RibbonClient(name = "localapp1", configuration = LocalRibbonClientConfiguration.class), @RibbonClient(name = "localapp2", configuration = LocalRibbonClientConfiguration.class), @RibbonClient(name = "localapp3", configuration = LocalRibbonClientConfiguration.class), @RibbonClient(name = "localapp4", configuration = LocalRibbonClientConfiguration.class), @RibbonClient(name = "localapp5", configuration = LocalRibbonClientConfiguration.class) }) protected static class Application { // needs to be in parent context to test multiple HystrixClient beans @Bean public HystrixClientFallback hystrixClientFallback() { return new HystrixClientFallback(); } @Bean public HystrixClientFallbackFactory hystrixClientFallbackFactory() { return new HystrixClientFallbackFactory(); } @Bean FeignFormatterRegistrar feignFormatterRegistrar() { return new FeignFormatterRegistrar() { @Override public void registerFormatters(FormatterRegistry registry) { registry.addFormatter(new Formatter<OtherArg>() { @Override public String print(OtherArg object, Locale locale) { if("foo".equals(object.value)) { return "bar"; } return object.value; } @Override public OtherArg parse(String text, Locale locale) throws ParseException { return new OtherArg(text); } }); } }; } @RequestMapping(method = RequestMethod.GET, path = "/hello") public Hello getHello() { return new Hello(HELLO_WORLD_1); } @RequestMapping(method = RequestMethod.GET, path = "/hello2") public Hello getHello2() { return new Hello(OI_TERRA_2); } @RequestMapping(method = RequestMethod.GET, path = "/hellos") public List<Hello> getHellos() { ArrayList<Hello> hellos = getHelloList(); return hellos; } @RequestMapping(method = RequestMethod.GET, path = "/hellostrings") public List<String> getHelloStrings() { ArrayList<String> hellos = new ArrayList<>(); hellos.add(HELLO_WORLD_1); hellos.add(OI_TERRA_2); return hellos; } @RequestMapping(method = RequestMethod.GET, path = "/helloheaders") public List<String> getHelloHeaders(@RequestHeader(MYHEADER1) String myheader1, @RequestHeader(MYHEADER2) String myheader2) { ArrayList<String> headers = new ArrayList<>(); headers.add(myheader1); headers.add(myheader2); return headers; } @RequestMapping(method = RequestMethod.GET, path = "/helloheadersplaceholders") public String getHelloHeadersPlaceholders( @RequestHeader("myPlaceholderHeader") String myPlaceholderHeader) { return myPlaceholderHeader; } @RequestMapping(method = RequestMethod.GET, path = "/helloparams") public List<String> getParams(@RequestParam("params") List<String> params) { return params; } @RequestMapping(method = RequestMethod.GET, path = "/noContent") ResponseEntity<Void> noContent() { return ResponseEntity.noContent().build(); } @RequestMapping(method = RequestMethod.HEAD, path = "/head") ResponseEntity<Void> head() { return ResponseEntity.ok().build(); } @RequestMapping(method = RequestMethod.GET, path = "/fail") String fail() { throw new RuntimeException("always fails"); } @RequestMapping(method = RequestMethod.GET, path = "/notFound") ResponseEntity<String> notFound() { return ResponseEntity.status(HttpStatus.NOT_FOUND).body((String) null); } @RequestMapping(method = RequestMethod.POST, consumes = "application/vnd.io.spring.cloud.test.v1+json", produces = "application/vnd.io.spring.cloud.test.v1+json", path = "/complex") String complex(@RequestBody String body, @RequestHeader("Content-Length") int contentLength) { if (contentLength <= 0) { throw new IllegalArgumentException("Invalid Content-Length "+ contentLength); } return body; } @RequestMapping(method = RequestMethod.GET, path = "/tostring") String getToString(@RequestParam("arg") Arg arg) { return arg.toString(); } @RequestMapping(method = RequestMethod.GET, path = "/tostring2") String getToString(@RequestParam("arg") OtherArg arg) { return arg.value; } @RequestMapping(method = RequestMethod.GET, path = "/tostringcollection") Collection<String> getToString(@RequestParam("arg") Collection<OtherArg> args) { List<String> result = new ArrayList<>(); for(OtherArg arg : args) { result.add(arg.value); } return result; } public static void main(String[] args) { new SpringApplicationBuilder(Application.class) .properties("spring.application.name=feignclienttest", "management.contextPath=/admin") .run(args); } } private static ArrayList<Hello> getHelloList() { ArrayList<Hello> hellos = new ArrayList<>(); hellos.add(new Hello(HELLO_WORLD_1)); hellos.add(new Hello(OI_TERRA_2)); return hellos; } @Test public void testClient() { assertNotNull("testClient was null", this.testClient); assertTrue("testClient is not a java Proxy", Proxy.isProxyClass(this.testClient.getClass())); InvocationHandler invocationHandler = Proxy.getInvocationHandler(this.testClient); assertNotNull("invocationHandler was null", invocationHandler); } @Test public void testRequestMappingClassLevelPropertyReplacement() { Hello hello = this.testClient.getHelloUsingPropertyPlaceHolder(); assertNotNull("hello was null", hello); assertEquals("first hello didn't match", new Hello(OI_TERRA_2), hello); } @Test public void testSimpleType() { Hello hello = this.testClient.getHello(); assertNotNull("hello was null", hello); assertEquals("first hello didn't match", new Hello(HELLO_WORLD_1), hello); } @Test public void testGenericType() { List<Hello> hellos = this.testClient.getHellos(); assertNotNull("hellos was null", hellos); assertEquals("hellos didn't match", hellos, getHelloList()); } @Test public void testRequestInterceptors() { List<String> headers = this.testClient.getHelloHeaders(); assertNotNull("headers was null", headers); assertTrue("headers didn't contain myheader1value", headers.contains("myheader1value")); assertTrue("headers didn't contain myheader2value", headers.contains("myheader2value")); } @Test public void testHeaderPlaceholders() { String header = this.testClient.getHelloHeadersPlaceholders(); assertNotNull("header was null", header); assertEquals("header was wrong", "myPlaceholderHeaderValue", header); } @Test public void testFeignClientType() throws IllegalAccessException { assertThat(this.feignClient, is(instanceOf(LoadBalancerFeignClient.class))); LoadBalancerFeignClient client = (LoadBalancerFeignClient) this.feignClient; Client delegate = client.getDelegate(); assertThat(delegate, is(instanceOf(feign.Client.Default.class))); } @Test public void testServiceId() { assertNotNull("testClientServiceId was null", this.testClientServiceId); final Hello hello = this.testClientServiceId.getHello(); assertNotNull("The hello response was null", hello); assertEquals("first hello didn't match", new Hello(HELLO_WORLD_1), hello); } @Test public void testParams() { List<String> list = Arrays.asList("a", "1", "test"); List<String> params = this.testClient.getParams(list); assertNotNull("params was null", params); assertEquals("params size was wrong", list.size(), params.size()); } @Test public void testHystrixCommand() throws NoSuchMethodException { HystrixCommand<List<Hello>> command = this.testClient.getHellosHystrix(); assertNotNull("command was null", command); assertEquals( "Hystrix command group name should match the name of the feign client", "localapp", command.getCommandGroup().name()); String configKey = Feign.configKey(TestClient.class, TestClient.class.getMethod("getHellosHystrix", (Class<?>[]) null)); assertEquals("Hystrix command key name should match the feign config key", configKey, command.getCommandKey().name()); List<Hello> hellos = command.execute(); assertNotNull("hellos was null", hellos); assertEquals("hellos didn't match", hellos, getHelloList()); } @Test public void testSingle() { Single<Hello> single = this.testClient.getHelloSingle(); assertNotNull("single was null", single); Hello hello = single.toBlocking().value(); assertNotNull("hello was null", hello); assertEquals("first hello didn't match", new Hello(HELLO_WORLD_1), hello); } @Test public void testNoContentResponse() { ResponseEntity<Void> response = testClient.noContent(); assertNotNull("response was null", response); assertEquals("status code was wrong", HttpStatus.NO_CONTENT, response.getStatusCode()); } @Test public void testHeadResponse() { ResponseEntity<Void> response = testClient.head(); assertNotNull("response was null", response); assertEquals("status code was wrong", HttpStatus.OK, response.getStatusCode()); } @Test public void testHttpEntity() { HttpEntity<Hello> entity = testClient.getHelloEntity(); assertNotNull("entity was null", entity); Hello hello = entity.getBody(); assertNotNull("hello was null", hello); assertEquals("first hello didn't match", new Hello(HELLO_WORLD_1), hello); } @Test public void testMoreComplexHeader() { String response = testClient.moreComplexContentType("{\"value\":\"OK\"}"); assertNotNull("response was null", response); assertEquals("didn't respond with {\"value\":\"OK\"}", "{\"value\":\"OK\"}", response); } @Test public void testDecodeNotFound() { ResponseEntity<String> response = decodingTestClient.notFound(); assertNotNull("response was null", response); assertEquals("status code was wrong", HttpStatus.NOT_FOUND, response.getStatusCode()); assertNull("response body was not null", response.getBody()); } @Test public void testConvertingExpander() { assertEquals(Arg.A.toString(), testClient.getToString(Arg.A)); assertEquals(Arg.B.toString(), testClient.getToString(Arg.B)); assertEquals("bar", testClient.getToString(new OtherArg("foo"))); List<OtherArg> args = new ArrayList<>(); args.add(new OtherArg("foo")); args.add(new OtherArg("goo")); List<String> expectedResult = new ArrayList<>(); expectedResult.add("bar"); expectedResult.add("goo"); assertEquals(expectedResult, testClient.getToString(args)); } @Test public void testHystrixFallbackWorks() { Hello hello = hystrixClient.fail(); assertNotNull("hello was null", hello); assertEquals("message was wrong", "fallback", hello.getMessage()); } @Test public void testHystrixFallbackSingle() { Single<Hello> single = hystrixClient.failSingle(); assertNotNull("single was null", single); Hello hello = single.toBlocking().value(); assertNotNull("hello was null", hello); assertEquals("message was wrong", "fallbacksingle", hello.getMessage()); } @Test public void testHystrixFallbackCommand() { HystrixCommand<Hello> command = hystrixClient.failCommand(); assertNotNull("command was null", command); Hello hello = command.execute(); assertNotNull("hello was null", hello); assertEquals("message was wrong", "fallbackcommand", hello.getMessage()); } @Test public void testHystrixFallbackObservable() { Observable<Hello> observable = hystrixClient.failObservable(); assertNotNull("observable was null", observable); Hello hello = observable.toBlocking().first(); assertNotNull("hello was null", hello); assertEquals("message was wrong", "fallbackobservable", hello.getMessage()); } @Test public void testHystrixFallbackFuture() throws Exception { Future<Hello> future = hystrixClient.failFuture(); assertNotNull("future was null", future); Hello hello = future.get(1, TimeUnit.SECONDS); assertNotNull("hello was null", hello); assertEquals("message was wrong", "fallbackfuture", hello.getMessage()); } @Test public void testHystrixClientWithFallBackFactory() throws Exception { Hello hello = hystrixClientWithFallBackFactory.fail(); assertNotNull("hello was null", hello); assertNotNull("hello#message was null", hello.getMessage()); assertTrue("hello#message did not contain the cause (status code) of the fallback invocation", hello.getMessage().contains("500")); } @Test public void namedFeignClientWorks() { assertNotNull("namedHystrixClient was null", this.namedHystrixClient); } @Test public void testHystrixSetterFactory() { HystrixCommand<List<Hello>> command = this.hystrixSetterFactoryClient .getHellosHystrix(); assertNotNull("command was null", command); String setterPrefix = TestHystrixSetterFactoryClientConfig.SETTER_PREFIX; assertEquals( "Hystrix command group name should match the name of the feign client with a prefix of " + setterPrefix, setterPrefix + "localapp5", command.getCommandGroup().name()); assertEquals( "Hystrix command key name should match the request method (space) request path with a prefix of " + setterPrefix, setterPrefix + "GET /hellos", command.getCommandKey().name()); List<Hello> hellos = command.execute(); assertNotNull("hellos was null", hellos); assertEquals("hellos didn't match", hellos, getHelloList()); } @Data @AllArgsConstructor @NoArgsConstructor public static class Hello { private String message; } @Configuration public static class TestDefaultFeignConfig { @Bean Logger.Level feignLoggerLevel() { return Logger.Level.FULL; } } // Load balancer with fixed server list for "local" pointing to localhost @Configuration public static class LocalRibbonClientConfiguration { @Value("${local.server.port}") private int port = 0; @Bean public ServerList<Server> ribbonServerList() { return new StaticServerList<>(new Server("localhost", this.port)); } } }